DEV Community

Cover image for Secret Management: Securely Storing Passwords, API Keys, and Certificates
Rhuturaj Takle
Rhuturaj Takle

Posted on

Secret Management: Securely Storing Passwords, API Keys, and Certificates

Secret Management: Securely Storing Passwords, API Keys, and Certificates

A practical guide to secret management — the discipline and tooling for securely storing, distributing, and rotating passwords, API keys, connection strings, and certificates — covering Azure Key Vault, AWS Secrets Manager, Kubernetes secret patterns, .NET integration, rotation strategies, and how secret management ties together nearly every other guide in this series.


Table of Contents

  1. Introduction
  2. What Counts as a Secret
  3. Why Secrets Don't Belong in Source Control
  4. Azure Key Vault
  5. AWS Secrets Manager and Parameter Store
  6. .NET Integration Patterns
  7. Secrets in Kubernetes
  8. Secrets in CI/CD Pipelines
  9. Managed Identity: Eliminating the Bootstrap Secret
  10. Secret Rotation
  11. Certificate Management
  12. Local Development Without Compromising Security
  13. Detecting and Responding to a Leaked Secret
  14. Common Pitfalls
  15. Quick Reference Table
  16. Conclusion

Introduction

Secret management is the discipline of storing, distributing, and rotating sensitive credentials — database passwords, API keys, connection strings, TLS certificates, signing keys — so they're available to the systems that legitimately need them, without ever being exposed in source control, logs, or to anyone without a genuine need to access them. This guide pulls together secret-handling threads already touched on throughout this series — GitHub Actions secrets, Kubernetes Secrets, the Docker guide's warning against baking secrets into images, GitOps' Sealed Secrets and External Secrets Operator — into a single, coherent picture of how secret management actually works end to end.

// The destination: application code that never sees a hardcoded secret
var connectionString = await _secretClient.GetSecretAsync("database-connection-string");
Enter fullscreen mode Exit fullscreen mode
// The anti-pattern this guide exists to prevent:
appsettings.json: { "ConnectionStrings": { "Default": "Server=prod-db;Password=Sup3rS3cr3t!" } }
                     committed to Git, visible in history forever, even if later "removed"
Enter fullscreen mode Exit fullscreen mode

1. What Counts as a Secret

The obvious categories

  • Passwords — database credentials, service account passwords.
  • API keys and tokens — third-party service credentials, internal service-to-service auth tokens.
  • Connection strings — often contain embedded credentials, making the whole string sensitive even though it looks like configuration.
  • Certificates and private keys — TLS certificates' private keys, code-signing keys, JWT signing keys (directly relevant to this series' JWT Validation guide).
  • Encryption keys — keys used to encrypt data at rest or in transit within your own application.

The less obvious categories, easy to overlook

Connection strings that LOOK like configuration but contain embedded credentials:
  "Server=db.internal;Database=Orders;User Id=app;Password=..."

Webhook URLs with embedded tokens:
  "https://hooks.example.com/services/T00000/B00000/XXXXXXXXXXXXXXXXXXXXXXXX"

Internal hostnames/IP ranges, in a sufficiently sensitive threat model, revealing infrastructure topology
Enter fullscreen mode Exit fullscreen mode

A connection string is a classic example of something that looks like ordinary configuration but is, in fact, a secret the moment it contains an embedded password — treating it identically to a genuinely non-sensitive setting (like a timeout value) because it "lives in the same config file" is a common, easy mistake. Similarly, a webhook URL with a token baked directly into the path is functionally a bearer credential, even though it superficially resembles a plain URL.

Not everything in configuration is a secret

ASPNETCORE_ENVIRONMENT=Production        ← not sensitive
FeatureFlags:NewCheckoutFlow=true         ← not sensitive
ConnectionStrings:Default=Server=...;Password=...  ← sensitive
ApiKeys:PaymentProvider=sk_live_...        ← sensitive
Enter fullscreen mode Exit fullscreen mode

Being deliberate about which specific configuration values are actually secrets — rather than treating an entire configuration file as uniformly sensitive or uniformly not — is what makes the distinctions in this guide practically actionable, rather than either over-classifying (making everything needlessly hard to work with) or under-classifying (missing something that genuinely needed protection).


2. Why Secrets Don't Belong in Source Control

Git history is forever, by design

git log --all --full-history -- "**/appsettings.Production.json"
git show <commit-hash>:appsettings.Production.json
Enter fullscreen mode Exit fullscreen mode

