The fastest way to leak credentials in a .NET app is to put them where they're convenient: appsettings.json. It's readable, it's checked into git, and it ends up baked into your container image and your build logs. Secrets management is the discipline of keeping connection strings, API keys, tokens, and certificates out of source and out of images — and making them available to the app only at runtime, from a place designed to guard them. This guide walks the .NET secrets story from local development to production, using the configuration system .NET already gives you.
Why config files are the wrong place for secrets
The .NET configuration system layers providers — JSON files, environment variables, command line, and external stores — into a single IConfiguration. That layering is the feature: you can keep non-secret settings in appsettings.json and inject secrets from a provider that never touches source control. The moment a secret lands in a committed file, three problems appear at once: it's in git history forever (deleting it in a later commit doesn't remove it), it's in every clone and fork, and it's in the built image's layers. Remediation isn't "delete the line" — it's "rotate every exposed credential," which is expensive and error-prone. The goal is to make that mistake structurally impossible.
Local development: the Secret Manager
For development, .NET ships the Secret Manager (user secrets), which stores values outside the project directory so they can't be committed:
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Db" "Server=...;Password=..."
Values live in your user profile (for example, under %APPDATA%\Microsoft\UserSecrets\<id> on Windows), and the configuration system reads them automatically in the Development environment:
var conn = builder.Configuration.GetConnectionString("Db");
Two caveats keep this honest: user secrets are stored in plaintext — they protect against accidental commits, not against someone with access to your machine — and they are a development-only mechanism. Never use them for production.
Production: a managed vault plus a managed identity
In production, secrets should come from a dedicated store — Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, or your platform's equivalent — bound through a configuration provider. The critical detail is how the app authenticates to the vault. The wrong answer is another secret (a vault password in an environment variable creates the same problem one level up). The right answer is a workload/managed identity, so no bootstrap secret exists:
builder.Configuration.AddAzureKeyVault(
new Uri("https://my-vault.vault.azure.net/"),
new DefaultAzureCredential()); // uses the managed identity, no secret
DefaultAzureCredential picks up the managed identity in production and falls back to developer credentials locally, so the same code works in both places. This gives you central rotation, access auditing, and revocation — none of which a config file offers.
When you must use environment variables
Container platforms often inject configuration as environment variables, and .NET reads them natively (with : written as __ for nested keys, e.g. ConnectionStrings__Db). They're acceptable as a transport when the platform's secret store populates them at runtime, but be aware of the pitfalls: environment variables are visible to the whole process and often to diagnostics endpoints, they can end up in crash dumps and logs, and a Dockerfile ENV line bakes them into the image permanently. Prefer platform secret mounts (Kubernetes secrets as files, for example) or the vault provider over ENV in a Dockerfile.
Catch leaks before they're pushed
Prevention beats cleanup. Add a secret scanner to your workflow so a mistyped credential never reaches a remote branch:
- Run
gitleaksortrufflehogas a pre-commit hook and as a CI step. - Enable your platform's push protection (secret scanning that blocks the push).
- Scan the whole history when onboarding a repo — leaks often predate your policy.
Pair this with least-privilege: the credentials an app holds should be scoped to exactly what it needs, and short-lived where possible, so a leak's blast radius is small and its window is short.
A secrets management checklist
| Environment | Store | Auth to store |
|---|---|---|
| Local dev | Secret Manager (user secrets) | N/A (local file) |
| CI | CI secret store (masked) | Scoped, rotated tokens |
| Production | Key Vault / Secrets Manager | Managed / workload identity |
- No secrets in
appsettings*.json,web.config, or source - Secret Manager for local dev; understood as plaintext + dev-only
- Vault provider + managed identity in production
- No secrets in Dockerfile
ENV; use platform secret mounts - Secret scanning as a pre-commit hook and CI gate; push protection on
- Credentials least-privilege, scoped, and rotated
Infrastructure carries secrets too. A scan of your Terraform, Helm, and Kubernetes manifests catches secrets committed into infrastructure-as-code and misconfigured vault access policies that application-level scanning misses.
How Safeguard Helps
Safeguard closes the loop between "we have a policy" and "the policy is enforced." It scans your repositories and CI for exposed secrets — connection strings, API keys, tokens, private keys — and flags them with the file, commit, and blast radius identified, so rotation is targeted rather than guesswork. Its infrastructure-as-code scanning extends that to secrets and overly broad access policies embedded in your deployment manifests. You can run the same checks locally and in CI through the Safeguard CLI, and the platform correlates a leaked credential with the dependencies and services it can reach so you understand real impact. Teams consolidating secret and dependency scanning often compare it on the Safeguard vs Mend page.
Start free at app.safeguard.sh/register and read the secret-scanning setup at docs.safeguard.sh.