Skip to main content

Secrets Management

Volume 5 — Platform Engineering

The Azure Key Vault chapter covered one specific secret store in depth. This chapter is broader: which store is the right one for a given context, how secrets move (or shouldn't move) between them, and the handful of absolute rules worth never breaking regardless of how much time pressure is involved.

1. A Secret's Lifecycle Has Multiple Contexts

The same conceptual secret — say, the database connection string — legitimately needs to exist in several different places, each with a different appropriate storage mechanism:

ContextRight storeWrong store
A developer's personal reference (initial setup, one-off manual testing)BitwardenA Slack DM, a text file on the desktop, a sticky note
Local development environment.env file (gitignored), or appsettings.Development.json (gitignored)Hardcoded directly in checked-in source
CI/CD pipeline executionAzure DevOps secret variable / variable groupPlain pipeline variable, visible in logs
Running production applicationKey Vault, resolved via managed identityA pipeline variable baked into a deployed config file

The same secret value, three or four legitimate homes, each serving a genuinely different need — the goal isn't "one store for everything," it's the right store for each context, with the fewest possible additional copies of the actual value floating around.

2. Bitwarden — the Team's Secret Store

Bitwarden is used across Traxs for the secrets that don't belong in any automated system at all — credentials a human needs to reference or hand-enter: third-party service logins, initial account setup credentials, personal API keys for services not yet wired into Key Vault.

Practices worth holding to:

  • One entry per credential, named specifically enough that "which one is this" is never a guessing game six months later.
  • Shared organization vault for anything team-relevant (a shared service account), never a personal vault, so access doesn't disappear if one person is unavailable.
  • Never paste a Bitwarden-stored secret into Slack, email, or a ticket to "share" it — share the Bitwarden entry (or grant vault access), not the value in plaintext somewhere that itself becomes a new location the secret now lives, unencrypted, indefinitely.

3. What Should Never Happen, Regardless of Time Pressure

These aren't stylistic preferences — each has a direct, concrete failure mode:

NeverBecause
Commit a secret to Git, even briefly, even in a private repoGit history retains it indefinitely — removing it in a later commit does not remove it from history (see the Git Troubleshooting chapter's section on this exact scenario)
Share a secret over Slack/email/chat "just this once"Chat history is itself a persistent, searchable, often broadly-accessible store the secret now lives in, outside any secret management system's visibility
Reuse the same secret across dev and productionA leaked or overly-broadly-shared development credential becomes a production compromise
Hardcode a secret "temporarily" during debugging, intending to remove it before commitThis is exactly how secrets end up committed — the intention to remove it is not a safeguard, only actually not committing it is
Log a secret value, even at debug levelLog aggregators (Seq, App Service Log Stream) retain history and are often more broadly accessible than the secret store itself

If any of these happen anyway — treat the exposed credential as compromised immediately and rotate it. Don't wait to assess whether it was "actually seen" by anyone; the cost of an unnecessary rotation is far lower than the cost of a credential that was genuinely compromised and left in place.

4. Local Development: .env Files

# .env — gitignored, never committed
DB_PASSWORD=LocalDevOnly!2026
SENDGRID_API_KEY=SG.local-dev-key-here
# .gitignore
.env
appsettings.Development.json

Covered mechanically in the Docker Compose chapter — the principle worth restating here is that a local development secret should be genuinely different from the production one, not a shortcut copy of it, specifically so a leaked local .env file never translates into meaningful production exposure.

5. Secret Rotation

TriggerResponse
Scheduled/routine rotationRotate on a defined interval regardless of any known issue — limits the window a leaked-but-undetected credential remains useful
Known/suspected exposureRotate immediately, don't wait to confirm actual misuse first
Team member offboardingRotate any shared credential they had access to, not just revoke their individual account access
Third-party breach notificationRotate the specific credential(s) affected, even if Traxs's own systems weren't directly compromised

Rotation is only as good as the update propagating everywhere the old value was actually used — the Azure Key Vault chapter's stop/start caching gotcha is a direct, concrete example of a rotation that looks complete (the new value is in Key Vault) but silently isn't actually in effect yet until the specific follow-up step is taken.

6. Least Exposure, Not Just Least Privilege

Beyond Azure RBAC's least-privilege principle (who can access a secret store), apply the same discipline to how many places a secret's actual value exists at all:

  • Prefer a system resolving a secret at the point of use (a Key Vault reference resolved by App Service) over that value being copied into multiple configuration files or pipeline variables.
  • When a secret genuinely needs to exist in more than one place, know and be able to enumerate exactly where — an unaccounted-for third copy is exactly the kind of thing that gets missed during a rotation.

7. Troubleshooting Playbook

SymptomLikely causeWhat to check
Secret was committed to GitBypassed .gitignore, or added before a .gitignore entry existedRotate the credential immediately (Git Troubleshooting chapter covers cleaning history, but rotation comes first, always)
Rotated a secret, an old value is still somehow in useA copy exists somewhere beyond the store that was updated — a cached value, a second config file, an unmanaged pipeline variableEnumerate every known location the secret's value exists (Section 6) and confirm each was actually updated
Local .env value ended up committed.gitignore entry missing, added too late, or the file was force-added at some point (git add -f)Confirm .env is genuinely gitignored; check git log for whether it was ever actually tracked despite the current .gitignore
Team member's access revoked, but a shared service credential they knew is unrotatedOffboarding only removed their individual account access, not shared secrets they had visibility intoRotate any shared credential a departing team member had access to, as a standard part of offboarding, not an afterthought
Unsure whether a specific value is still "live" anywhereNo enumeration of the secret's known locations existsTreat this as a gap to close going forward — document where each sensitive value legitimately lives as part of introducing it, not after the fact

8. Quick Reference

ContextStoreNotes
Human reference / manual credentialsBitwardenShared org vault, never a personal one, for team-relevant credentials
Local development.env / gitignored configGenuinely different values from production
CI/CD pipeline executionAzure DevOps secret variable/groupMasked in logs automatically
Running application, runtimeAzure Key Vault, via managed identitySee the Azure Key Vault chapter for the stop/start rotation gotcha
Absolute ruleNever commit, never chat-share, never reuse dev/prodTreat any violation as immediate compromise, rotate regardless of certainty

Part of the Traxs Engineering Handbook — Volume 5: Platform Engineering. Companion chapters in this volume: Linux Fundamentals, Linux Administration, Kubernetes Fundamentals, CI/CD, Observability.

Volume 5 — Platform Engineering is now complete: Linux Fundamentals, Linux Administration, Kubernetes Fundamentals, CI/CD, Observability, and Secrets Management.