Even if a secret is committed and then "removed" in a later commit, it remains fully retrievable from Git's history for as long as the repository exists, by anyone with read access to that history — this is Git's core design property (immutable, complete history) working directly against you the moment a secret ends up in it. The only genuinely reliable fix once a secret has been committed is treating it as compromised and rotating it immediately (Section 12) — not just deleting it from the latest commit.

Repository access is broader than production access, usually

A codebase's Git repository is typically readable by every engineer on the team (and, for open-source or misconfigured-visibility repositories, potentially the public) — a far broader audience than should have access to production database credentials or third-party API keys. Committing a secret to source control effectively grants it to that broader audience, regardless of the repository's intended access controls around actual production systems.

The alternative: a reference, not the value

// The application config contains a REFERENCE to where the secret lives, not the secret itself
builder.Configuration.AddAzureKeyVault(keyVaultUri, credential);
Enter fullscreen mode Exit fullscreen mode

The pattern this entire guide builds toward: configuration files and source code contain pointers to secrets (a Key Vault URI, a secret name) — genuinely safe to commit, since they reveal nothing exploitable on their own — while the actual sensitive values live exclusively in a purpose-built secret store with its own access controls, audit logging, and rotation capability.


3. Azure Key Vault

Core concepts

az keyvault create --name my-app-vault --resource-group my-rg --location eastus

az keyvault secret set --vault-name my-app-vault --name "DatabaseConnectionString" --value "Server=...;Password=..."

az keyvault secret show --vault-name my-app-vault --name "DatabaseConnectionString"
Enter fullscreen mode Exit fullscreen mode

Azure Key Vault stores three distinct types of sensitive material, each with slightly different handling: secrets (arbitrary sensitive strings — passwords, connection strings, API keys), keys (cryptographic keys used for encryption/signing operations, which can be used without ever being extracted from the vault — the vault performs the cryptographic operation itself), and certificates (X.509 certificates with integrated lifecycle management, covered in Section 10).

Access policies and RBAC

az keyvault set-policy --name my-app-vault --object-id <managed-identity-object-id> --secret-permissions get list
Enter fullscreen mode Exit fullscreen mode
# Or, the more modern, recommended approach: Azure RBAC applied to the vault itself
az role assignment create --role "Key Vault Secrets User" --assignee <managed-identity-object-id> --scope <vault-resource-id>
Enter fullscreen mode Exit fullscreen mode

Key Vault supports two access control models — the older, vault-specific access policies, and the newer, generally recommended Azure RBAC integration, which applies the same role-based access control model covered in this series' Azure Compute guide directly to vault resources, giving more granular, centrally-auditable permission management consistent with how the rest of an Azure environment's access is governed.

Versioning: every update creates a new version, the old one still retrievable

az keyvault secret set --vault-name my-app-vault --name "DatabaseConnectionString" --value "new-value"
az keyvault secret show --vault-name my-app-vault --name "DatabaseConnectionString" --version <old-version-id>
Enter fullscreen mode Exit fullscreen mode

Updating a secret doesn't overwrite it — it creates a new version, with previous versions still individually retrievable by ID (though not exposed by default) — this is what makes rotation (Section 9) safe: a brief overlap window where both old and new values are valid means a rolling deployment (some instances still running old code, per this series' CI/CD Pipelines guide) doesn't experience a hard cutover failure.

Soft-delete and purge protection

az keyvault update --name my-app-vault --enable-soft-delete true --enable-purge-protection true
Enter fullscreen mode Exit fullscreen mode

Soft-delete means a deleted vault or secret is recoverable for a retention period (rather than immediately, irrecoverably gone) — protecting against accidental deletion; purge protection goes further, preventing even an authorized user from permanently purging a soft-deleted vault before its retention period expires, a deliberate friction point against both accidental and malicious permanent deletion.

Audit logging

az monitor diagnostic-settings create --resource <vault-resource-id> \
    --logs '[{"category": "AuditEvent", "enabled": true}]' \
    --workspace <log-analytics-workspace-id>
Enter fullscreen mode Exit fullscreen mode

Every access to a Key Vault secret — who accessed it, when, from what — is logged, feeding directly into the security logging and monitoring practices covered in this series' OWASP Top 10 guide; an unusual pattern of secret access (an identity retrieving many secrets it's never accessed before, at an unusual hour) is a genuine, actionable security signal.


