---
name: secrets-management
description: Comprehensive guidance for secure secrets management including storage solutions (Vault, AWS Secrets Manager, Azure Key Vault), environment variables, secret rotation, scanning tools, and CI/CD pipeline security. Use when implementing secrets storage, configuring secret rotation, preventing secret leaks, or reviewing credentials handling.
allowed-tools: Read, Glob, Grep, Task, Bash
---
# Secrets Management
Comprehensive guidance for securely storing, accessing, rotating, and protecting secrets.
## When to Use This Skill
Use this skill when:
- Choosing a secrets management solution
- Implementing secret rotation
- Preventing secrets in source code
- Configuring CI/CD pipeline secrets
- Setting up secrets scanning
- Reviewing credentials handling
- Migrating from insecure secret storage
## Secrets Management Solutions
### Comparison Matrix
| Solution | Self-Hosted | Cloud | Dynamic Secrets | Rotation | Cost |
|----------|-------------|-------|-----------------|----------|------|
| HashiCorp Vault | ✅ | ✅ | ✅ | ✅ | Free (OSS) / $$ |
| AWS Secrets Manager | ❌ | ✅ | ❌ | ✅ | $ |
| Azure Key Vault | ❌ | ✅ | ❌ | ✅ | $ |
| Google Secret Manager | ❌ | ✅ | ❌ | ✅ | $ |
| Doppler | ❌ | ✅ | ❌ | ❌ | $$ |
| Environment Variables | ✅ | ✅ | ❌ | Manual | Free |
### When to Use What
| Use Case | Recommended Solution |
|----------|---------------------|
| Enterprise, multi-cloud | HashiCorp Vault |
| AWS-native applications | AWS Secrets Manager |
| Azure-native applications | Azure Key Vault |
| GCP-native applications | Google Secret Manager |
| Simple applications | Environment variables |
| Development | .env files (never commit!) |
## HashiCorp Vault
### Basic Usage
```bash
# Enable secrets engine
vault secrets enable -path=secret kv-v2
# Store a secret
vault kv put secret/myapp/database \
username="dbuser" \
password="supersecret"
# Read a secret
vault kv get secret/myapp/database
# Get specific field
vault kv get -field=password secret/myapp/database
```
### Application Integration (C#)
```csharp
using System.Text.Json;
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;
///
/// HashiCorp Vault client for secrets retrieval.
///
public sealed class VaultClient
{
private readonly IVaultClient _client;
public VaultClient(string url, string token)
{
var authMethod = new TokenAuthMethodInfo(token);
var settings = new VaultClientSettings(url, authMethod);
_client = new VaultSharp.VaultClient(settings);
}
///
/// Get a secret from Vault KV v2.
///
public async Task GetSecretAsync(string path, string key, CancellationToken cancellationToken = default)
{
var secret = await _client.V1.Secrets.KeyValue.V2.ReadSecretAsync(path: path);
return secret.Data.Data[key].ToString()!;
}
///
/// Get database credentials.
///
public async Task GetDatabaseCredentialsAsync(CancellationToken cancellationToken = default)
{
return new DatabaseCredentials(
Username: await GetSecretAsync("myapp/database", "username", cancellationToken),
Password: await GetSecretAsync("myapp/database", "password", cancellationToken)
);
}
}
public sealed record DatabaseCredentials(string Username, string Password);
// Usage
var vault = new VaultClient(
url: Environment.GetEnvironmentVariable("VAULT_ADDR")!,
token: Environment.GetEnvironmentVariable("VAULT_TOKEN")!
);
var dbCreds = await vault.GetDatabaseCredentialsAsync();
```
### Dynamic Database Credentials
```bash
# Enable database secrets engine
vault secrets enable database
# Configure PostgreSQL connection
vault write database/config/mydb \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/mydb" \
allowed_roles="readonly,readwrite" \
username="vault" \
password="vault-password"
# Create a role
vault write database/roles/readonly \
db_name=mydb \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
# Get dynamic credentials
vault read database/creds/readonly
# Returns: username=v-token-readonly-xxx, password=xxx, lease_id=xxx
```
**For detailed Vault patterns:** See [Vault Patterns Reference](references/vault-patterns.md)
## AWS Secrets Manager
### Store and Retrieve Secrets
```csharp
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using System.Text.Json;
///
/// AWS Secrets Manager client.
///
public sealed class AwsSecretsClient(IAmazonSecretsManager client)
{
///
/// Retrieve secret from AWS Secrets Manager.
///
public async Task GetSecretAsync(string secretName, CancellationToken cancellationToken = default)
{
var response = await client.GetSecretValueAsync(
new GetSecretValueRequest { SecretId = secretName },
cancellationToken
);
return JsonSerializer.Deserialize(response.SecretString)!;
}
}
// Usage with DI
public sealed record DbCredentials(string Username, string Password);
// In Startup/Program.cs
services.AddAWSService();
services.AddSingleton();
// In application code
var dbCreds = await secretsClient.GetSecretAsync("prod/myapp/database");
// Returns: DbCredentials { Username = "dbuser", Password = "secret" }
```
### Automatic Rotation
```csharp
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using System.Text.Json;
///
/// Create secret with automatic rotation enabled.
///
public static async Task CreateSecretWithRotationAsync(
IAmazonSecretsManager client,
string secretName,
object secretValue,
string rotationLambdaArn,
int rotationDays = 30,
CancellationToken cancellationToken = default)
{
// Create the secret
await client.CreateSecretAsync(new CreateSecretRequest
{
Name = secretName,
SecretString = JsonSerializer.Serialize(secretValue)
}, cancellationToken);
// Enable rotation (requires Lambda function)
await client.RotateSecretAsync(new RotateSecretRequest
{
SecretId = secretName,
RotationLambdaARN = rotationLambdaArn,
RotationRules = new RotationRulesType
{
AutomaticallyAfterDays = rotationDays
}
}, cancellationToken);
}
```
## Environment Variables
### Best Practices
```bash
# Set environment variables (not in code!)
export DATABASE_URL="postgresql://user:pass@localhost/db"
export API_KEY="sk_live_xxx"
# In systemd service file
[Service]
Environment="DATABASE_URL=postgresql://user:pass@localhost/db"
EnvironmentFile=/etc/myapp/secrets.env
# In Docker
docker run -e DATABASE_URL="postgresql://..." myapp
# Or from file
docker run --env-file ./secrets.env myapp
# In Kubernetes
kubectl create secret generic myapp-secrets \
--from-literal=DATABASE_URL="postgresql://..." \
--from-literal=API_KEY="sk_live_xxx"
```
### Loading in Application
```csharp
using Microsoft.Extensions.Configuration;
///
/// Application configuration loaded from environment variables.
///
public sealed class AppConfig
{
public required string DatabaseUrl { get; init; }
public required string ApiKey { get; init; }
public bool Debug { get; init; }
}
// In Program.cs or Startup.cs
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddUserSecrets(optional: true) // For development
.Build();
// Bind to strongly-typed config
services.Configure(options =>
{
options.DatabaseUrl = configuration["DATABASE_URL"]
?? throw new InvalidOperationException("DATABASE_URL is required");
options.ApiKey = configuration["API_KEY"]
?? throw new InvalidOperationException("API_KEY is required");
options.Debug = bool.TryParse(configuration["DEBUG"], out var debug) && debug;
});
// Or use options pattern
services.AddOptions()
.Bind(configuration.GetSection("App"))
.ValidateDataAnnotations()
.ValidateOnStart();
// In application code
public class MyService(IOptions config)
{
private readonly AppConfig _config = config.Value;
}
```
### .env File Security
```bash
# .env (NEVER commit this!)
DATABASE_URL=postgresql://user:pass@localhost/db
API_KEY=sk_live_xxx
# .env.example (commit this as template)
DATABASE_URL=postgresql://user:pass@localhost/db
API_KEY=your-api-key-here
```
```gitignore
# .gitignore - ALWAYS include
.env
.env.local
.env.*.local
*.pem
*.key
secrets/
```
## Secret Rotation
### Rotation Strategy
```csharp
using System.Security.Cryptography;
///
/// Secret rotation with overlap period for zero-downtime rotation.
///
public sealed class SecretRotator(ISecretsStore secrets, INotificationClient notifications)
{
private static readonly TimeSpan GracePeriod = TimeSpan.FromHours(24);
///
/// Rotate an API key with overlap period.
///
public async Task RotateApiKeyAsync(string keyName, CancellationToken cancellationToken = default)
{
// 1. Generate new key
var newKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
.Replace('+', '-').Replace('/', '_').TrimEnd('=');
// 2. Store new key as pending
await secrets.StoreAsync($"{keyName}_pending", newKey, cancellationToken);
// 3. Update primary key (old key still valid)
var oldKey = await secrets.GetAsync(keyName, cancellationToken);
await secrets.StoreAsync($"{keyName}_old", oldKey, cancellationToken);
await secrets.StoreAsync(keyName, newKey, cancellationToken);
// 4. Notify dependent services
await notifications.SendAsync(
$"API key {keyName} rotated. Update your configuration.",
cancellationToken
);
// 5. Schedule old key deletion (grace period)
await secrets.ScheduleDeletionAsync($"{keyName}_old", GracePeriod, cancellationToken);
return newKey;
}
///
/// Accept both old and new keys during rotation.
///
public async Task ValidateDuringRotationAsync(string keyName, string providedKey, CancellationToken cancellationToken = default)
{
var current = await secrets.GetAsync(keyName, cancellationToken);
if (CryptographicOperations.FixedTimeEquals(
System.Text.Encoding.UTF8.GetBytes(providedKey),
System.Text.Encoding.UTF8.GetBytes(current)))
{
return true;
}
var old = await secrets.GetOrDefaultAsync($"{keyName}_old", cancellationToken);
if (old is not null && CryptographicOperations.FixedTimeEquals(
System.Text.Encoding.UTF8.GetBytes(providedKey),
System.Text.Encoding.UTF8.GetBytes(old)))
{
return true;
}
return false;
}
}
// Interfaces for secrets and notifications
public interface ISecretsStore
{
Task GetAsync(string key, CancellationToken cancellationToken);
Task GetOrDefaultAsync(string key, CancellationToken cancellationToken);
Task StoreAsync(string key, string value, CancellationToken cancellationToken);
Task ScheduleDeletionAsync(string key, TimeSpan delay, CancellationToken cancellationToken);
}
public interface INotificationClient
{
Task SendAsync(string message, CancellationToken cancellationToken);
}
```
### Rotation Timeline
```text
Day 0: Generate new key, deploy to secrets manager
├── Old key: ACTIVE
└── New key: PENDING
Day 1: Update applications to use new key
├── Old key: ACTIVE (grace period)
└── New key: ACTIVE
Day 7: Revoke old key
├── Old key: REVOKED
└── New key: ACTIVE
```
## Secrets Scanning
### Pre-commit Scanning
```yaml
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
```
### CI/CD Scanning
```yaml
# GitHub Actions
name: Security Scan
on: [push, pull_request]
jobs:
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # Full history for scanning
- name: Gitleaks scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: TruffleHog scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
extra_args: --only-verified
```
### Scanning Tools Comparison
| Tool | Strengths | Weaknesses |
|------|-----------|------------|
| gitleaks | Fast, good regex patterns | May miss custom formats |
| TruffleHog | Verifies secrets are live | Slower, network calls |
| detect-secrets | Baseline support, plugins | More false positives |
| git-secrets | AWS patterns built-in | AWS-focused |
**For detailed scanning setup:** See [Secrets Scanning Reference](references/secrets-scanning.md)
## CI/CD Pipeline Secrets
### GitHub Actions
```yaml
# Store secrets in repository settings
# Access via ${{ secrets.SECRET_NAME }}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: |
# Secrets available as environment variables
./deploy.sh
# For OIDC authentication (preferred for cloud)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
```
### GitLab CI
```yaml
# Store in Settings > CI/CD > Variables
# Mark as "Masked" and "Protected"
deploy:
script:
- echo "Deploying with DB_PASSWORD=$DB_PASSWORD" # Never do this!
- ./deploy.sh
variables:
# Override for this job only
ENVIRONMENT: production
```
### Best Practices for CI/CD Secrets
1. **Use OIDC when possible** - No long-lived credentials
2. **Mask secrets in logs** - CI systems should auto-mask
3. **Limit secret scope** - Per-environment, per-branch
4. **Audit secret access** - Who accessed what when
5. **Rotate regularly** - Especially after team changes
## Quick Decision Tree
**Where should I store this secret?**
1. **Production database credentials** → Secrets Manager + rotation
2. **API keys for third-party services** → Secrets Manager
3. **Encryption keys** → HSM or Vault
4. **Development credentials** → .env file (gitignored)
5. **CI/CD deployment credentials** → CI/CD secrets + OIDC
6. **Inter-service authentication** → Vault dynamic secrets
7. **User-submitted API keys** → Encrypted database column
## Anti-Patterns to Avoid
### Never Do This
```csharp
// WRONG: Hardcoded secrets
const string ApiKey = "sk_live_abc123";
const string DatabaseUrl = "postgresql://admin:password123@prod.db.example.com/app";
// WRONG: Secrets in appsettings.json (committed to git)
// {
// "Database": {
// "Password": "supersecret"
// }
// }
// WRONG: Secrets in Docker images
// COPY secrets.env /app/secrets.env
// WRONG: Logging secrets
_logger.LogInformation("Connecting with password: {Password}", password);
// WRONG: Secrets in error messages
throw new Exception($"Failed to connect: {connectionString}");
// WRONG: Secrets in URLs
await httpClient.GetAsync($"https://api.example.com?api_key={apiKey}");
```
### Do This Instead
```csharp
// RIGHT: Environment variables
var apiKey = Environment.GetEnvironmentVariable("API_KEY")
?? throw new InvalidOperationException("API_KEY not configured");
// RIGHT: Secrets manager
var apiKey = await secretsManager.GetSecretAsync("api-key");
// RIGHT: Configuration with User Secrets (dev) or Azure Key Vault (prod)
var apiKey = configuration["ApiKey"];
// RIGHT: Masked logging (use structured logging)
_logger.LogInformation("Connecting to database..."); // No credentials
// RIGHT: Generic error messages
throw new InvalidOperationException("Database connection failed"); // No details
// RIGHT: Secrets in headers (for APIs)
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
await httpClient.GetAsync("https://api.example.com");
```
## Security Checklist
### Storage
- [ ] No hardcoded secrets in source code
- [ ] Secrets stored in dedicated secrets manager
- [ ] Environment variables for configuration
- [ ] .env files gitignored
### Access Control
- [ ] Least privilege access to secrets
- [ ] Audit logging enabled
- [ ] Secrets scoped to environments
- [ ] Regular access reviews
### Rotation
- [ ] Rotation policy defined
- [ ] Automated rotation where possible
- [ ] Grace period for old secrets
- [ ] Notification on rotation
### Detection
- [ ] Pre-commit hooks for secret scanning
- [ ] CI/CD pipeline scanning
- [ ] Git history scanning
- [ ] Regular repository audits
### CI/CD
- [ ] Using CI platform's secrets management
- [ ] OIDC for cloud authentication
- [ ] Secrets masked in logs
- [ ] Limited secret scope
## References
- [Vault Patterns Reference](references/vault-patterns.md) - HashiCorp Vault deep dive
- [Secrets Scanning Reference](references/secrets-scanning.md) - Scanning tools setup
## Related Skills
| Skill | Relationship |
|-------|-------------|
| `cryptography` | Encryption for secrets at rest |
| `devsecops-practices` | CI/CD security integration |
| `authentication-patterns` | API key and token management |
## Version History
- v1.0.0 (2025-12-26): Initial release with Vault, cloud providers, rotation, scanning
---
**Last Updated:** 2025-12-26