Skip to content

security(service-auth): where should the service keys' master copy live? The plaintext all-keys export is sanctioned by no decision #16366

Description

@mrveiss

Decision needed (owner)

Found while scoping #16348. It asks where the service keys' master copy should live, given the standing rule that required credentials are stored in AutoBot's secrets management system.

How service keys flow today

None of the hops goes through the secrets manager. File:line references are to base.

Hop Where Store
1. Generate generate_service_keys.py calls ServiceAuthManager.generate_service_key (autobot-backend/security/service_auth.py:55) Redis service:key:{id}, 90-day expiry, which the verifier trusts (service_auth.py:192)
2. Export _save_backup (generate_service_keys.py:~107) every live key in plaintext, written to config/service-keys/service-keys-<ts>.yaml. This is the artifact that leaked (#16301). Rotation never prunes old exports.
3. Rotate rotate-service-keys.yml:142-145 runs the generator with chdir: {{ autobot_root }} (hard-coded /opt/autobot, :88) (the file above)
4. Deploy role service_auth: service_auth_keys_source_dir (defaults/main.yml:25), then deploy-keys.yml:35-75 picks the newest YAML /etc/autobot/service-keys/<id>.env, mode 0600
5. Runtime systemd EnvironmentFile=; utils/service_client.py:341-367; the NPU worker reads its .env env and Redis

What's already decided: umbrella #10088's bootstrap constraint makes the per-host .env files and the auth-bootstrap keys an irreducible file-based set, because you must authenticate before you can call the secrets service. So hops 4 and 5 are sanctioned.

What isn't decided anywhere: the all-keys plaintext export (hop 2). #16300 and #16301 reused it without making a storage choice.

Options

  1. Keep the file export, made safe (security(service-auth): generate_service_keys.py writes its live-key export relative to the working directory, so a run inside a checkout drops keys into the work tree #16348 as written): an explicit directory from SSOT, refusal inside a git work tree, prune to the newest export, and the rotation directory pinned to the role's directory. There's no architecture change, and the plaintext master copy remains on the SLM host.
  2. Move the master copy into the system vault (/api/v2/secrets/system, the envelope store, Umbrella/PRD: unify AutoBot secrets — one RBAC-governed store, vaults + one key abstraction (replaces 11 stores, 5 keys, 2 UIs) #10088). The generator writes each key to the vault, and Ansible renders the 0600 .env files from the vault rather than from a YAML export. That removes the plaintext master copy. But it conflicts with Umbrella/PRD: unify AutoBot secrets — one RBAC-governed store, vaults + one key abstraction (replaces 11 stores, 5 keys, 2 UIs) #10088's classification of service-auth keys as irreducibly file-based, so Umbrella/PRD: unify AutoBot secrets — one RBAC-governed store, vaults + one key abstraction (replaces 11 stores, 5 keys, 2 UIs) #10088's bootstrap section would need amending, e.g. vault for the master copy, files only on the hosts that use them. It also introduces a chicken-and-egg problem on first install, when there is no key to reach the vault yet.
  3. Drop the export entirely. Deploy reads the keys from Redis on the SLM host at rotation time and writes the .env files directly, with no file at rest outside /etc/autobot/service-keys/.

Recommendation: land 1 now as #16348, since it stops the leak class whatever is decided, and take 3 as the target. It removes the plaintext master copy without the vault bootstrap problem, and it stays within #10088's recorded classification. Choose 2 if you want every credential's master copy in the vault regardless.

Also found (not a decision, but needs an owner)

  • The generator creates a key for slm-backend (generate_service_keys.py:43), but the role's service map (roles/service_auth/defaults/main.yml:30-42) has no slm-backend entry. That key is generated and exported but never deployed to a .env file. Is it dead, or is a consumer missing?
  • docs/developer/THREAT_MODEL.md §4 (secrets) mentions neither the envelope store nor service keys.

Refs #10088, #16348, #16301, #16300, #12684

Activity

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

    @mrveiss
    OwnerAuthor

    Owner direction, 2026-09-11 (still under discussion, not final): leaning towards the system vault as the master copy of service keys, for scalable infrastructure, with a master (root) key rotation option designed in.

    The sketch under discussion:

    The open sub-decisions are where the root key lives (a host env file now, with a KMS later via #10088 Task 1.5), the rotation mechanism, the trigger, and the first-step scope. A read-only fact-finding pass on the envelope store's current key-versioning support is running. Interim: #16372 (the safe export) still merges, because it reduces the risk today.

  3. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Owner decision, 2026-09-11: the phased design is filed as #16406 (a sub-issue of #10088). Phase 1 is a versioned key ring plus bulk re-wrap, which gives root-key rotation. Phase 2 is a vault write path from the SLM host. Phase 3 is service keys mastered in the vault. KeePass (the owner's suggestion) is evaluated there, as the operator-held escrow and backup for the root key, not as the runtime store. #16372's hardened export is the interim until phase 3.

  4. removed
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Sep 11, 2026
  5. added this to the Backlog milestone on Sep 12, 2026
  6. added
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Sep 14, 2026
  7. mrveiss commented on Oct 3, 2026

    @mrveiss
    OwnerAuthor

    Closed — the question was answered and the work it produced is tracked elsewhere

    This issue asked where the service keys' master copy should live. It is a decide
    issue, and the decision exists:

    Owner decision, 2026-09-11: the phased design is filed as #16406 (a sub-issue
    of #10088).

    It carries no unticked acceptance criteria — its deliverable was an answer, and the
    answer produced a design issue with a parent. That is the correct ending for a decision
    issue: the ruling is recorded here, the engineering lives at #16406, and #10088 holds
    the parentage so neither is orphaned.

    Why this stayed open is the general case, not an oversight. A decide issue has no
    code to merge, so nothing auto-closes it and no acceptance checklist goes green — the
    mechanisms that close ordinary issues do not apply, and nobody is prompted. It has been
    answered for three weeks. Tonight's triage of the needs-decision label found this shape
    repeatedly: issues ruled on and left open because closing them is a separate manual act
    nothing asks for.

    Also removing the needs-decision label, which is now false and makes the real queue of
    unanswered questions look larger than it is.

    Found by the autobot-marketing-a5 triage of the needs-decision backlog; the decision
    quote re-verified here.

  8. removed
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Oct 3, 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