4. AWS Secrets Manager and Parameter Store

AWS Secrets Manager

aws secretsmanager create-secret --name prod/database/connection-string --secret-string "Server=...;Password=..."

aws secretsmanager get-secret-value --secret-id prod/database/connection-string
Enter fullscreen mode Exit fullscreen mode

AWS Secrets Manager is functionally similar to Key Vault's secrets capability — versioned, access-controlled (via IAM, consistent with the least-privilege task-role guidance covered in this series' AWS Compute guide), audit-logged (via CloudTrail), and with built-in automatic rotation support for several common secret types (RDS database credentials especially) via Lambda-based rotation functions.

AWS Systems Manager Parameter Store

aws ssm put-parameter --name /myapp/prod/api-key --value "sk_live_..." --type SecureString --key-id alias/my-kms-key
Enter fullscreen mode Exit fullscreen mode

Parameter Store is a lighter-weight, often more cost-effective alternative for configuration and secrets alike — SecureString parameters are encrypted at rest via AWS KMS, and it integrates natively with ECS task definitions and Lambda environment variable configuration. The general guidance: Secrets Manager for genuinely sensitive credentials specifically benefiting from automatic rotation and finer-grained access policies; Parameter Store for a broader mix of configuration (sensitive and non-sensitive) where the additional Secrets Manager-specific features aren't needed, at meaningfully lower cost for high parameter counts.

Referencing secrets directly from ECS task definitions

{
  "containerDefinitions": [{
    "secrets": [
      { "name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/database/password" }
    ]
  }]
}
Enter fullscreen mode Exit fullscreen mode

As covered in this series' AWS Compute guide, ECS can inject a secret directly from Secrets Manager or Parameter Store as a container environment variable at task startup — the secret value is fetched by the ECS agent itself using the task's IAM role, never passing through or being visible in the task definition JSON itself.


5. .NET Integration Patterns

Azure Key Vault configuration provider

var builder = WebApplication.CreateBuilder(args);

var keyVaultUri = new Uri(builder.Configuration["KeyVaultUri"]!);
builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential());

var app = builder.Build();
Enter fullscreen mode Exit fullscreen mode
// A Key Vault secret named "ConnectionStrings--Default" becomes accessible exactly like
// any other configuration value — the "--" convention maps to nested configuration sections
var connectionString = builder.Configuration["ConnectionStrings:Default"];
Enter fullscreen mode Exit fullscreen mode

This is the cleanest integration pattern for ASP.NET Core applications — Key Vault secrets are merged directly into the standard IConfiguration system covered in this series' ASP.NET Core guide, meaning application code doesn't need to know or care whether a given configuration value came from appsettings.json, an environment variable, or Key Vault; it's all just IConfiguration from the code's perspective.

DefaultAzureCredential: one credential type across every environment

builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential());
Enter fullscreen mode Exit fullscreen mode

DefaultAzureCredential automatically tries several authentication methods in sequence — Managed Identity when running in Azure (Section 8), environment variables, Azure CLI credentials when running locally, Visual Studio's signed-in account — meaning the identical line of code authenticates correctly whether running in a local development environment or deployed to Azure App Service/AKS, without environment-specific branching logic in application code.

Direct SecretClient usage for on-demand access

var client = new SecretClient(keyVaultUri, new DefaultAzureCredential());
KeyVaultSecret secret = await client.GetSecretAsync("ApiKeys--PaymentProvider");
var apiKey = secret.Value;
Enter fullscreen mode Exit fullscreen mode

For scenarios needing more explicit control than the configuration-provider integration offers — fetching a secret only when a specific code path actually needs it, rather than loading everything at startup — the SecretClient SDK provides direct, on-demand access.

AWS SDK equivalent

var client = new AmazonSecretsManagerClient();
var response = await client.GetSecretValueAsync(new GetSecretValueRequest { SecretId = "prod/database/connection-string" });
var connectionString = response.SecretString;
Enter fullscreen mode Exit fullscreen mode

The AWS SDK for .NET provides the equivalent direct-access pattern for Secrets Manager, and AWS's Amazon.Extensions.Configuration.SystemsManager package provides a similar IConfiguration-integrated experience for Parameter Store, mirroring the Key Vault configuration provider pattern above.


6. Secrets in Kubernetes

The gap this series' Kubernetes/Helm and GitOps guides already flagged

