Repository navigation
security(redis): the rendered config is open by default — wildcard bind, protected-mode off, and a requirepass that is silently omitted #17551
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Sep 26, 2026 - added a commit that references this issue
on Sep 26, 2026 Gate landed in #17444 (
1db87b0c922f), and host evidence raises the severityVerified the finding independently before acting, then checked it against a
provisioned node rather than only the templates. The host evidence is worse
than the template reading suggested.What the daemon reads today
The packaged fallback the unit actually starts with contains one directive:
port 6379No
bind, noprotected-mode, norequirepass. So on a provisioned node,
right now:Bind no directive → all interfaces (confirmed listening on 0.0.0.0:6379)Authentication none protected-modedefaults to yesThe store is already wildcard-bound and passwordless.
protected-modeis the
only control holding it shut. That is not a latent risk in a template — it is
the current state of a running node.The rendered-but-unread config carries
bind 0.0.0.0,protected-mode noand
norequirepass. #17434 is what makes the daemon read it. So the ungated change
would have switched off the single control currently protecting the store —
turning "refuses every non-loopback connection" into "accepts anyone".Why the gate is not a behaviour change
Because remote access does not work today (protected-mode is on by default),
keepingprotected-mode yeswhen no password resolves reproduces current
behaviour exactly: loopback serves, everything else is refused. There is no
regression to weigh against the safety — the alternative was not "keep working",
it was "start answering strangers".With a password the intended
protected-mode nois restored unchanged.What shipped
One trimmed expression,
_redis_auth, answers "is this authenticated?" for both
protected-modeandrequirepass, so the two cannot disagree.
default('', true)substitutes on any falsy value — without the boolean
argument| stringturns0andFalseinto the truthy strings"0"and
"False", which would reopen the store on a config that set the key to a falsy
scalar. Trimming matches the role's own convention attasks/main.yml:249.repo_tests/redis_unauthenticated_conjunction_17551_test.pyasserts the
conjunction, as this issue's last criterion asks — not the three directives
separately. Five falsy password spellings, a negative control proving the
detector can see an open config, and a contrast pair proving the guard does not
simply pinprotected-modeon. Three mutations bite: restoring the
unconditional directive (5 failures), dropping the trim (1), dropping the
boolean default (3).Two things deliberately NOT done here
The real credential.
redis_passwordresolves throughvault_redis_password,
which nothing assigns. The SLM-generated secret is not a counterexample:
tasks/main.yml:246assigns it toredis_client_password— the client
credential — and reads it after this template renders. The server stays nopass.
That split is #16627 and this gate does not close it; it makes the absence
of a password safe instead of silent.A tree sweep asserting the vault key is never assigned. That needed a
*.yml
glob, andrepo_tests/_glob_declared_uncovered.pysays not to add an entry
there to make a new uncovered dependency pass. The evidence is recorded here
instead; the test checks the part the gate depends on — thatredis_passwordis
still an indirection with an empty fallback. Losing the sweep costs nothing: an
assigned vault key makes the store safer and the conjunction guard holds either
way.Remaining on this issue
The conjunction is now guarded at render time. What is still open is giving the
store an actual credential soprotected-mode nobecomes legitimate rather than
merely unreachable — which is #16627's subject, and the reason this issue should
stay open after #17444 merges.Correcting my own comment above, and the gate is reverted
My earlier comment on this issue said, of a provisioned node:
protected-modedefaults toyes… The store is already wildcard-bound
and passwordless.protected-modeis the only control holding it shut.That is wrong. Measured on the node rather than reasoned from the packaged
config:protected-modeisno. There is no such control.I had inferred it from the packaged configuration file containing only
port 6379, and concluded the default applied. It does not, because the setting
never comes from a configuration file at all — the packaged launcher passes
--protected-mode noon the command line, unconditionally, after the config it
is handed. Right instrument, wrong layer: I read the file the daemon loads and
never read the command line that starts it.The consequence for this issue's fix
The gate merged into #17444 — rendering
protected-mode yeswhen no password
resolves — could never have worked. Redis documents command-line arguments
as producing "an in-memory temporary config file (possibly concatenating the
config file passed by the user, if any)", so the launcher's flag overrides
anything the template writes. The comment I shipped with it, claiming the store
would "serve loopback and refuse every other interface", was false.A gate that reports protection it does not provide is worse than no gate. It is
the same false-assurance shape this PR spent the day clearing, committed inside
the fix for it.Reverted at the owner's direction. #17444 is now the config-path fix and
nothing else: the_redis_authnormalisation and the protected-mode branch are
gone,test_redis_password_provisioned_16627.pyis restored tomain
unchanged, andredis_unauthenticated_conjunction_17551_test.pyis removed —
its conjunction assertion was measured against one of three renderers while
reading as a statement about the store, which CodeRabbit's finding about the
inline config indeploy-native-services.ymlexposed.What replaces it
#17572 records the measured state of a provisioned node — unauthenticated,
+@allon the default user, network segmentation as the only control — filed
as a sub-issue of #16627, which owns the missing credential. The launcher
override is documented there so the next person does not try to fix this in the
config layer either.This issue's own remainder is unchanged: the rendered config should not be able
to produce an open store, and that has to be asserted at every renderer,
not one.
The exposure
autobot-slm-backend/ansible/roles/redis/templates/redis-stack.conf.j2renders aRedis Stack config with these three properties at once:
:5bind 0.0.0.0:11protected-mode no:79-81{% if redis_password is defined and redis_password %}requirepass {{ redis_password }}{% endif %}requirepasswhen the password is emptyAnd the password is empty, by construction rather than by accident:
vault_redis_passwordis never defined anywhere in the repository. Arepository-wide search for an assignment (
^\s*vault_redis_password\s*:) returnsnothing; every occurrence is a consumer, three of the four through
| default(''). Soredis_passwordresolves to the empty string, the Jinjaguard is false, and the rendered config contains no
requirepassline.The three together describe an unauthenticated Redis Stack listening on all
interfaces with the protocol-level guard switched off.
Why it has not bitten yet, and what changes that
The rendered template has not been the config the daemon reads. On
main,ansible/templates/systemd/redis-stack-server.service.j2:30starts the daemonwith a different path from the one the role renders to, so the rendered
content has never taken effect through that path. That mismatch is the subject of
#17434.
Fixing the mismatch is correct and should happen. The consequence worth deciding
deliberately is that it makes this configuration live: the same restart that
finally hands the daemon its rendered config is the restart that applies
bind 0.0.0.0+protected-mode no+ no password for the first time.What is verified here: the three directives, the empty-password chain, and
that the two paths differ on
main. All from the repository.What is not: whether any running node is exposed today by some other route.
That needs a readback from the host (
CONFIG GET bind,protected-mode,requirepass) and is not derivable from the repository. Not claimed either way —this is filed on the configuration as written, which is enough to act on.
Acceptance criteria
protected-mode noand an emptyrequirepassat the same time: the play fails with a named reason, ratherthan rendering a config that is open by default.
vault_redis_passwordeither resolves through the canonical secrets manageror its absence is a hard error on this path — never a silent
''.Per the standing rule, credentials come from the secrets manager, never a
bare value or a parallel path.
bindis not a wildcard unless authentication is in effect.three is defensible alone; only together are they an open instance, so a
guard that checks them one at a time cannot see this.
Note on the last criterion
This is the shape that made it survive review:
bind 0.0.0.0is ordinary for acontainerised service,
protected-mode nois the documented way to allow that,and an empty password is invisible because the Jinja guard omits the directive
rather than emitting an empty one. Nothing is wrong on any single line. The guard
therefore has to assert the conjunction.