Passwords/tokens today live in per-repo Gitea Actions secrets (7+ new *_S3_SECRET_KEY from #413 alone), deploy-shell exports (MinIO root), appsettings defaults (RabbitMQ dockerUser/dockerPassword!), and at least one token COMMITTED to git (the shared act_runner registration token, gitea-act-runner/docker-stack.yml — its own comment tracks rotation in README §9).
Decision: OpenBao (Linux Foundation fork of Vault; MPL open source — Vault itself is BUSL now). Rationale: PKI secrets engine covers the "certificate enforced" requirement natively (internal CA → replace generate-internal-certs.sh → mTLS later); AppRole gives per-service/per-runner machine identities with least-privilege policies (same model we just proved with scoped MinIO keys, but centralized + audited + rotatable); Raft HA scales 1→3 nodes; Vault-compatible API = mature .NET client (VaultSharp) + Keycloak OIDC for human SSO. Considered: Infisical (nicer UI, weaker PKI/EE-gated bits), Vaultwarden (right tool for HUMAN passwords — optional later addition, not the machine backbone), Conjur (stagnant).
Phase 1: CI secrets → Bao via AppRole; rotate the committed runner token
Phase 2: service config secrets via a SpikerSoft.Secrets host-builder seam (flag-gated, env fallback — #493 dual-run pattern)
Phase 3: PKI — internal CA, replace CI cert generation, mTLS for the stateful tier
Root-of-trust rule: Bao's own bootstrap (unseal shares, initial root token) lives OFFLINE, never in Bao, never in git.
Passwords/tokens today live in per-repo Gitea Actions secrets (7+ new *_S3_SECRET_KEY from #413 alone), deploy-shell exports (MinIO root), appsettings defaults (RabbitMQ dockerUser/dockerPassword!), and at least one token COMMITTED to git (the shared act_runner registration token, gitea-act-runner/docker-stack.yml — its own comment tracks rotation in README §9).
**Decision: OpenBao** (Linux Foundation fork of Vault; MPL open source — Vault itself is BUSL now). Rationale: PKI secrets engine covers the "certificate enforced" requirement natively (internal CA → replace generate-internal-certs.sh → mTLS later); AppRole gives per-service/per-runner machine identities with least-privilege policies (same model we just proved with scoped MinIO keys, but centralized + audited + rotatable); Raft HA scales 1→3 nodes; Vault-compatible API = mature .NET client (VaultSharp) + Keycloak OIDC for human SSO. Considered: Infisical (nicer UI, weaker PKI/EE-gated bits), Vaultwarden (right tool for HUMAN passwords — optional later addition, not the machine backbone), Conjur (stagnant).
Phases (tickets follow the #515 pattern):
- Phase 0: deploy the vault (stack + init/unseal runbook + Keycloak OIDC + audit)
- Phase 1: CI secrets → Bao via AppRole; rotate the committed runner token
- Phase 2: service config secrets via a SpikerSoft.Secrets host-builder seam (flag-gated, env fallback — #493 dual-run pattern)
- Phase 3: PKI — internal CA, replace CI cert generation, mTLS for the stateful tier
Root-of-trust rule: Bao's own bootstrap (unseal shares, initial root token) lives OFFLINE, never in Bao, never in git.
QA Team — inventory input for this epic, 2026-07-14: a full fact-checking audit of every spikersoft-backend project (README rebuild, 100+ agents reading every csproj/appsettings/Program.cs) surfaced live-looking credentials committed in appsettings.json files. Types and locations only (values intentionally not repeated here):
Keycloak client secret for spikersoft-backend committed identically in all three appsettings variants of at least one project.
MinIO per-service keys (uploads-svc, metadata-svc) present in swarm service env (visible to anyone with docker service inspect).
Jetson-influx .env-10N files on the shared mount carry plaintext InfluxDB creds (already flagged in #554's thread).
Suggested handling within the epic: treat all of the above as compromised-by-default (they're in git history regardless of what happens to the working tree) — rotation list for Phase 1/2, and the appsettings files should end up with empty placeholders + README guidance, which the new per-project READMEs now provide hooks for. The full per-project question list from the audit (includes several more config-hygiene items) is with @spikerj.
**QA Team** — inventory input for this epic, 2026-07-14: a full fact-checking audit of every spikersoft-backend project (README rebuild, 100+ agents reading every csproj/appsettings/Program.cs) surfaced **live-looking credentials committed in appsettings.json files**. Types and locations only (values intentionally not repeated here):
- `SpikerSoft.Api/appsettings.json`: Twilio AuthToken, SMTP password, `Encryption:MasterKey`, InfluxDB token, Keycloak client secrets, DNS-server API token, MediatR license key.
- Keycloak client secret for `spikersoft-backend` committed **identically in all three appsettings variants** of at least one project.
- MinIO per-service keys (`uploads-svc`, `metadata-svc`) present in swarm service env (visible to anyone with `docker service inspect`).
- Jetson-influx `.env-10N` files on the shared mount carry plaintext InfluxDB creds (already flagged in #554's thread).
Suggested handling within the epic: treat all of the above as **compromised-by-default** (they're in git history regardless of what happens to the working tree) — rotation list for Phase 1/2, and the appsettings files should end up with empty placeholders + README guidance, which the new per-project READMEs now provide hooks for. The full per-project question list from the audit (includes several more config-hygiene items) is with @spikerj.
QA Team — triage update 2026-07-14: per spikerj, the committed appsettings credentials inventoried above are known development values — no emergency rotation needed. The inventory stands as the epic's migration checklist (these are the keys that must come from OpenBao instead of appsettings when their phases land), just without the compromised-by-default urgency in the earlier note.
**QA Team** — triage update 2026-07-14: per spikerj, the committed appsettings credentials inventoried above are **known development values** — no emergency rotation needed. The inventory stands as the epic's migration checklist (these are the keys that must come from OpenBao instead of appsettings when their phases land), just without the compromised-by-default urgency in the earlier note.
Epic status roll-up — audited every phase against origin/master. Phase 2's seam is live fleet-wide; Phases 3 and 4 have not started, and the "retire scattered passwords" half is where the gap is.
Phase
State
1 — vault deploy + init
✅ live (openbao/docker-stack.yml, docs/openbao-init-runbook.md)
Phase 2's seam is genuinely done and done well.EventHandlerHostBuilder.cs:76 calls AddBaoSecrets(), so all ~30 workers inherit it; the three hosts outside that builder call it directly (SpikerSoft.Api/Program.cs:53, GpuCoordinator/Program.cs:26, NxCacheServer/Program.cs:27). 27 stacks set Secrets__UseBao=true, 26 mount the AppRole creds, and openbao/provision-services-approle.sh makes it reproducible. That's the hard part.
But six stacks were left behind, and two of them are traps rather than oversights: spikersoft-embeddings/docker-stack-gpu.yml and spikersoft-quiz-generation/docker-stack-gpu.yml are GPU variants of services whose main stack has the seam. Once Stage 4 blanks the committed plaintext, deploying a GPU variant boots with empty credentials and will look like a config bug. Details on #546.
Phase 3 has no artifact at all.git grep -rniE "pki|internal ca|mtls" across openbao/* and docs/*.md returns nothing outside the secrets-management doc itself. No CA role, no cert issuance, no mTLS on the stateful tier.
The honest state of "retire scattered passwords." This epic's headline promise is the part that hasn't landed, and I have concrete numbers from auditing #633 today: six sets of committed plaintext credentials remain — four in backend appsettings*.json (the Gitea account password, InfluxDB admin token, Keycloak client secret, RabbitMQ dockerUser/dockerPassword), plus five literals in mailserver/docker-stack.yml that were outside every previous sweep's scope. One partial win: PR #498 blanked every committed Seq key exceptCodeExecution/appsettings.Example.json.
Two things make this more urgent than an infrastructure-hygiene epic normally is:
#857 — I re-ran its reproduction today and the registry still issues anonymous pull tokens over the public internet. Every spikerj/* image is freely downloadable. If any of those six credential sets was ever in a build context, it's in a publicly-pullable layer.
#643 — SonarQube's secret detection is switched off on three appsettings.json files via sonar-scan.yml:75-79, and the suppressions are file-scoped rather than value-scoped. So new secrets in those files won't be flagged either.
Suggested sequencing:close#546's six-stack gap (cheap, and it's a live trap), then #633's rotation — before Phase 3, and before #563 deploys another service carrying a committed secret. PKI is the right long-term direction but it doesn't reduce today's exposure.
**Epic status roll-up** — audited every phase against `origin/master`. **Phase 2's seam is live fleet-wide; Phases 3 and 4 have not started, and the "retire scattered passwords" half is where the gap is.**
| Phase | State |
|---|---|
| 1 — vault deploy + init | ✅ live (`openbao/docker-stack.yml`, `docs/openbao-init-runbook.md`) |
| 2 — `SpikerSoft.Secrets` host-builder seam (#546) | ⚠️ seam adopted fleet-wide; **6 stacks missed, Stage 4 unswept** |
| 3 — PKI / internal CA / mTLS (#547) | ❌ **not started** |
| — LE cert centralization (#610) | ❌ **not started** |
**Phase 2's seam is genuinely done and done well.** `EventHandlerHostBuilder.cs:76` calls `AddBaoSecrets()`, so all ~30 workers inherit it; the three hosts outside that builder call it directly (`SpikerSoft.Api/Program.cs:53`, `GpuCoordinator/Program.cs:26`, `NxCacheServer/Program.cs:27`). 27 stacks set `Secrets__UseBao=true`, 26 mount the AppRole creds, and `openbao/provision-services-approle.sh` makes it reproducible. That's the hard part.
**But six stacks were left behind**, and two of them are traps rather than oversights: `spikersoft-embeddings/docker-stack-gpu.yml` and `spikersoft-quiz-generation/docker-stack-gpu.yml` are **GPU variants of services whose main stack has the seam**. Once Stage 4 blanks the committed plaintext, deploying a GPU variant boots with empty credentials and will look like a config bug. Details on #546.
**Phase 3 has no artifact at all.** `git grep -rniE "pki|internal ca|mtls"` across `openbao/*` and `docs/*.md` returns nothing outside the secrets-management doc itself. No CA role, no cert issuance, no mTLS on the stateful tier.
**The honest state of "retire scattered passwords."** This epic's headline promise is the part that hasn't landed, and I have concrete numbers from auditing #633 today: **six sets of committed plaintext credentials remain** — four in backend `appsettings*.json` (the Gitea account password, InfluxDB admin token, Keycloak client secret, RabbitMQ `dockerUser`/`dockerPassword`), plus five literals in `mailserver/docker-stack.yml` that were outside every previous sweep's scope. One partial win: PR #498 blanked every committed Seq key **except** `CodeExecution/appsettings.Example.json`.
**Two things make this more urgent than an infrastructure-hygiene epic normally is:**
1. **#857** — I re-ran its reproduction today and the registry **still issues anonymous pull tokens over the public internet**. Every `spikerj/*` image is freely downloadable. If any of those six credential sets was ever in a build context, it's in a publicly-pullable layer.
2. **#643** — SonarQube's secret detection is **switched off** on three `appsettings.json` files via `sonar-scan.yml:75-79`, and the suppressions are file-scoped rather than value-scoped. So new secrets in those files won't be flagged either.
**Suggested sequencing:** close #546's six-stack gap (cheap, and it's a live trap), then #633's rotation — before Phase 3, and before #563 deploys another service carrying a committed secret. PKI is the right long-term direction but it doesn't reduce today's exposure.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Passwords/tokens today live in per-repo Gitea Actions secrets (7+ new *_S3_SECRET_KEY from #413 alone), deploy-shell exports (MinIO root), appsettings defaults (RabbitMQ dockerUser/dockerPassword!), and at least one token COMMITTED to git (the shared act_runner registration token, gitea-act-runner/docker-stack.yml — its own comment tracks rotation in README §9).
Decision: OpenBao (Linux Foundation fork of Vault; MPL open source — Vault itself is BUSL now). Rationale: PKI secrets engine covers the "certificate enforced" requirement natively (internal CA → replace generate-internal-certs.sh → mTLS later); AppRole gives per-service/per-runner machine identities with least-privilege policies (same model we just proved with scoped MinIO keys, but centralized + audited + rotatable); Raft HA scales 1→3 nodes; Vault-compatible API = mature .NET client (VaultSharp) + Keycloak OIDC for human SSO. Considered: Infisical (nicer UI, weaker PKI/EE-gated bits), Vaultwarden (right tool for HUMAN passwords — optional later addition, not the machine backbone), Conjur (stagnant).
Phases (tickets follow the #515 pattern):
Root-of-trust rule: Bao's own bootstrap (unseal shares, initial root token) lives OFFLINE, never in Bao, never in git.
QA Team — inventory input for this epic, 2026-07-14: a full fact-checking audit of every spikersoft-backend project (README rebuild, 100+ agents reading every csproj/appsettings/Program.cs) surfaced live-looking credentials committed in appsettings.json files. Types and locations only (values intentionally not repeated here):
SpikerSoft.Api/appsettings.json: Twilio AuthToken, SMTP password,Encryption:MasterKey, InfluxDB token, Keycloak client secrets, DNS-server API token, MediatR license key.spikersoft-backendcommitted identically in all three appsettings variants of at least one project.uploads-svc,metadata-svc) present in swarm service env (visible to anyone withdocker service inspect)..env-10Nfiles on the shared mount carry plaintext InfluxDB creds (already flagged in #554's thread).Suggested handling within the epic: treat all of the above as compromised-by-default (they're in git history regardless of what happens to the working tree) — rotation list for Phase 1/2, and the appsettings files should end up with empty placeholders + README guidance, which the new per-project READMEs now provide hooks for. The full per-project question list from the audit (includes several more config-hygiene items) is with @spikerj.
QA Team — triage update 2026-07-14: per spikerj, the committed appsettings credentials inventoried above are known development values — no emergency rotation needed. The inventory stands as the epic's migration checklist (these are the keys that must come from OpenBao instead of appsettings when their phases land), just without the compromised-by-default urgency in the earlier note.
Epic status roll-up — audited every phase against
origin/master. Phase 2's seam is live fleet-wide; Phases 3 and 4 have not started, and the "retire scattered passwords" half is where the gap is.openbao/docker-stack.yml,docs/openbao-init-runbook.md)SpikerSoft.Secretshost-builder seam (#546)Phase 2's seam is genuinely done and done well.
EventHandlerHostBuilder.cs:76callsAddBaoSecrets(), so all ~30 workers inherit it; the three hosts outside that builder call it directly (SpikerSoft.Api/Program.cs:53,GpuCoordinator/Program.cs:26,NxCacheServer/Program.cs:27). 27 stacks setSecrets__UseBao=true, 26 mount the AppRole creds, andopenbao/provision-services-approle.shmakes it reproducible. That's the hard part.But six stacks were left behind, and two of them are traps rather than oversights:
spikersoft-embeddings/docker-stack-gpu.ymlandspikersoft-quiz-generation/docker-stack-gpu.ymlare GPU variants of services whose main stack has the seam. Once Stage 4 blanks the committed plaintext, deploying a GPU variant boots with empty credentials and will look like a config bug. Details on #546.Phase 3 has no artifact at all.
git grep -rniE "pki|internal ca|mtls"acrossopenbao/*anddocs/*.mdreturns nothing outside the secrets-management doc itself. No CA role, no cert issuance, no mTLS on the stateful tier.The honest state of "retire scattered passwords." This epic's headline promise is the part that hasn't landed, and I have concrete numbers from auditing #633 today: six sets of committed plaintext credentials remain — four in backend
appsettings*.json(the Gitea account password, InfluxDB admin token, Keycloak client secret, RabbitMQdockerUser/dockerPassword), plus five literals inmailserver/docker-stack.ymlthat were outside every previous sweep's scope. One partial win: PR #498 blanked every committed Seq key exceptCodeExecution/appsettings.Example.json.Two things make this more urgent than an infrastructure-hygiene epic normally is:
spikerj/*image is freely downloadable. If any of those six credential sets was ever in a build context, it's in a publicly-pullable layer.appsettings.jsonfiles viasonar-scan.yml:75-79, and the suppressions are file-scoped rather than value-scoped. So new secrets in those files won't be flagged either.Suggested sequencing: close #546's six-stack gap (cheap, and it's a live trap), then #633's rotation — before Phase 3, and before #563 deploys another service carrying a committed secret. PKI is the right long-term direction but it doesn't reduce today's exposure.