security(redis): P1b — SLM-generated Redis password, every consumer authenticates as default while the server stays nopass (#13568) #16627
Description
Activity
Requirement for this phase, from the #16626 review:
roles/backend/templates/backend.env.j2puts the Redis password, and since #16626 the username, intoAUTOBOT_REDIS_URLwithout URL-encoding.Today that is harmless. The password is empty, and
AUTOBOT_REDIS_URLis only checked for presence (autobot-backend/startup_validation.py:40); no client connects with it. Once this phase renders the SLM-generated password there, a:,@or/in the password breaks the URL.Do one of the following:
- generate the password from a URL-safe alphabet, such as hex or
secrets.token_urlsafe; - encode both fields in the template;
- drop the composite URL, if its presence check can take the separate fields instead.
ssot_config.redis_url_with_authalready encodes both credentials as of #16626.- generate the password from a URL-safe alphabet, such as hex or
- added a commit that references this issue
on Sep 13, 2026 Owner decisions for P1b, 2026-09-13:
- The secret is read on the SLM host. The shared read task reads the SLM-generated password from the SLM's own secrets file on the controller, once per run. The SLM runs every playbook, so every node gets the same value, remote nodes included. This departs on purpose from the security(chromadb): no CHROMA_SERVER_AUTHN configured — any internal-network container can reach chroma unauthenticated #12513 chroma pattern, which reads on each target and so only reaches hosts that carry the file. When a run has no secrets file, nothing is adopted and nothing changes.
- Windows NPU worker delivery is automated in P1b. The worker reads its Redis credentials from a local bootstrap config, and nothing delivered them until now. This PR adds that delivery path, so the worker also authenticates as
defaultwith the password.
Owner decision, 2026-09-13, which refines the Windows NPU answer above: P1b strips the Redis password from the unauthenticated
POST /api/npu/workers/bootstrapresponse (#16657). Windows workers getdefaultplus the password through the admin-only pair and repair push. An authenticated bootstrap now blocks P2 (#16628).Owner decision, 2026-09-13, which refines the Windows NPU answer again: the worker ignores a Redis config pushed at pairing, and wiring it up while
/pairis unauthenticated (#16658) would let anyone who can reach a worker redirect its Redis. So P1b adds thedefaultusername to the admin pair/repair payloads and changes no worker code. Windows workers keep connecting without Redis auth, which is fine while the server staysnopass. Delivery moves to #16658, which now blocks #16628.- added 4 commits that reference this issue
on Sep 13, 2026 - added a commit that references this issue
on Sep 14, 2026 AC evidence against origin/main after #16660 (07815fd) and #16679 (a494d4c). Two of four ticked.
AC status evidence / gap The SLM generates the password once and never regenerates or blanks it. Roles read it and never generate it. ticked autobot-slm-backend/ansible/roles/slm_manager/tasks/main.yml:429-432keeps a non-emptyslm_redis_passwordand otherwise callslookup('password', '/dev/null length=48 chars=ascii_letters,digits').slm-secrets.envis templated onlywhen: not slm_secrets_stat.stat.exists(:452, :463). For an existing file, the key is back-filled only whengrep -q '^AUTOBOT_REDIS_PASSWORD=.\+'(:540) fails (when: … _slm_redis_pw_check.rc != 0, :581-583). Test:tests/test_redis_password_provisioned_16627.py:152test_no_consumer_generates_a_redis_password.Every consumer in the #13568 inventory authenticates as defaultwith the passwordnot ticked Not every consumer is verified. The deploy playbooks (inventory 10–11) are the open follow-up #16678. The NPU worker's credential path (inventory 4) is #16657, which is open. I could not determine the redis-replicationrole's password source (inventory 8).SLM replication and backup run redis-cli --userwith the canonical passwordticked services/replication.py:649and:772auth = redis_cli_auth(redis_password), with the password from:488ssot_config.redis.password.services/backup.py:528auth = redis_cli_auth().services/redis_cli_auth.py:46falls back tossot_config.redis.password, and:49setsusername = ssot_config.redis.username or "default". The source is canonical:slm-secrets.env.j2:38AUTOBOT_REDIS_PASSWORD={{ slm_redis_password }}is loaded byautobot-slm-backend.service.j2:48EnvironmentFile=(defaultslm-secrets.env,defaults/main.yml:20-21) and read atautobot_shared/ssot_config.py:762alias="AUTOBOT_REDIS_PASSWORD". Test:tests/test_redis_cli_auth_16627.py:51assertsredis-cli --user default INFO replication(:58). Out of scope: backup's verify command atbackup.py:327(redis-cli PING && redis-cli DBSIZE) sends no auth. It is one of the six no-auth sites that #13568 assigns to #16628.Host evidence: every consumer authenticates against the still-nopass server not ticked Needs host evidence. Code cannot show it. Found during this audit: #16686.
deploy-hybrid.sh:151anddeploy-native.sh:175write a literal Redis password with no username.Checked the 4 local branches assigned against this issue (
pr16660latest3,pr16660new,pr16660review,pr16660latest2) — all four are stale local snapshots of PR #16660, which already merged 2026-09-14. Ran the file-level supersession test: every core file they introduce (services/redis_cli_auth.py,ansible/_shared/tasks/read_redis_password.yml,services/backup.py,services/replication.py,tests/test_redis_cli_auth_16627.py, the changelog entry) is byte-identical between the branch tips andorigin/main. The ansible role files that differ do so only from ordinary later drift — spot-checkedroles/slm_manager/tasks/main.yml(the largest diff) and confirmed theslm_redis_passwordlogic is still there, just reformatted/extended since. Nothing to consolidate; none of the four add anything main doesn't already have. Not reviving any of them.On the two open ACs from the 2026-09-14 audit comment:
AC2 (every #13568-inventory consumer authenticates as
default) — resolved the one item that comment left as "could not determine": inventory item 8, theredis-replicationrole, has no password source because it is not wired into anything. Grepped every playbook'sroles:list and everyinclude_role/import_role/string reference acrossautobot-slm-backend/ansible/forredis-replication/redis_replication— it appears nowhere outside its own role directory.provision_host.yml/recover_host.ymlboth userole: redis(the regular role, already fixed here), neverredis-replication. So this isn't an undetermined password source, it's an unwired role that never runs. Filed #17176 rather than guessing at whether it should be wired in or retired — that's an owner call. Items 10-11 (deploy playbooks, #16678) and item 4 (NPU worker, #16657) are still tracked as their own open issues, unchanged by this pass.AC4 (host evidence) — still needs the owner or a live host; nothing I can add by reading source.
Not closing — two ACs remain genuinely open, tracked precisely by #16678, #16657, and now #17176.
Blocked by #17434 — this issue's mechanism does not reach the server.
Host evidence from a role-provisioned node: the running Redis Stack reports
config_file:/opt/redis-stack/etc/redis-stack.conf(23 bytes,port 6379+daemonize yes, no include), whileroles/redis/tasks/main.yml:162renders its 2933-byte config to/etc/redis-stack.conf. The unit'sExecStartpasses no config argument, so the rendered file is never read.redis-cli CONFIG GET requirepass -> ""So
requirepassis currently empty on a node where the role ran, and rendering it into/etc/redis-stack.conf— which is what this issue's approach does — would not change that. The path question in #17434 has to be settled first, or a green render here will read as done while the server stays unauthenticated.Bounded: one node checked, not the
database-role node. #17434 AC1 is that check.Correction to my earlier comment — the empty
requirepasshas TWO independent causes and #17434 fixes only one. Credit to e5, who found this while answering #17434's AC1; I verified it before posting.I wrote that rendering
requirepassinto/etc/redis-stack.confwould not take effect because nothing reads that file. True, but incomplete:requirepassis not in the render at all.roles/redis/templates/redis-stack.conf.j2:72-74:{% if redis_password is defined and redis_password %} requirepass {{ redis_password }} {% endif %}roles/redis/defaults/main.yml:10setsredis_password: ""— falsy, so the block is skipped. The one inventory that overrides it,group_vars/database.yml:37, setspassword: "{{ vault_redis_password }}", andvault_redis_passwordis referenced in four files and defined in none:grep -rln vault_redis_password -> roles/redis/vars/main.yml inventory/group_vars/database.yml roles/ai-stack/defaults/main.yml roles/redis/tasks/code_only.yml grep -rn '^vault_redis_password' -> (no definition)So the two causes are independent and both must clear:
cause owner state the rendered config is never read #17434 direction settled — canonical is /etc/redis-stack.conf; e5 has a branchthere is no password value to render this issue open #17434 does not set a password. It removes the reason a password could not take effect — which is what this issue needs, and is why it is a blocker rather than a duplicate.
The practical consequence for closing this issue: a green render is not evidence. The acceptance criterion has to be
CONFIG GET requirepassreturning a non-empty value from a provisioned node, because a template whose conditional is false produces a perfectly valid file with no authentication in it and nothing anywhere reports a problem.- addedblocked: observation-gatedCode remains, but cannot be written until one observation answers a questionCode remains, but cannot be written until one observation answers a question
on Oct 3, 2026 Host evidence for the final AC — the server is still
nopassas the design assumes, but consumers are not authenticating; they are landing ondefaultanonymouslyRead-only from a live
database-role node. This settles half of the last criterion and falsifies the other half, so I am not ticking it.The server half — confirmed, exactly as the design assumes
$ redis-cli PING # no AUTH sent PONG $ redis-cli ACL LIST user default on nopass sanitize-payload ~* &* +@allStill
nopass. The rendered config carries norequirepassdirective at all — the template gates it on{% if redis_password is defined and redis_password %}and that branch was false, leaving a# Authenticationheader with nothing under it. Soredis_passwordis empty on this node and the SLM-generated password of AC1 has not reached it.The consumer half — not met, and the gap is quantified
Every connected client is the
defaultuser:$ redis-cli CLIENT LIST | grep -oE 'user=[^ ]+' | sort | uniq -c 24 user=default26 of them identify as
redis-py. And there are no connection failures:Metric Value connected_clients24 total_connections_received42,528 (over uptime_in_days:1, 145,212 s)rejected_connections0 total_error_replies141 But "every consumer is
default" is not "every consumer authenticates asdefault", and this AC asks for the second. Against anopassserver, a client that sendsAUTH default <password>is refused. Probed directly:$ redis-cli AUTH "" ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?That is an
ERRreply, so it lands inerrorstat_ERR. If consumers were sendingAUTHon connect, the ERR count would track the connection count. It does not:errorstat_ERR:count=125 vs total_connections_received:42528125 against 42,528. At most ~0.3% of connections could have carried an
AUTH, and that is an upper bound —ERRalso counts unrelated failures, so the true figure is lower still. The conclusion the numbers support is that consumers connect anonymously and are assigneddefaultbecause that is what an unauthenticated connection gets, not because they authenticate as it.Why this matters to this issue specifically
AC1 is ticked: the SLM generates the password and roles never blank it. The remaining criteria are about consumers being ready for that password. The evidence says they are not — the
user=defaultreading looks like readiness and is not. The momentrequirepassis rendered, these 24 clients break, because nothing in the current traffic is sending credentials.That is precisely the failure #16628 exists to prevent, and it is the reason its ordering matters: consumers must authenticate before the server stops being
nopass, and theuser=defaultmetric cannot tell you whether they do.Criteria status
- AC2 — every consumer in the Database role: Redis config + exporter unit still have no update path (credential has no source) #13568 inventory authenticates as
defaultwith the password — not met. The live traffic shows noAUTHat a rate consistent with any consumer sending one. Which specific inventory members do and do not sendAUTHI could not determine from server-side stats:errorstat_ERRaggregates by error code, not by message or client, so I cannot attribute the 125. That needs a client-side audit of the inventory, not another host read. - AC4 — host evidence: every consumer authenticates as
defaultagainst the still-nopassserver, with no connection errors — not met. The "still-nopass" premise and the "no connection errors" clause are both confirmed (rejected_connections:0); the "authenticates" clause is contradicted by the ERR-to-connection ratio.
Leaving both unticked and the issue open.
Context
- security(redis): the role's Redis Stack config is never read — requirepass is empty on a provisioned node, and one path has four spellings #17434 closed today. Its AC1 is answered: the daemon reads
/etc/redis-stack.conf(INFO server→config_file), and the drop-in now passes it. This issue's design does not change — arequirepassrendered there will take effect. That was the open question its AC6 routed here. /etc/redis-stack/and/etc/redis/do not exist on the node, so any older code path writing a password to either spelling reached nothing.
Read-only:
PING,ACL LIST,CLIENT LIST,INFO, and oneAUTH ""probe that sent no credential. No value was read or printed;ACL LISToutput was redacted of any hash before quoting. Nothing was written and no service was touched.- AC2 — every consumer in the Database role: Redis config + exporter unit still have no update path (credential has no source) #13568 inventory authenticates as
Scope
Phase P1b of #13568, which follows P1a.
_shared/tasks/read_redis_password.ymladopts it only when non-empty.defaultplus that password. The server staysnopass, which acceptsAUTH default <any>, so nothing breaks.Acceptance criteria
defaultwith the password. That includes SLM replication and backup, which read the canonical source instead of grepping a config file. This is bug(slm): replication and backup read the Redis password from /etc/redis/redis.conf, a file roles/redis never renders #16625's AC1.services/replication.py,services/backup.py) runredis-cli --userwith the canonical password. This moved from security(redis): P1a — optional username support in every Redis consumer, default unset (#13568) #16626.defaultagainst the still-nopassserver, with no connection errors.