← Open in the full interactive course (progress tracking, search & more)

A leaked feature flag is an inconvenience. A leaked database password is an incident. Secrets need a fundamentally higher bar than ordinary configuration — this lesson is about exactly what that bar looks like.

Lesson 300 ended on a caveat, deliberately: environment variables are convenient, but they're not inherently secure storage. That caveat wasn't a footnote — it's the entire subject of this lesson. A database connection string with an embedded password, an API key for a third-party payment gateway, a certificate used to sign tokens — these aren't "just configuration" in the way a timeout value or a feature flag is. If any of them leak, the consequence isn't a bug ticket; it's a security incident, with real, often irreversible fallout.

So secrets need more than "make sure it's set correctly" — they need protection specifically designed around the fact that they're valuable to an attacker in a way ordinary settings simply aren't.

In this lesson, you'll learn what dedicated secret-management services actually offer over plain environment variables, the real trade-off between injecting a secret at startup versus fetching it dynamically from a vault, where .NET's built-in Secret Manager tool fits (and where it explicitly doesn't), and why committing a secret to source control — even briefly — is a genuine incident, not just an embarrassing mistake.

What Is It?

The Simple Explanation

A secret is any configuration value that would cause real harm if someone unauthorized got hold of it — a password, an API key, a signing certificate, a connection string with embedded credentials. Secrets management is the discipline (and the tooling) around storing, distributing, and rotating those specific values with far more care than ordinary configuration ever needs.

The Technical Definition

Secrets management is the practice of storing sensitive credentials in a system specifically designed for that purpose — one offering access control (who or what is even allowed to read a given secret), audit logging (a record of every access, for after-the-fact review), and often automatic rotation (changing a secret's value periodically without manual intervention) — rather than storing them alongside ordinary, non-sensitive configuration values.

Why Does It Exist?

The Problem — Not All Configuration Carries the Same Risk

Lesson 300 covered real, concrete ways an ordinary environment variable can leak: process listing tools, crash dumps, container inspection commands, accidental logging. Treating a database timeout value and a database password identically is fine right up until the password leaks through one of those exact same paths — and then it isn't a shrug-worthy inconvenience, it's an attacker with valid production credentials.

The Solution — Give Secrets a Purpose-Built Home

Dedicated secret-management services exist specifically to close this gap: they're built around the assumption that what they're storing is genuinely sensitive, so access control, auditing, and rotation aren't optional add-ons — they're the reason the service exists at all.

Big Picture — Dedicated Secret Stores

ServiceEcosystemCore idea
Azure Key VaultAzureCentralized store for secrets, keys, and certificates, with fine-grained access policies and audit logging of every access
AWS Secrets ManagerAWSCentralized secret storage with fine-grained IAM-based access control, audit trails, and built-in support for automatic rotation of many credential types
HashiCorp VaultCloud-agnostic / self-hostedA dedicated secrets engine usable across any cloud or on-prem environment, offering dynamic secrets, fine-grained policies, and detailed audit logging
Fine-Grained Access Policies
Only specific identities/services can read a specific secret — not "anyone who can see the environment"
Audit Logs
A record of who/what accessed a secret and when — something a plain environment variable simply has no concept of
Automatic Rotation
A secret's value can change on a schedule, with dependent apps picking up the new value without a person manually swapping it

These are precisely the capabilities a plain environment variable structurally can't offer — an environment variable has no concept of "who read this," no built-in access policy narrower than "whoever can see this container's environment," and no rotation mechanism beyond a human manually changing it and redeploying.

How It Works — Two Genuinely Different Delivery Models

There's a real, meaningful difference between two ways a secret can end up available to a running app, and it's worth understanding both precisely rather than treating "use a vault" as one undifferentiated idea.

Injected at container startup

Fetched dynamically at runtime

DYNAMIC RUNTIME FETCH — CONCEPTUAL FLOW
1. THE APP AUTHENTICATES TO THE VAULT, USING ITS OWN IDENTITY
2. THE APP REQUESTS A SPECIFIC SECRET, BY NAME
3. THE VAULT LOGS THE ACCESS AND RETURNS THE CURRENT VALUE
4. IF THE SECRET ROTATES LATER, THE APP CAN PICK UP THE NEW VALUE ON ITS NEXT FETCH

Simple Example — Local Development with the Secret Manager Tool

.NET ships a built-in tool, dotnet user-secrets, specifically for local development — worth knowing about, and worth understanding its exact, deliberately narrow scope:

dotnet user-secrets init dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;Database=Orders;User=dev;Password=devonly123;"

This stores the value in a JSON file in the local user's profile directory — outside the project directory entirely — where IConfiguration automatically picks it up in the Development environment, with no code change required. Because it lives outside the project folder, it physically can't be accidentally git add'd — it was never inside the repository to begin with.

Scope this correctly: Secret Manager is explicitly, deliberately a local-development-only tool. Microsoft's own documentation is direct about this — it does not encrypt the stored values at rest, and it is not designed or intended for production use in any form. Its entire value is keeping a developer's own local connection strings and API keys out of source control while they're coding — nothing more.

Real-World Example

A payment API's lifecycle across environments shows the whole spectrum this lesson covers. On a developer's laptop, the Stripe test API key lives in dotnet user-secrets — never in appsettings.json, never committed. In staging and production, the real API key lives in Azure Key Vault; the deployment pipeline (or the app itself, via a managed identity) fetches it, and every fetch is recorded in Key Vault's audit log. When the security team rotates that key on a quarterly schedule, the app configured to fetch dynamically simply picks up the new value on its next refresh — no emergency redeploy required, no window where the app is running with a dead key. Contrast that with a team that, out of convenience, put the same key directly into a Kubernetes manifest as a plain environment variable: rotating it means editing and reapplying that manifest, coordinated with a deploy, every single time.

Analogy

A Sticky Note vs. a Bank Vault

A plain environment variable holding a secret is like a sticky note with a key written on it, stuck to a desk in a shared office — convenient, anyone can grab it in a second, and there's no record of who did. It works, right up until the wrong person walks past.

A dedicated secrets service is a bank vault: getting the key out requires proving who you are, every single withdrawal is logged, and the bank can swap the lock and issue a new key without you ever needing to walk in and physically replace anything on your end — your access to it is what updates, not a note you're carrying around. That last part is exactly what dynamic runtime fetching buys you over static injection: rotation without you having to go find and replace every copy of the old value yourself.

Under the Hood — Why a Committed Secret Is a Real Incident, Not Just a Mistake

It's worth being precise about why "I committed a secret and then deleted it in the next commit" doesn't actually solve the problem — this is one of the most common, and most underestimated, real-world security mistakes.

WHY GIT HISTORY MAKES THIS PERMANENT
1. GIT DOESN'T "REMOVE" DATA WHEN A NEW COMMIT CHANGES A FILE
2. ANYONE WHO CLONED THE REPO ALREADY HAS A COPY
3. AUTOMATED SCANNERS FIND EXPOSED SECRETS IN PUBLIC HISTORY QUICKLY

The only real fix once a secret has been committed and pushed is to treat it as compromised: rotate the actual credential (change the real database password, revoke and reissue the real API key) — not merely remove it from the file. Purging it from git history is good hygiene afterward, but it doesn't undo whoever may have already seen it; the rotation is the part that actually closes the exposure.

Common Confusion

1. "dotnet user-secrets encrypts my secrets" — no, it just keeps them out of the project folder

The values are stored in plain, unencrypted JSON on the local machine. Its entire security value is location — outside the repo, so it can't be accidentally committed — not encryption. That's a real and useful property for local development, but it's not remotely the same guarantee a production secrets service offers.

2. "Deleting the file and committing again removes the secret" — no, it's still in history

This is the exact misconception the "Under the Hood" section above walks through — a follow-up commit that removes a secret does not remove it from the repository's history. The secret has to be treated as compromised and rotated, regardless of what later commits do to the file.

Common Mistakes

Mistake 1 — Using Secret Manager as if it were a production solution

Deploying an app to production still reading from the local Secret Manager store — it wouldn't even work, since that store lives on a specific developer's machine, but the underlying mistake is treating it as "secure enough" for anything beyond local development in the first place. Use a real secret-management service (Key Vault, Secrets Manager, Vault) in every non-local environment.

Mistake 2 — Treating "I deleted it in a later commit" as resolved

Removing a leaked secret from the current file and considering the incident closed, without rotating the actual credential. Rotate the real credential immediately — the file change alone does not undo the exposure that already happened.

Mistake 3 — Putting a genuinely sensitive value in a plain environment variable "just for now"

Shipping a real production database password as a plain ConnectionStrings__Default environment variable because it's quick, with a vague intention to "fix it properly later." Recognize the exposure paths from Lesson 300 apply from day one — reach for a dedicated secrets service for anything genuinely sensitive, not just for values that feel important enough in the moment.

When Should I Use It?

Rule of thumb: Ask "what happens if this leaks?" For most configuration, the honest answer is "not much." For a secret, the honest answer should always be enough to justify the extra handling this lesson describes.

Mental Model

dotnet user-secrets = local development only, kept out of the repo by location, not encryption
Injected at startup = simple, but still an environment variable underneath — rotation needs a redeploy
Fetched dynamically = access-controlled, audited, and rotatable without redeploying — more moving parts, more security
Committed to git = permanent, the moment it's pushed — the only real fix is rotating the actual credential

Remember: "leaked briefly" and "never leaked" are not the same outcome — treat any exposure as real the instant it happens.

Key Takeaway


Check Your Understanding

You've seen why secrets deserve more careful handling than ordinary config, and what that handling actually looks like in practice. Let's confirm it clicked.

1. What real capability does a service like Azure Key Vault or AWS Secrets Manager offer that a plain environment variable structurally cannot?

Show answer

Correct: B

Why B is correct: These are the specific, real capabilities the lesson names — access control, auditing, and rotation are the exact gap a plain environment variable has no concept of at all.

Why A is incorrect: Read performance isn't the differentiator here — the value is in security and operational control, not speed.

Why C is incorrect: Character-limit handling isn't a meaningful distinction between these mechanisms.

Why D is incorrect: These services don't perform language translation of any kind — they store and control access to sensitive values.

Reinforcement: Access control, auditing, and rotation are the three concrete advantages to remember.

2. What is the specific, correct scope of .NET's dotnet user-secrets tool?

Show answer

Correct: B

Why B is correct: This is exactly its documented, intended scope — a convenience for local development, whose safety comes from location (outside the repo), not from encryption or production-grade access control.

Why A is incorrect: The stored values are not encrypted, and Microsoft explicitly documents this tool as not intended for production use.

Why C is incorrect: It has no relationship to cloud secret stores at all — it's purely a local file on the developer's own machine.

Why D is incorrect: There's no rotation mechanism here — it's a static local store a developer sets values into manually.

Reinforcement: Local dev convenience, not production security — keep that boundary clear.

3. What is the meaningful advantage of fetching a secret dynamically from a vault at runtime, compared to injecting it as an environment variable at container startup?

Show answer

Correct: B

Why B is correct: This is the precise, meaningful trade-off the lesson draws — dynamic fetching adds complexity but buys rotation-without-redeploy and per-access auditing, neither of which static startup injection provides.

Why A is incorrect: If anything, a runtime fetch to a vault adds a network round trip during startup or first use — it doesn't speed things up.

Why C is incorrect: The opposite is true — dynamic fetching requires the app to reach the vault over the network, which static injection avoids entirely at runtime.

Why D is incorrect: Converting to a typed object is a separate concern (the Options pattern), unrelated to where the raw value came from.

Reinforcement: Rotation-without-redeploy and audit logging are the two concrete wins dynamic fetching buys over static injection.

4. A developer accidentally commits and pushes a real production API key, then immediately removes it in the very next commit. What is the correct response?

Show answer

Correct: B

Why B is correct: The key still exists in the repository's history and in any clone made before the fix, regardless of what later commits do to the current file — the only action that actually closes the exposure is rotating the real credential.

Why A is incorrect: This is exactly the misconception the lesson calls out directly — deleting the value in a later commit does not remove it from history.

Why C is incorrect: Waiting to see if misuse is reported leaves a genuinely compromised credential active in the meantime — the correct response doesn't depend on whether misuse has been observed yet.

Why D is incorrect: Renaming the variable that references the key does nothing to the key's actual value or its exposure in history.

Reinforcement: Once pushed, a secret must be treated as exposed permanently — rotating the real credential is the only fix that matters.

You now understand how secrets deserve fundamentally different handling than ordinary configuration — from local development with Secret Manager all the way through vault-backed, rotatable production secrets. That closes out the configuration and containerization foundation this Part builds on.


dotnetmadeeasy.com — Learn C# and .NET, the right way.