As covered in both this series' Kubernetes/Helm and GitOps guides, native Kubernetes Secrets are only base64-encoded, not encrypted, by default — a distinction that's easy to misunderstand as a genuine security guarantee it doesn't actually provide.

kubectl get secret product-api-secrets -o jsonpath='{.data.ConnectionStrings__Default}' | base64 -d
Enter fullscreen mode Exit fullscreen mode

Anyone with read access to Secret objects (or to the underlying etcd datastore backing the cluster) can trivially recover the plaintext with a one-line command — genuine protection requires additional layers.

Encryption at rest for etcd

# EncryptionConfiguration applied at the cluster level
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: ["secrets"]
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-encryption-key>
Enter fullscreen mode Exit fullscreen mode

Configuring etcd encryption at rest (often handled automatically by managed Kubernetes offerings like AKS/EKS, but worth explicitly confirming rather than assuming) ensures the underlying data store itself doesn't hold plaintext secret values, addressing one layer of the base64-isn't-encryption gap.

External Secrets Operator: the generally recommended pattern

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: product-api-secrets
spec:
  secretStoreRef:
    name: azure-keyvault-store
    kind: SecretStore
  target:
    name: product-api-secrets
  data:
    - secretKey: ConnectionStrings__Default
      remoteRef:
        key: database-connection-string
Enter fullscreen mode Exit fullscreen mode

As covered in this series' GitOps guide, this pattern treats the Git-committed Kubernetes object as a reference to a secret living in Key Vault/Secrets Manager, with an operator running in the cluster fetching and materializing the actual value — keeping the single, centralized secrets manager as the true source of truth rather than duplicating secret material into Kubernetes' own storage as a second, separately-managed copy.


7. Secrets in CI/CD Pipelines

The pipeline itself as a sensitive system

As covered in this series' GitHub Actions and Azure DevOps guides, a CI/CD pipeline frequently needs credentials — to push a container image, to deploy to a cloud environment, to publish a package — making the pipeline's own secret storage a genuinely high-value target in its own right.

Platform-native secret stores

# GitHub Actions
- run: az webapp deploy --name my-api
  env:
    AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
Enter fullscreen mode Exit fullscreen mode
# Azure DevOps — sourced from a variable group, potentially linked to Key Vault directly
variables:
  - group: 'production-secrets'
Enter fullscreen mode Exit fullscreen mode

Both platforms provide encrypted, audit-logged, log-redacted secret storage as covered in their respective guides — the general principle is using the CI/CD platform's own native secret mechanism rather than, for instance, storing a secret in a plain configuration file within the repository that the pipeline reads from.

OIDC federation: the strongest available pattern

permissions:
  id-token: write
steps:
  - uses: azure/login@v2
    with:
      client-id: ${{ secrets.AZURE_CLIENT_ID }}   # not a secret value itself — a public client identifier
      tenant-id: ${{ secrets.AZURE_TENANT_ID }}
Enter fullscreen mode Exit fullscreen mode

As covered in this series' GitHub Actions guide, OIDC federation lets a pipeline authenticate to a cloud provider using a short-lived, dynamically-issued token rather than a stored, long-lived credential at all — this is worth restating here as the single most impactful secret-management improvement available for CI/CD specifically: it removes an entire category of "a long-lived cloud credential sits in CI/CD secret storage" risk, rather than just storing that credential more carefully.

Secrets flowing from CI/CD into deployed infrastructure

# Terraform, referencing a secret from a variable rather than a literal value
variable "db_password" {
  sensitive = true
}
Enter fullscreen mode Exit fullscreen mode

As covered in this series' Terraform/Bicep guide, infrastructure-as-code tools support marking variables as sensitive (suppressing them from plan/apply output logs), and should source genuinely sensitive values from the pipeline's own secret store or, better, directly from Key Vault/Secrets Manager via a data source — never as a literal value embedded in committed .tf/.bicep files.


8. Managed Identity: Eliminating the Bootstrap Secret

The chicken-and-egg problem secret management naturally runs into

"To fetch secrets from Key Vault, my application needs to authenticate to Key Vault...
 ...but authenticating requires a credential... which is itself a secret... that needs to be stored somewhere"
Enter fullscreen mode Exit fullscreen mode

Every secret management approach eventually confronts this bootstrapping problem — something needs an initial credential to access the secret store itself, and if that initial credential is just another static secret sitting in configuration, you haven't actually eliminated the core risk, just moved it one level up.

