[Security][Go-live] Move all secrets out of source control & establish secret injection #404

Closed
opened 2026-07-05 20:23:42 +00:00 by spikerj · 4 comments
Owner

Context: learn.spikersoft.com is pre-beta (no real users/data), so this is a go-live prerequisite, not an active breach. Today these are dev/test credentials — but the pattern must never reach production, and any secret reused in prod is already burned.

Problem: The entire secret set is committed to git, including the AES master key that would encrypt user PII. There is no production override, so committed values carry straight to prod.

Evidence:

  • SpikerSoft.Api/appsettings.json:451 — Encryption:MasterKey (wraps every data-encryption key)
  • :268 / :272 — Keycloak client secrets
  • :295 Stripe · :446 Twilio · :413 SMTP · :307 MSSQL sa · :212/217 InfluxDB · :402 Seq
  • Same secrets repeated across ~41 tracked files + SpikerSoft.AI.MCPServer/appsettings.*.json:4

Fix:

  • Move all secrets to Docker/Swarm secrets or a vault; inject at runtime
  • Rotate any credential that will be reused in production
  • Purge from git history (BFG / git-filter-repo)
  • Add pre-commit + CI secret scanning

Acceptance criteria:

  • No secrets in any committed appsettings*.json (placeholders only)
  • Secrets sourced from Swarm secrets/vault at runtime
  • CI fails on a newly committed secret
  • Git history scrubbed of the master key & live credentials

Effort: M–L · Prerequisite for the production deployment model (INF-2, this batch).

**Context:** learn.spikersoft.com is pre-beta (no real users/data), so this is a **go-live prerequisite, not an active breach**. Today these are dev/test credentials — but the pattern must never reach production, and any secret reused in prod is already burned. **Problem:** The entire secret set is committed to git, including the AES master key that would encrypt user PII. There is no production override, so committed values carry straight to prod. **Evidence:** - `SpikerSoft.Api/appsettings.json:451` — `Encryption:MasterKey` (wraps every data-encryption key) - `:268` / `:272` — Keycloak client secrets - `:295` Stripe · `:446` Twilio · `:413` SMTP · `:307` MSSQL sa · `:212/217` InfluxDB · `:402` Seq - Same secrets repeated across ~41 tracked files + `SpikerSoft.AI.MCPServer/appsettings.*.json:4` **Fix:** - Move all secrets to Docker/Swarm secrets or a vault; inject at runtime - Rotate any credential that will be reused in production - Purge from git history (BFG / git-filter-repo) - Add pre-commit + CI secret scanning **Acceptance criteria:** - No secrets in any committed `appsettings*.json` (placeholders only) - Secrets sourced from Swarm secrets/vault at runtime - CI fails on a newly committed secret - Git history scrubbed of the master key & live credentials **Effort:** M–L · Prerequisite for the production deployment model (INF-2, this batch).
spikerj added the agentic label 2026-07-05 20:23:42 +00:00
Author
Owner

Prerequisite for the production deployment model INF-2 #414.

Prerequisite for the production deployment model **INF-2 #414**.
Author
Owner

Verified finding — live secrets hardcoded in SpikerSoft.Api/appsettings.json (surfaced while adding webhook-signature validation)

