Every breach post-mortem I have read in the last decade has a secrets problem buried somewhere in it. A connection string in a config file checked into source control. An API key pasted into a chat channel so a colleague could test something on a Friday. A service account password that was set once in 2019 and never rotated because nobody could remember which three services depended on it. Secrets management is not glamorous work, but in a regulated fintech environment it is the difference between a controlled incident and a regulatory disclosure.
I have spent a good chunk of my career building and operating payment systems on .NET, and I want to lay out how I think about secrets across the lifecycle of an application. This is not a vendor pitch. It is a practical account of the patterns that have held up under audit, under load, and under the kind of scrutiny you get when money moves through your platform.
What Actually Counts as a Secret
Before you can manage secrets well, you need a shared definition of what a secret is, because teams disagree more than you would expect. My working rule is simple: a secret is any value that, if disclosed, would let an attacker impersonate a principal, decrypt protected data, or move laterally into another system. That covers database passwords, API keys, signing keys, TLS private keys, OAuth client secrets, encryption keys, and webhook verification tokens.
What is not a secret is just as important. A database hostname is configuration, not a secret. A feature flag is configuration. A public certificate is, by definition, public. Conflating configuration with secrets leads to two bad outcomes: either you over-protect benign values and slow everyone down, or you under-protect the dangerous ones because the category became meaningless. Keep the boundary crisp and document it.
The reason this matters in .NET specifically is that the configuration system happily merges all of these together into a single IConfiguration tree. That is convenient, but it means a careless developer can treat a signing key with the same casualness as a log level. The framework will not stop them. Your conventions and your pipeline have to.
The .NET Configuration Model and Where It Leaks
The .NET configuration system is a layered set of providers, each one overriding the last. A typical web application stacks them in this order: appsettings.json, then an environment-specific appsettings file, then User Secrets in development, then environment variables, then command-line arguments. The last writer wins, which is exactly the property you want for promoting a value from a default to an environment-specific override.
The leaks happen at the bottom of that stack. The appsettings.json file lives in source control, so any secret placed there is permanently in your Git history even after you remove it. I have watched teams rotate a key, delete the line from the file, and forget that the old value is one git log -p away forever. Environment variables are better, but they show up in process listings and crash dumps, and they get inherited by child processes you did not intend to share them with.
The single most valuable habit I have instilled in teams is this: the appsettings files describe the shape of configuration, never its sensitive content. If a key in there has a real secret value, that is a bug, full stop.
Local Development with User Secrets
For local development, the .NET User Secrets tool is the correct first step and costs almost nothing to adopt. It stores secrets in a JSON file in your user profile, well outside the repository directory, keyed by a GUID in the project file. Because the file lives outside the solution folder, there is no path by which it gets committed. It is not encrypted at rest, so it is not a substitute for a vault, but for a developer machine it removes the most common cause of leaked credentials.
Initializing it is a one-liner per project, and setting a value is equally direct. The mental model that helps the team is that User Secrets is the development-only bottom layer that shadows appsettings without touching it.
dotnet user-secrets init
dotnet user-secrets set "Payments:GatewayApiKey" "sk_test_local_only_value"
// In Program.cs the provider is added automatically in Development:
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>();
}
// Consume it through the options pattern, never raw indexing:
builder.Services.Configure<PaymentsOptions>(
builder.Configuration.GetSection("Payments"));
One nuance worth stressing: User Secrets only loads in the Development environment by default. That is a feature, not a limitation. It guarantees that your local-only test keys cannot accidentally bleed into a staging or production process, because the provider simply is not registered there. Production secrets must come from somewhere else entirely.
Centralized Vaults in Production
In production, secrets belong in a dedicated secret store, not in environment variables baked into a deployment manifest. Azure Key Vault, AWS Secrets Manager, and HashiCorp Vault all do the same essential job: they hold secrets encrypted at rest, gate access behind an identity and a policy, and produce an audit log of every read. The .NET configuration providers for these stores let you pull secrets at startup so the rest of your code keeps consuming IConfiguration exactly as before.
The reason I insist on a vault rather than environment variables is auditability. When an examiner asks who could have read the production database password and when it was last accessed, "it was an environment variable on a server" is not an answer that survives the conversation. A vault gives you a defensible record. It also centralizes rotation, so you change a value in one place rather than redeploying every service that happens to embed it.
There is a real cost here that I will not pretend away. A vault introduces a network dependency into your startup path, so you need sensible retry and caching, and you need to think about what happens during a vault outage. My pattern is to load secrets once at startup with a bounded retry, cache them in memory through the options system, and refresh on a deliberate schedule rather than reaching out to the vault on every request.
Eliminating the Bootstrap Secret with Managed Identity
Centralizing secrets in a vault creates an obvious chicken-and-egg problem: how does the application authenticate to the vault without a secret of its own? If the answer is another stored credential, you have just moved the problem rather than solved it. This bootstrap secret is the one that tends to live forever, precisely because it is so awkward to rotate.
The clean answer on a cloud platform is workload identity, which on Azure means a managed identity assigned to the compute resource. The platform issues a short-lived token to the process based on the identity of the machine or container it runs on, and the vault trusts that token. There is no secret on disk, nothing to rotate, and nothing to leak. In .NET this is handled transparently by DefaultAzureCredential, which uses the managed identity in production and falls back to your developer credentials locally.
When I cannot use a managed identity, for example on infrastructure that is not cloud-native, I fall back to a short-lived certificate-based credential rather than a long-lived shared secret, and I automate its renewal. The principle is the same regardless of platform: push the trust anchor down to something the platform can attest, so that the human-managed secret count trends toward zero.
Rotation, Revocation, and the Two-Secret Trick
A secret you cannot rotate quickly is a liability, because the moment you suspect compromise you are forced to choose between an outage and continued exposure. The hard part of rotation is not generating a new value; it is the window during which old and new credentials must both be valid so that you can roll the change through without downtime. This is where teams get stuck and end up never rotating at all.
The technique that has served me best is to support two active secrets per credential at all times, often called primary and secondary. You promote the secondary to primary, distribute the new secondary, and the overlap means no request ever fails because it presented the wrong one. Most managed secret stores support this directly, and most well-designed APIs accept either key during the transition.
- Generate the new secret in the vault as the secondary value without touching the primary.
- Deploy or refresh consumers so they accept both values during the overlap window.
- Promote the secondary to primary and update the upstream system to issue the new value.
- Retire the old value only after confirming, through logs, that nothing is still using it.
- Treat any failed rotation as an incident and investigate the dependency you missed.
Revocation is rotation under pressure. If you have practiced the routine rotation path, emergency revocation is the same steps run faster. If you have never rotated a given secret, the first time you try will be during an incident, which is the worst possible time to discover a hidden dependency.
Keeping Secrets Out of Logs and Telemetry
The store can be perfect and you can still leak through the side door of logging. Structured logging in .NET is a genuine asset for observability, but it will faithfully serialize whatever object you hand it, including the property holding your API key. The same goes for unhandled exception middleware that dumps a request body, and for distributed tracing that captures headers. Every one of these is a path by which a secret ends up in a log aggregator that has far broader access than the vault ever did.
My defenses are layered. At the type level, I wrap sensitive values so they cannot be serialized casually, overriding the string representation to return a redacted placeholder. At the framework level, I configure logging redaction for known sensitive keys. And at the pipeline level, I run a scanner over both source and log output to catch patterns that look like keys before they reach anyone.
public sealed class SecretValue
{
private readonly string _value;
public SecretValue(string value) => _value = value;
// Only an explicit, audited call can reveal the value.
public string Reveal() => _value;
// Anything that serializes or logs this gets a safe placeholder.
public override string ToString() => "***redacted***";
}
The wrapper is not bulletproof, since anyone can call Reveal, but it converts an accidental leak into a deliberate one. That distinction matters enormously when you are reviewing code, because a search for explicit reveal calls is tractable in a way that auditing every log statement is not.
Secrets in the Pipeline and Continuous Detection
Secrets leak through the build and deployment pipeline at least as often as through application code. Build agents need registry credentials, deployment steps need cloud access, and integration tests need real-ish endpoints. The temptation is to drop these into pipeline variables as plain text, which puts them one misconfigured log statement away from exposure. Treat pipeline secrets with the same discipline as application secrets: store them in a vault or the platform's protected variable mechanism, mark them secret so they are masked in logs, and scope them to the minimum jobs that need them.
Detection is the safety net under all of this. I run secret scanning on every commit and as a pre-receive hook so that a leaked key is caught before it is even merged, and I run a periodic scan over history because conventions drift and people make mistakes. When the scanner fires, the response is not just to delete the line; it is to assume the value is compromised and rotate it, because by the time you have noticed, the value has been on a developer's machine, in a CI log, and possibly in a fork.
The cultural point underneath the tooling is that a leaked secret is a fact about the value, not a moral failing of the person who leaked it. If rotation is cheap and blameless, people report leaks immediately. If it is expensive and embarrassing, they stay quiet and the exposure window grows. Make the safe path the easy path.
Conclusion
Managing secrets in .NET is not about adopting a single product. It is about being honest regarding what a secret is, keeping sensitive values out of source control and logs by construction, centralizing them in an audited store, and reducing the number of human-managed credentials toward zero with workload identity. Each of these is a modest engineering effort on its own, and together they turn the most common cause of catastrophic breaches into a managed, observable, rotatable part of your system. In a regulated environment that is not a luxury; it is the baseline you will be measured against, and the work you are glad you did the day an examiner or an incident comes knocking.
