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.
Provides comprehensive guidance for secure secrets management across storage solutions (Vault, AWS/Azure Key Vault), rotation strategies, and leak prevention. Use when implementing secrets storage, configuring secret rotation, setting up scanning tools, or securing CI/CD pipelines.
/plugin marketplace add melodic-software/claude-code-plugins/plugin install security@melodic-softwareThis skill is limited to using the following tools:
references/secrets-scanning.mdreferences/vault-patterns.mdComprehensive guidance for securely storing, accessing, rotating, and protecting secrets.
Use this skill when:
| 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 |
| 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!) |
# 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
using System.Text.Json;
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;
/// <summary>
/// HashiCorp Vault client for secrets retrieval.
/// </summary>
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);
}
/// <summary>
/// Get a secret from Vault KV v2.
/// </summary>
public async Task<string> 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()!;
}
/// <summary>
/// Get database credentials.
/// </summary>
public async Task<DatabaseCredentials> 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();
# 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
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using System.Text.Json;
/// <summary>
/// AWS Secrets Manager client.
/// </summary>
public sealed class AwsSecretsClient(IAmazonSecretsManager client)
{
/// <summary>
/// Retrieve secret from AWS Secrets Manager.
/// </summary>
public async Task<T> GetSecretAsync<T>(string secretName, CancellationToken cancellationToken = default)
{
var response = await client.GetSecretValueAsync(
new GetSecretValueRequest { SecretId = secretName },
cancellationToken
);
return JsonSerializer.Deserialize<T>(response.SecretString)!;
}
}
// Usage with DI
public sealed record DbCredentials(string Username, string Password);
// In Startup/Program.cs
services.AddAWSService<IAmazonSecretsManager>();
services.AddSingleton<AwsSecretsClient>();
// In application code
var dbCreds = await secretsClient.GetSecretAsync<DbCredentials>("prod/myapp/database");
// Returns: DbCredentials { Username = "dbuser", Password = "secret" }
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using System.Text.Json;
/// <summary>
/// Create secret with automatic rotation enabled.
/// </summary>
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);
}
# 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"
using Microsoft.Extensions.Configuration;
/// <summary>
/// Application configuration loaded from environment variables.
/// </summary>
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<Program>(optional: true) // For development
.Build();
// Bind to strongly-typed config
services.Configure<AppConfig>(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<AppConfig>()
.Bind(configuration.GetSection("App"))
.ValidateDataAnnotations()
.ValidateOnStart();
// In application code
public class MyService(IOptions<AppConfig> config)
{
private readonly AppConfig _config = config.Value;
}
# .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 - ALWAYS include
.env
.env.local
.env.*.local
*.pem
*.key
secrets/
using System.Security.Cryptography;
/// <summary>
/// Secret rotation with overlap period for zero-downtime rotation.
/// </summary>
public sealed class SecretRotator(ISecretsStore secrets, INotificationClient notifications)
{
private static readonly TimeSpan GracePeriod = TimeSpan.FromHours(24);
/// <summary>
/// Rotate an API key with overlap period.
/// </summary>
public async Task<string> 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;
}
/// <summary>
/// Accept both old and new keys during rotation.
/// </summary>
public async Task<bool> 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<string> GetAsync(string key, CancellationToken cancellationToken);
Task<string?> 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);
}
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
# .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']
# GitHub Actions
name: Security Scan
on: [push, pull_request]
jobs:
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
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
| 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
# 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
# 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
Where should I store this secret?
// 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}");
// 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");
| Skill | Relationship |
|---|---|
cryptography | Encryption for secrets at rest |
devsecops-practices | CI/CD security integration |
authentication-patterns | API key and token management |
Last Updated: 2025-12-26
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.