Committed production credentials currently in source (listing keys only, not values):

  • Twilio:AuthToken — Twilio account auth token (also the key that signs webhook HMACs, so it's security-critical).
  • Encryption:MasterKey — a base64 AES master key.
  • InfluxDB:Token / InfluxDashboard:Token — InfluxDB API token (same value in both blocks).
  • Keycloak:backend-api:secret and Keycloak:health-check:secret — Keycloak client secrets.

Anyone with repo read access has these. Because .NET config layering lets environment variables override appsettings.json, the migration is low-risk per key: inject each as Section__Key env (Docker/Swarm secret), then blank the value in appsettings.json. Rotate every exposed credential after it's out of source (they must be treated as already compromised).

Suggested bounded first slice (no architecture decision needed): pick the highest-risk few (Encryption:MasterKey, Keycloak:*:secret, Twilio:AuthToken) and move them to injected env + blank in source, once the deploy confirms the env vars are actually set (else the app loses the value on boot). I can prep that PR if you confirm the prod deployment already injects — or will inject — these as env/secrets; without that confirmation, blanking them would break boot, so it's the one gate here.

(Context: found while shipping webhook HMAC validation — Twilio #165 (merged), SonarQube #166. Both reference Twilio:AuthToken / a SonarQube secret from config.)

### Verified finding — live secrets hardcoded in `SpikerSoft.Api/appsettings.json` (surfaced while adding webhook-signature validation) Committed **production** credentials currently in source (listing keys only, not values): - `Twilio:AuthToken` — Twilio account auth token (also the key that signs webhook HMACs, so it's security-critical). - `Encryption:MasterKey` — a base64 AES master key. - `InfluxDB:Token` / `InfluxDashboard:Token` — InfluxDB API token (same value in both blocks). - `Keycloak:backend-api:secret` and `Keycloak:health-check:secret` — Keycloak client secrets. Anyone with repo read access has these. Because .NET config layering lets environment variables override `appsettings.json`, the migration is low-risk per key: inject each as `Section__Key` env (Docker/Swarm secret), then blank the value in `appsettings.json`. Rotate every exposed credential after it's out of source (they must be treated as already compromised). Suggested bounded first slice (no architecture decision needed): pick the highest-risk few (`Encryption:MasterKey`, `Keycloak:*:secret`, `Twilio:AuthToken`) and move them to injected env + blank in source, once the deploy confirms the env vars are actually set (else the app loses the value on boot). I can prep that PR if you confirm the prod deployment already injects — or will inject — these as env/secrets; without that confirmation, blanking them would break boot, so it's the one gate here. (Context: found while shipping webhook HMAC validation — Twilio #165 (merged), SonarQube #166. Both reference `Twilio:AuthToken` / a SonarQube secret from config.)
Author
Owner

Board-sweep status (2026-07-22) — this ticket's notes predate the OpenBao rollout, so catching it up: the epic it seeded (#543/#545/#546) has landed the core: SpikerSoft.Api/appsettings.json now carries blank values for all 12 API credentials (incl. every key the 07-06 comment listed — Twilio:AuthToken, Encryption:MasterKey, Influx tokens, Keycloak client secrets), served from OpenBao with strict fail-fast when Secrets__UseBao=true (merged #546 stage-4, backend 4bcec842). CI secrets are Bao-fetched repo-wide (two AppRole secrets per repo).

Still open before this go-live gate closes: (1) rotation of the previously-committed values — they must be treated as compromised; #600 tracks the Influx token, #633 the dev-environment set; (2) the worker-fleet Bao overlay rollout is ~26 of 60 stacks (#546's remaining batches); (3) plaintext creds in .env-10N files on the shared mount (flagged in #554). Leaving open as the go-live umbrella for those.

Board-sweep status (2026-07-22) — this ticket's notes predate the OpenBao rollout, so catching it up: the epic it seeded (#543/#545/#546) has landed the core: `SpikerSoft.Api/appsettings.json` now carries **blank values for all 12 API credentials** (incl. every key the 07-06 comment listed — Twilio:AuthToken, Encryption:MasterKey, Influx tokens, Keycloak client secrets), served from OpenBao with strict fail-fast when `Secrets__UseBao=true` (merged #546 stage-4, backend `4bcec842`). CI secrets are Bao-fetched repo-wide (two AppRole secrets per repo). **Still open before this go-live gate closes:** (1) rotation of the previously-committed values — they must be treated as compromised; #600 tracks the Influx token, #633 the dev-environment set; (2) the worker-fleet Bao overlay rollout is ~26 of 60 stacks (#546's remaining batches); (3) plaintext creds in `.env-10N` files on the shared mount (flagged in #554). Leaving open as the go-live umbrella for those.
Author
Owner

Migrated to spikerj/spikersoft-backend#551 and spikerj/spikersoft-infrastructure#178 as part of the umbrella-tracker breakup.

This ticket needed changes in more than one repo, so it became one issue per repo:

Verified 2026-08-07 against spikersoft-backend@98102023 and spikersoft-infrastructure@86d03ff6, and live: bao.spikersoft.com/v1/sys/health → initialized: true, sealed: false, standby: false (3-node raft, openbao_openbao-1/2/3 all 1/1).

Status: partially done, criterion by criterion —

AC State
No secrets in committed appsettings*.json mostly — SpikerSoft.Api/appsettings.json is fully blanked, but four backend services still carry live literals (spikerj/spikersoft-backend#532)
Secrets sourced from vault at runtime yes — AddBaoSecrets() centrally registered; 27 infra stacks carry Secrets__UseBao (6 stragglers on spikerj/spikersoft-infrastructure#170)
CI fails on a newly committed secret no — gitleaks|trufflehog|detect-secrets returns no matches in any repo
Git history scrubbed no — every previously-committed value is still recoverable

Because the injection half landed and the rotation half is tracked separately, the two children are scoped to exactly what nothing else covers: the CI secret-scanning gate and the history scrub, plus the infra-side literals (.env-10N, stack-file sweep). The related halves already have homes: rotation → spikerj/spikersoft-infrastructure#167, remaining backend literals → spikerj/spikersoft-backend#532, stack rollout → spikerj/spikersoft-infrastructure#170.

Closing here. Work now lives in the repos that hold the fix, so fixes #<M> in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.

— Opus 5 Agent

Migrated to **spikerj/spikersoft-backend#551** and **spikerj/spikersoft-infrastructure#178** as part of the umbrella-tracker breakup. This ticket needed changes in more than one repo, so it became one issue per repo: - Backend (CI secret-scanning gate + appsettings guard test + history scrub): spikerj/spikersoft-backend#551 - Infrastructure (stack-file literals, `.env-10N` on the shared mount, infra CI gate): spikerj/spikersoft-infrastructure#178 Verified 2026-08-07 against `spikersoft-backend@98102023` and `spikersoft-infrastructure@86d03ff6`, and live: `bao.spikersoft.com/v1/sys/health` → `initialized: true, sealed: false, standby: false` (3-node raft, `openbao_openbao-1/2/3` all 1/1). Status: partially done, criterion by criterion — | AC | State | |---|---| | No secrets in committed `appsettings*.json` | **mostly** — `SpikerSoft.Api/appsettings.json` is fully blanked, but four backend services still carry live literals (spikerj/spikersoft-backend#532) | | Secrets sourced from vault at runtime | **yes** — `AddBaoSecrets()` centrally registered; 27 infra stacks carry `Secrets__UseBao` (6 stragglers on spikerj/spikersoft-infrastructure#170) | | CI fails on a newly committed secret | **no** — `gitleaks\|trufflehog\|detect-secrets` returns **no matches in any repo** | | Git history scrubbed | **no** — every previously-committed value is still recoverable | Because the injection half landed and the rotation half is tracked separately, the two children are scoped to exactly what nothing else covers: the **CI secret-scanning gate** and the **history scrub**, plus the infra-side literals (`.env-10N`, stack-file sweep). The related halves already have homes: rotation → spikerj/spikersoft-infrastructure#167, remaining backend literals → spikerj/spikersoft-backend#532, stack rollout → spikerj/spikersoft-infrastructure#170. Closing here. Work now lives in the repos that hold the fix, so `fixes #<M>` in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only. — Opus 5 Agent
Sign in to join this conversation.