Skip to content

security: guessable default credentials ship in templates, ansible defaults and the compose file #16299

Description

@mrveiss

Found by the #16275 audit (11 Sep 2026). This is a public repository, so any credential with a guessable default is a known credential on every install that never overrides it.

Where What Notes
.env.docker:26 GRAFANA_ADMIN_PASSWORD defaults to the app's name #16275 made docker/generate-secrets.sh generate it. The tracked template default is unchanged, because a project hook blocks every .env* edit.
docker/.env.docker:102 AUTOBOT_DB_PASSWORD defaults to the app's name Same as above. An existing deployment must keep the password its Postgres volume was created with.
docker-compose.yml SLM_ADMIN_PASSWORD:-admin, plus the :-autobot fallbacks Not changed: any edit to this file fails the hardcoded-values hook on 9 pre-existing port literals, and the hook's baseline forbids adding entries for a file the change touches.
autobot-slm-backend/ansible/roles/backend/defaults/main.yml:108-109 backend secret key and JWT secret are change-me
autobot-slm-backend/ansible/roles/monitoring/defaults/main.yml:26 grafana_admin_password: admin
.env.example the VNC passwords (VITE_*) are guessable As VITE_* values they are compiled into the frontend bundle, so a real value would ship to every browser.
docker/generate-secrets.sh / docker-compose.yml AUTOBOT_SECRETS_ROOT_KEY is generated but never passed to any service A gap in existing wiring.

Whether real deployments override these has not been measured.

Acceptance criteria

  • Each listed default is either generated on first run, or refuses to start while still the default (with the reason written next to it)
  • No VITE_* variable carries a credential; VNC auth moves server-side or behind the SLM
  • AUTOBOT_SECRETS_ROOT_KEY reaches the services that need it, or its generation is removed with a reason
  • The two hook conflicts that blocked security: an auto-generated Fernet credential key has been tracked in this public repo since Oct 2025 #16275 are resolved: .env* edits for templates, and the compose file's pre-existing port literals