Managed Identity: identity derived from the platform itself, no stored credential at all

builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential());
Enter fullscreen mode Exit fullscreen mode
az webapp identity assign --name my-api --resource-group my-rg
az keyvault set-policy --name my-app-vault --object-id <the-app-service-managed-identity> --secret-permissions get list
Enter fullscreen mode Exit fullscreen mode

Managed Identity (Azure's implementation; AWS's equivalent is IAM roles for EC2/ECS/Lambda, covered in this series' AWS Compute guide) solves the bootstrap problem by deriving an application's identity from the compute platform itself — an App Service instance, an AKS pod, an Azure Function automatically has an identity the platform vouches for, with no credential ever explicitly stored, configured, or rotated by a developer at all. DefaultAzureCredential (Section 5) automatically discovers and uses this identity when running on Azure infrastructure.

Why this is the single most impactful secret management improvement available

Every other secret management technique in this guide is about protecting a stored credential more carefully — Managed Identity eliminates the stored credential from the equation for the specific, common case of "my application needs to authenticate to Azure services," which removes the single most common and consequential category of secret leak (a static cloud credential accidentally exposed) at its root, rather than mitigating it after the fact.

AWS's equivalent: IAM roles

// ECS task definition  the task role grants Secrets Manager access with NO stored AWS credentials anywhere
{ "taskRoleArn": "arn:aws:iam::123456789:role/product-api-task-role" }
Enter fullscreen mode Exit fullscreen mode

As covered in this series' AWS Compute guide, an ECS task role or an EC2 instance profile provides the identical benefit within AWS — the compute platform itself vouches for the application's identity to AWS APIs (including Secrets Manager), with no access key or secret key ever needing to be stored in application configuration.


9. Secret Rotation

Why rotation matters even without a known compromise

Regularly rotating secrets — not just in response to a suspected leak, but on a routine schedule — limits the exposure window of any leak that hasn't yet been detected; a secret that's silently been exposed (in an old log file, a forgotten backup, a departed employee's local environment) becomes worthless to an attacker once it's rotated, regardless of whether anyone ever realized the original exposure happened.

Automatic rotation for database credentials

aws secretsmanager rotate-secret --secret-id prod/database/connection-string \
    --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789:function:SecretsManagerRDSRotation \
    --rotation-rules AutomaticallyAfterDays=30
Enter fullscreen mode Exit fullscreen mode

AWS Secrets Manager's built-in rotation for RDS database credentials handles the full cycle automatically — creating a new database user/password, updating the secret, and (after a defined propagation window) deactivating the old credential — without a human manually coordinating a credential change across the database and every application that depends on it.

Azure Key Vault: rotation policies + Event Grid notifications trigger a similar workflow,
                  typically requiring a custom Function/Logic App to perform the actual rotation logic
                  (Key Vault provides the scheduling/notification scaffolding, not a fully turnkey
                  rotation implementation for arbitrary secret types the way AWS RDS rotation is)
Enter fullscreen mode Exit fullscreen mode

Rotation without downtime: the overlap window

1. New secret version created, old version still valid
2. Application instances gradually pick up the new version (via cache refresh, restart, or explicit re-fetch)
3. Once ALL instances are confirmed on the new version, the old version is deactivated/deleted
Enter fullscreen mode Exit fullscreen mode

This mirrors the expand/contract pattern covered in this series' Database Migrations guide — a safe rotation isn't an instantaneous swap, it's a brief period where both old and new credentials work simultaneously, giving every application instance (potentially mid-rolling-deployment, per this series' CI/CD Pipelines guide) time to pick up the new value before the old one stops working.

Rotating certificates specifically

Certificate rotation carries the same overlap-window principle but with an added wrinkle — clients need to trust the new certificate before the old one expires, which for externally-facing TLS certificates specifically benefits from automated issuance and renewal (Section 10) rather than manual, calendar-reminder-driven rotation, which has a long, well-documented history of expired-certificate outages caused simply by a rotation reminder being missed.


10. Certificate Management

Why certificates deserve their own consideration within secret management

A TLS certificate's private key is a secret in every sense covered so far, but certificates also carry unique lifecycle concerns — a defined expiration date, a chain of trust back to a certificate authority, and (for public-facing certificates) domain validation requirements — that don't apply to a simple password or API key.

Azure Key Vault's integrated certificate management

az keyvault certificate create --vault-name my-app-vault --name my-api-cert \
    --policy "$(az keyvault certificate get-default-policy)"
Enter fullscreen mode Exit fullscreen mode

Key Vault can manage a certificate's full lifecycle — generating the key pair, handling renewal (including automatic renewal shortly before expiry, when integrated with a supported CA), and exposing both the public certificate and, separately and more tightly access-controlled, the private key — directly to applications or to Azure resources like App Service and Application Gateway.

Let's Encrypt and automated public certificate issuance

# cert-manager in Kubernetes, requesting a Let's Encrypt certificate automatically
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-api-tls
spec:
  secretName: my-api-tls-secret
  issuerRef:
    name: letsencrypt-prod
  dnsNames:
    - api.example.com
Enter fullscreen mode Exit fullscreen mode

For internet-facing services, cert-manager (in Kubernetes environments, connecting to this series' Kubernetes/Helm guide) automates the entire Let's Encrypt issuance and renewal cycle — requesting a certificate, completing domain validation, and renewing well before expiry, with the resulting certificate materialized as a Kubernetes Secret an Ingress controller can reference directly. This has made "an expired certificate caused a production outage" a substantially rarer failure mode than it was before automated certificate lifecycle tooling became standard.

Internal/mTLS certificates

For service-to-service mutual TLS (referenced in this series' gRPC guide), internal certificate authorities (often via a service mesh's built-in CA, like Istio's, or a dedicated internal PKI) issue and rotate short-lived certificates automatically for every service instance — applying the same "short-lived, automatically rotated, never manually managed" principle internally that Let's Encrypt/cert-manager applies to public-facing certificates.


11. Local Development Without Compromising Security

The temptation, and why it's a real risk

// appsettings.Development.json  tempting to just paste real credentials here for convenience
{ "ConnectionStrings": { "Default": "Server=prod-db;Password=RealProductionPassword!" } }
Enter fullscreen mode Exit fullscreen mode

Using real production credentials for local development convenience is a genuinely common shortcut that meaningfully expands the exposure surface of production secrets — now present on every developer's laptop, in shell history, potentially in IDE state, with none of the access controls or audit logging the actual secret store provides.

User Secrets for local-only configuration

dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;Database=DevDb;Trusted_Connection=true"
Enter fullscreen mode Exit fullscreen mode

ASP.NET Core's Secret Manager (dotnet user-secrets) stores developer-specific configuration outside the project directory entirely (in a per-user profile location), specifically so it can never be accidentally committed to source control — the right tool for local development values, especially ones a developer might otherwise be tempted to hardcode directly into appsettings.Development.json.

Local development against a real Key Vault, with limited-scope access

builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential());
Enter fullscreen mode Exit fullscreen mode

As covered in Section 5, DefaultAzureCredential transparently uses a developer's own Azure CLI login when running locally — a common, more robust pattern than either hardcoded local secrets or fully mocked-out configuration is pointing local development at a genuinely separate, narrowly-scoped development Key Vault (containing non-production credentials for non-production resources), authenticated via the developer's own identity with read-only access, keeping the exact same code path exercised locally as in every deployed environment.

Never using genuinely shared, static local secrets across a team

A shared .env file with real credentials, distributed via Slack or a shared drive, defeats individual accountability (who actually used this credential, and when) and makes rotation painful (everyone's copy needs updating) — even for local development, per-developer scoped access to a dedicated non-production secret store scales better than a shared static file, even though it requires slightly more upfront setup.


12. Detecting and Responding to a Leaked Secret

Automated secret scanning

# GitHub push protection / secret scanning — blocks a push containing a recognizable secret pattern
Enter fullscreen mode Exit fullscreen mode
# gitleaks / trufflehog — scanning repository history for previously-committed secrets
gitleaks detect --source . --verbose
Enter fullscreen mode Exit fullscreen mode

GitHub's built-in secret scanning (and push protection, which can block a commit containing a recognizable secret pattern before it's even pushed) catches many common secret formats automatically; dedicated tools like gitleaks or trufflehog scan a repository's full history for anything that might have been committed and later "removed," which — as covered in Section 2 — remains fully present in history regardless.

The incident response sequence once a leak is confirmed

1. Rotate the secret IMMEDIATELY — assume it's compromised the moment leak is suspected, don't wait for confirmation
2. Investigate scope: what could the leaked credential have accessed, and for how long was it exposed
3. Review logs (Section 3's audit logging) for any actual unauthorized use during the exposure window
4. Remove the secret from wherever it was exposed (revoke a public gist, clean Git history if truly necessary)
5. Root-cause: how did this happen, and what process/tooling gap allowed it
Enter fullscreen mode Exit fullscreen mode

The single most important, time-sensitive step is rotation — everything else (investigation, cleanup, root-cause analysis) matters, but none of it undoes the exposure the way immediate rotation does; a leaked-but-promptly-rotated secret has a bounded, often quite short exposure window, while a leaked-and-not-yet-rotated secret remains actively exploitable for as long as that delay continues.

Why "just delete it from Git" is not a fix

As covered in Section 2, removing a secret from the latest commit doesn't remove it from history — treating a leaked secret as compromised and rotating it is the only reliable remediation; Git history cleanup (via git filter-repo or similar, and force-pushing a rewritten history) is, at best, a secondary cleanup step to reduce ongoing visibility, never a substitute for rotation.


13. Common Pitfalls

Pitfall Why it hurts Better approach
Committing a secret, then just removing it in a later commit Fully recoverable from Git history indefinitely Treat as compromised; rotate immediately, regardless of history cleanup
Using production credentials for local development Expands exposure surface with no access controls or audit trail Use dotnet user-secrets or a scoped, non-production secret store
A long-lived, static bootstrap credential to access the secret store itself Just moves the core risk up one level, doesn't eliminate it Use Managed Identity/IAM roles wherever the compute platform supports it
Treating Kubernetes Secrets' base64 encoding as encryption Trivially decodable by anyone with read access Enable etcd encryption at rest; use an External Secrets Operator
Manual, calendar-reminder-driven certificate renewal A missed reminder causes an expired-certificate outage Automate renewal (cert-manager/Let's Encrypt, Key Vault-integrated CAs)
No automated secret scanning on commits/pushes Leaked secrets go undetected until actively exploited Enable push protection and periodic repository history scanning
A shared, static local-dev secrets file distributed across the team No individual accountability, painful to rotate Per-developer scoped access to a dedicated non-production secret store
Rotation with a hard cutover instead of an overlap window Breaks in-flight requests / rolling-deployment instances still on the old value Keep both old and new versions valid briefly, mirroring expand/contract

Quick Reference Table

Concept Purpose
Azure Key Vault / AWS Secrets Manager Centralized, access-controlled, audit-logged secret storage
DefaultAzureCredential One credential abstraction working identically across local dev and Azure
Managed Identity / IAM roles Eliminates the bootstrap-credential problem entirely
Secret versioning Enables safe, overlap-window rotation without a hard cutover
External Secrets Operator Kubernetes Secrets as references to a centralized secret store, not duplicated storage
OIDC federation (CI/CD) Removes long-lived cloud credentials from pipeline secret storage entirely
dotnet user-secrets Local-only configuration, stored outside the project/repository
cert-manager / Let's Encrypt Automated certificate issuance and renewal, avoiding expiry-driven outages
Secret scanning / push protection Automated detection of accidentally committed secrets
Rotation Bounds the exposure window of a leak, detected or not

Conclusion

Secret management is less about any single tool and more about a consistent discipline applied everywhere a credential exists: never in source control, never as a stored long-lived credential when a platform-native identity (Managed Identity, IAM roles) can eliminate the need entirely, always versioned and rotatable without a hard cutover, and always monitored so unusual access is actually visible rather than only discoverable after the fact. Every piece of this series that's touched on secrets — GitHub Actions' OIDC federation, the GitOps guide's External Secrets Operator, the Docker guide's warning against baking secrets into image layers, the Terraform/Bicep guide's sensitive variable handling — is a specific application of the same small set of principles covered here in full.

The single highest-leverage improvement available to most organizations is adopting Managed Identity/IAM roles wherever the underlying platform supports it, since it removes an entire, historically very common category of leak (a static cloud credential sitting somewhere it shouldn't) at its root rather than mitigating it after the fact. Everything else in this guide — rotation, audit logging, scanning, certificate automation — is genuinely valuable defense in depth, but eliminating the stored credential in the first place, wherever possible, remains the most effective secret management strategy of all.


Found this useful? Feel free to star the repo, open an issue with corrections, or share the rotation that went smoothly because of an overlap window, instead of the outage it could have been.

Top comments (0)