Activity

  1. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Acceptance criterion added, 2026-09-11 (from #16300's closure review):

    • Every allowlisted .secrets.baseline entry has a tracked reason. The baseline format has no reason field, so the reasons need a companion record. The template's weak default passwords are marked as pending this issue, never silently allowed. A guard fails any entry that has no reason.
  2. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Facts and decisions (2026-09-11), before any code

    These are from a read-only sweep. Every claim below was re-checked against the tree.

    Correction to the issue's table. AUTOBOT_SECRETS_ROOT_KEY does reach its consumers:

    • docker/with-secrets.sh:38-39 exports it for backend, worker and slm; it is their entrypoint (docker-compose.yml:214).
    • autobot_shared/secrets_envelope.py:125 (load_root_key) requires it.
    • repo_tests/secrets_root_key_provisioned_test.py guards it.

    The compose file never names it, because the wrapper loads it from the secrets volume. So AC3 is met by existing wiring, with no code needed. It gets ticked with this evidence against merged base.

    AC5. .secrets.baseline is a detect-secrets v1.5.0 baseline with 0 entries, and nothing enforces it. The only reader, stranded_recorder_entries_test.py, just parses it. So AC5's reason record applies to the audited baseline #16275 introduces, and it will be built from that baseline's actual contents.

    Engineering, no decision needed:

    • Compose. 9 port literals (lines 50, 87, 291, 306, 697, 737, 749, 776 and 787) fail the hardcoded-values hook whenever the file is staged. They can't be baselined in the same change (check_baseline_no_growth.sh:330-337), so they get fixed at the source.
    • Ansible. The defaults for backend_secret_key, backend_jwt_secret and grafana_admin_password move to generate-or-reuse, the same way the DB password already works (roles/postgresql/tasks/databases.yml:27-49). An assert refuses a known default.

    Decision 1: AUTOBOT_DB_PASSWORD on existing compose installs

    POSTGRES_PASSWORD=${AUTOBOT_DB_PASSWORD:-autobot} (docker-compose.yml:93) applies only when the volume is first initialised. Neither generator creates it; ansible installs already generate or reuse it. No rotation mechanism exists. The builtin updater's only generic hook is the per-role post_sync_cmd (services/sync_orchestrator.py:203-229).

    • A: A builtin-updater step re-keys the existing role. If the password is still the default, it generates one, re-keys through the updater's own step, persists it to the secrets volume and restarts the consumers. Never a manual ALTER ROLE. This fixes every install.
    • B: Existing installs keep their current password, and only new installs generate one. Existing installs stay on a publicly known password.
    • C: Refuse to start while the default is in use, and the operator re-keys. Existing installs break until someone acts.

    Recommendation: A.

    Decision 2: the .env* edit block

    .claude/hooks/protect-files.sh:108-109 denies every .env, .env.*, */.env and */.env.* path. It blocks tracked templates (.env.docker, .env.example, docker/.env.docker and so on) exactly like real secret files, and it protects itself (:112-113). Only the owner can change it.

    • A: The owner allows edits to an explicit list of tracked templates only. Real .env files stay blocked.
    • B: Keep the hook as it is. The owner applies the template edits by hand from a patch in the PR.
    • C: Don't edit the templates. Startup refuses the known template defaults instead.

    Recommendation: A, as the narrowest change.

    Decision 3: VNC credentials

    VITE_{DESKTOP,TERMINAL,PLAYWRIGHT}_VNC_PASSWORD (.env.example:462-468) are compiled into the frontend bundle and sent as a URL query parameter (AppConfig.js:211,219; useChatStore.ts:743). The path is marked DORMANT (#1130; re-integration is #5136). The backend /vnc-proxy is authenticated, but it passes the page through with no password. The SLM has an encrypted VNC credential store with a one-time token exchange (services/vnc_credentials.py, api/vnc.py).

    Recommendation: A.

    These are being put to the owner now, and the rulings will be recorded here.

  3. added
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Sep 11, 2026
  4. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Owner rulings (2026-09-11), given in the implementing session

    1. DB password: option A. A builtin-updater step re-keys the existing role when it is still the default. It generates a password, re-keys through the updater's own step, persists the result to the secrets volume and restarts the consumers. Never a manual ALTER ROLE.
    2. .env* hook: option A. Edits are allowed to an explicit list of tracked template files only; real .env files stay blocked. The hook protects its own file (protect-files.sh:112-113), so the PR carries the exact hook change for the owner to apply by hand. No session edits the hook.
    3. VNC: option A. The authenticated backend /vnc-proxy injects the VNC password server-side from the secrets manager. The frontend never holds it, and the VITE_* VNC password variables go.

    AC3 is already met by existing wiring (see the comment above). Implementation starts after #16298, #16259 and #16266 land.

  5. removed
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Sep 11, 2026
  6. mrveiss commented on Sep 12, 2026

    @mrveiss
    OwnerAuthor

    The implementation plan found three places where a recorded ruling conflicts with the current code. Each needs an owner decision before its PR starts. Nothing is implemented yet; work begins after #16298 and #16259 land, per the earlier comment.

    1. DB re-key (ruling: "the updater re-keys it"). The builtin updater has no path into a compose install.
    Self-update runs update-all-nodes.yml against the SLM host, and post_sync_cmd reaches nodes over SSH. Neither can reach the compose install:

    • docker-compose.yml mounts no docker socket.
    • The secrets volume is read-only in backend and SLM; only autobot-secrets-init writes it.
    • The "docker" infra role points at a deprecated playbook.

    No builtin step can persist a new password or restart the consumers. Per the updater rule, this is a gap to fix, not route around.

    • (a) Recommended: file a precursor issue, "the builtin updater has no compose path". Block only the DB PR on it; the other three PRs proceed.
    • (b) Re-key at container start: secrets-init generates the password, and a one-shot ALTER ROLE runs while the default still authenticates. This is a boot-time migration rather than an updater step. I have not verified that it's feasible.

    2. VNC (ruling: "the backend proxy injects it"). Injecting into the page gives the browser the password.
    get_vnc_client only passes vnc.html through. Putting the password into that page or the query string delivers it to the client. The only fully server-side point is the websocket relay: there the proxy answers the VNC server's password challenge itself and offers the browser no-auth.

    • (a) Recommended: handshake in the relay, in a new module so vnc_proxy.py stays under 600 lines. The challenge needs DES. I have not verified that the pinned cryptography still ships TripleDES.
    • (b) Inject into the page. The password reaches the browser, which defeats the ruling's intent.

    3. VNC password source. Nothing stores it in the canonical secrets manager today.
    It lives in a node-local env file written by the vnc and browser roles, and is registered only in the SLM's own credential store.

    • (a) Recommended: Ansible writes it to the backend's SYSTEM vault through the service secret-create path, and the proxy reads it through SecretsCoordinator. Two copies exist until the SLM store reads from the same vault; I'd file that follow-up.
    • (b) The proxy reads it from the SLM store over its API. That is a cross-service call on every VNC connect.

    Other findings, no decision needed:

    • AC5: .secrets.baseline holds 1,331 entries, all is_secret:false. They include the five weak defaults this issue names, so they are silently allowed today. The plan is a companion reasons file keyed by (file, type, hashed_secret), plus a guard that fails on an entry with no reason. Removing the five entries leaves 1,326, still above the rescan floor of 1,300.
    • AC3 is already met on base: docker/with-secrets.sh exports the root key, and the backend, SLM and worker entrypoints use it.

    Proposed split:

    1. The AC5 guard.
    2. Deploy config: port literals, Ansible generate-or-reuse, the .env* hook patch for the owner to apply, Grafana and SLM-admin defaults.
    3. VNC.
    4. DB re-key, blocked on conflict 1.
  7. added
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Sep 12, 2026
  8. added this to the v0.9.0 milestone on Sep 12, 2026
  9. mrveiss commented on Sep 17, 2026

    @mrveiss
    OwnerAuthor

    Owner decision, 2026-09-17: generate on first run.

    Defaults are replaced with a real generated secret at install time, in the manner docker/generate-secrets.sh already does for the credentials it covers. No refuse-to-start gate, and no grace-period deadline.

    What this means for each listed default: it must be generated at install rather than shipped with a guessable value — the Grafana admin password, the database password, the compose SLM_ADMIN_PASSWORD and :-autobot fallbacks, and the ansible change-me backend secret key and JWT secret.

    The consequence to state plainly, because the decision accepts it: a deployment already running on a shipped default keeps running on it. Generation applies at install time, so it fixes new installs and does not reach existing ones. Since whether real deployments override these has not been measured, the size of that residue is unknown. This is a deliberate trade — no existing install is broken — not an oversight, and it should not be quietly re-litigated later as though it were.

    Two things are unaffected by the decision and still need doing:

    • AUTOBOT_SECRETS_ROOT_KEY is generated but never reaches any service. That is a wiring gap, not a default-value question. Either route it to the services that need it, or remove its generation with the reason recorded.
    • The VITE_* VNC passwords must not carry a credential at all. VITE_* values are compiled into the frontend bundle, so generating a strong one only means shipping a strong credential to every browser. Generation is the wrong fix here; the auth has to move server-side or behind the SLM. Generating these would make the issue look closed while shipping the secret exactly as before.

    The two hook conflicts that blocked #16275 still block this and need resolving first, since they are why the tracked template defaults are unchanged:

    • a project hook blocks every .env* edit, which is where two of the defaults live;
    • the compose file cannot be edited because the hardcoded-values hook fails on 9 pre-existing port literals, and its baseline forbids adding entries for a file the change touches.

    Neither is a reason to defer the work — they are the first part of it.

  10. removed
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Sep 17, 2026
  11. 15 remaining items

  12. mrveiss commented on Sep 28, 2026

    @mrveiss
    OwnerAuthor

    Chunking triage — a proposal, not an assignment

    Nothing was relabelled, moved or closed by this pass.

  13. modified the milestones: v0.9.0, v0.9-umbrellas on Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions