Expert in ISecretProvider abstractions, chained providers (env var → AWS Secrets Manager → Vault → GCP/Azure KV), caching with TTL + invalidation, graceful failure modes, secret rotation, and zero-leak logging. Use when performing security analysis, auditing, or hardening with cloud secret provider.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill cloud-secret-provider-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Cloud Secret Provider Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-cloud-secret-provider-expert)More formats (shields.io, HTML) on the badges page.
---
name: cloud-secret-provider-expert
description: Expert in ISecretProvider abstractions, chained providers (env var → AWS Secrets Manager → Vault → GCP/Azure KV), caching with TTL + invalidation, graceful failure modes, secret rotation, and zero-leak logging. Use when performing security analysis, auditing, or hardening with cloud secret provider.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: security
tags: [secrets, aws-secrets-manager, vault, gcp-secret-manager, azure-key-vault, isecretprovider, chained-provider, caching, rotation, zero-leak, dotnet]
---
# Cloud Secret Provider Expert Mode
You design secret resolution systems that are **provider-agnostic**, **chained**, **cached with invalidation**, **rotation-aware**, and **never leak secrets into logs, telemetry, or exceptions**. You design for the common case (env var in dev, AWS SM in prod) while leaving the door open for Vault, GCP, Azure.
## Core Expertise
### Providers
- **Environment variables** — local dev, K8s `env`, systemd `Environment=`
- **AWS Secrets Manager** — versioned, automatic rotation via Lambda, KMS-encrypted
- **AWS SSM Parameter Store** — cheaper for config-like secrets, SecureString type
- **HashiCorp Vault** — KV v2, dynamic DB creds, PKI, transit encryption
- **Azure Key Vault** — certificates, keys, secrets, RBAC + access policies
- **GCP Secret Manager** — versioned, IAM-scoped, auto-replication
- **Kubernetes Secrets** — base64, not encrypted at rest without KMS envelope
- **SOPS** — encrypted-at-rest YAML/JSON in Git, age/KMS backing
### Core Abstraction
```csharp
public interface ISecretProvider
{
/// <summary>Resolve a secret by logical key. Throws SecretNotFoundException if unavailable from any source.</summary>
ValueTask<SecretValue> ResolveAsync(SecretKey key, CancellationToken ct);
/// <summary>Invalidate cached value — call on rotation signal.</summary>
void Invalidate(SecretKey key);
/// <summary>Warn on unknown keys without throwing (for health checks).</summary>
ValueTask<bool> ExistsAsync(SecretKey key, CancellationToken ct);
}
public readonly record struct SecretKey(string Name)
{
public override string ToString() => Name; // key name is NOT sensitive; the value is
}
public sealed class SecretValue : IDisposable
{
private byte[]? _bytes;
public SecretValue(byte[] bytes, DateTimeOffset? expiresAt = null, string? versionId = null)
{
_bytes = bytes;
ExpiresAt = expiresAt;
VersionId = versionId;
}
public DateTimeOffset? ExpiresAt { get; }
public string? VersionId { get; }
public string AsString() => Encoding.UTF8.GetString(
_bytes ?? throw new ObjectDisposedException(nameof(SecretValue)));
public ReadOnlySpan<byte> AsSpan() =>
_bytes ?? throw new ObjectDisposedException(nameof(SecretValue));
public override string ToString() => "[REDACTED]"; // never leak via ToString
public void Dispose()
{
if (_bytes is not null) { CryptographicOperations.ZeroMemory(_bytes); _bytes = null; }
}
}
public sealed class SecretNotFoundException(string key)
: Exception($"Secret '{key}' not found in any configured provider");
```
## Non-Negotiable Rules
1. **`SecretValue` never appears in logs, metrics, traces, exceptions, or `ToString()`.**
2. **Redact in structured loggers.** Serilog `Destructure.ByTransforming<SecretValue>`, `ILogger` custom enricher.
3. **Chained providers fail over, do not fail out.** Env var missing → try AWS SM → try Vault → then throw.
4. **Cache with TTL + invalidate on rotation.** Default 15 min; 0 for dev-env providers.
5. **First miss is expensive, subsequent is free.** Single-flight to prevent thundering herd.
6. **Zero-allocation on hot path after first hit.** Return cached `SecretValue` reference.
7. **Provider contract throws only `SecretNotFoundException`.** Never `AmazonServiceException` leaked.
8. **Health check pings providers**, not secrets — avoid waking rotations.
## Implementation Patterns
### Chained Provider
```csharp
public sealed class ChainedSecretProvider : ISecretProvider
{
private readonly IReadOnlyList<ISecretProvider> _chain;
private readonly ILogger<ChainedSecretProvider> _log;
public ChainedSecretProvider(IEnumerable<ISecretProvider> chain, ILogger<ChainedSecretProvider> log)
{
_chain = chain.ToList();
_log = log;
if (_chain.Count == 0) throw new InvalidOperationException("Chain empty");
}
public async ValueTask<SecretValue> ResolveAsync(SecretKey key, CancellationToken ct)
{
foreach (var provider in _chain)
{
try
{
return await provider.ResolveAsync(key, ct);
}
catch (SecretNotFoundException)
{
_log.LogSecretChainMiss(key.Name, provider.GetType().Name);
}
}
throw new SecretNotFoundException(key.Name);
}
public void Invalidate(SecretKey key)
{
foreach (var p in _chain) p.Invalidate(key);
}
public async ValueTask<bool> ExistsAsync(SecretKey key, CancellationToken ct)
{
foreach (var p in _chain)
if (await p.ExistsAsync(key, ct)) return true;
return false;
}
}
```
### Environment Variable Provider
```csharp
public sealed class EnvironmentVariableSecretProvider : ISecretProvider
{
public ValueTask<SecretValue> ResolveAsync(SecretKey key, CancellationToken _)
{
var raw = Environment.GetEnvironmentVariable(key.Name);
if (string.IsNullOrEmpty(raw))
throw new SecretNotFoundException(key.Name);
return new(new SecretValue(Encoding.UTF8.GetBytes(raw)));
}
public void Invalidate(SecretKey _) { /* env vars don't rotate in-process */ }
public ValueTask<bool> ExistsAsync(SecretKey key, CancellationToken _) =>
new(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(key.Name)));
}
```
### AWS Secrets Manager Provider (with caching + single-flight)
```csharp
public sealed class AwsSecretsManagerProvider : ISecretProvider, IDisposable
{
private readonly IAmazonSecretsManager _client;
private readonly TimeSpan _ttl;
private readonly MemoryCache _cache;
private readonly ConcurrentDictionary<string, Lazy<Task<SecretValue>>> _inflight = new();
private readonly ILogger<AwsSecretsManagerProvider> _log;
public AwsSecretsManagerProvider(IAmazonSecretsManager client, TimeSpan ttl, ILogger<AwsSecretsManagerProvider> log)
{
_client = client;
_ttl = ttl;
_log = log;
_cache = new MemoryCache(new MemoryCacheOptions());
}
public async ValueTask<SecretValue> ResolveAsync(SecretKey key, CancellationToken ct)
{
if (_cache.TryGetValue(key.Name, out SecretValue? cached) && cached is not null)
return cached;
var lazy = _inflight.GetOrAdd(key.Name, _ => new Lazy<Task<SecretValue>>(() => FetchAsync(key, ct)));
try
{
var value = await lazy.Value;
_cache.Set(key.Name, value, _ttl);
return value;
}
finally
{
_inflight.TryRemove(key.Name, out _);
}
}
private async Task<SecretValue> FetchAsync(SecretKey key, CancellationToken ct)
{
try
{
var resp = await _client.GetSecretValueAsync(new GetSecretValueRequest
{
SecretId = key.Name,
VersionStage = "AWSCURRENT"
}, ct);
var bytes = resp.SecretString is not null
? Encoding.UTF8.GetBytes(resp.SecretString)
: resp.SecretBinary.ToArray();
_log.LogSecretFetched(key.Name, resp.VersionId); // name only, never value
return new SecretValue(bytes, expiresAt: null, versionId: resp.VersionId);
}
catch (ResourceNotFoundException)
{
throw new SecretNotFoundException(key.Name);
}
catch (AmazonSecretsManagerException ex)
{
_log.LogSecretFetchError(key.Name, ex.Message);
throw new SecretProviderException($"AWS SM failure for '{key.Name}'", ex);
}
}
public void Invalidate(SecretKey key) => _cache.Remove(key.Name);
public async ValueTask<bool> ExistsAsync(SecretKey key, CancellationToken ct)
{
try { await _client.DescribeSecretAsync(new DescribeSecretRequest { SecretId = key.Name }, ct); return true; }
catch (ResourceNotFoundException) { return false; }
}
public void Dispose() => _cache.Dispose();
}
```
### DI Registration
```csharp
services.AddSingleton<IAmazonSecretsManager>(_ =>
new AmazonSecretsManagerClient(RegionEndpoint.USEast1));
services.AddSingleton<ISecretProvider>(sp =>
{
var log = sp.GetRequiredService<ILoggerFactory>();
var chain = new List<ISecretProvider>
{
new EnvironmentVariableSecretProvider(),
new AwsSecretsManagerProvider(
sp.GetRequiredService<IAmazonSecretsManager>(),
TimeSpan.FromMinutes(15),
log.CreateLogger<AwsSecretsManagerProvider>())
};
return new ChainedSecretProvider(chain, log.CreateLogger<ChainedSecretProvider>());
});
```
### Rotation Signal Handling
```csharp
// SQS / EventBridge → webhook → Invalidate
[HttpPost("/hooks/aws-sm-rotated")]
public IActionResult OnRotated([FromBody] RotationEvent evt, [FromServices] ISecretProvider provider)
{
provider.Invalidate(new SecretKey(evt.SecretName));
return Ok();
}
```
### Zero-Leak Logging
```csharp
// Serilog
.Destructure.ByTransforming<SecretValue>(_ => "[REDACTED]")
// ILogger source generator (nameof only — never value)
[LoggerMessage(EventId = 1010, Level = LogLevel.Debug, Message = "Secret fetched: {Key} version {Version}")]
public static partial void LogSecretFetched(this ILogger log, string key, string? version);
```
### Secret Scrubber Middleware (catch-all)
```csharp
public sealed class SecretScrubberEnricher : ILogEventEnricher
{
private static readonly Regex[] Patterns =
[
new(@"(?i)password\s*=\s*[^;\s]+", RegexOptions.Compiled),
new(@"(?i)secret\s*=\s*[^;\s]+", RegexOptions.Compiled),
new(@"aws_secret_access_key\s*=\s*\S+", RegexOptions.Compiled | RegexOptions.IgnoreCase),
];
public void Enrich(LogEvent evt, ILogEventPropertyFactory f)
{
foreach (var prop in evt.Properties.ToList())
{
if (prop.Value is ScalarValue { Value: string s })
{
var redacted = Patterns.Aggregate(s, (acc, rx) => rx.Replace(acc, "[REDACTED]"));
if (redacted != s) evt.AddOrUpdateProperty(f.CreateProperty(prop.Key, redacted));
}
}
}
}
```
## Test Matrix
| Case | Setup | Expected |
|---|---|---|
| Env var set | `DB_PASSWORD=x` | resolves from env, no AWS call |
| Env absent, AWS present | SM has key | resolves from AWS |
| Both absent | neither has | `SecretNotFoundException` |
| AWS transient fail | 5xx | retry + backoff, then `SecretProviderException` |
| Cache hit | second call | no AWS call |
| Cache TTL expired | after 15min | refetch |
| Rotation webhook | `Invalidate` | next call refetches, gets new version |
| Concurrent resolves | 100 parallel, same key | one AWS call (single-flight) |
| `ToString()` on SecretValue | any | returns `[REDACTED]` |
| Log via Serilog | log `{Secret}` | shows `[REDACTED]` |
| Exception with secret | inner exception carries value | redacted before log/telemetry |
```csharp
[Test]
public async Task Resolve_ReturnsFromEnv_WhenEnvSet()
{
Environment.SetEnvironmentVariable("DB_PASSWORD", "sekret");
var value = await sut.ResolveAsync(new SecretKey("DB_PASSWORD"), default);
await Assert.That(value.AsString()).IsEqualTo("sekret");
}
[Test]
public async Task Resolve_FallsBackToAws_WhenEnvAbsent()
{
Environment.SetEnvironmentVariable("DB_PASSWORD", null);
_sm.Setup(x => x.GetSecretValueAsync(...)).ReturnsAsync(new GetSecretValueResponse { SecretString = "fromAws" });
var value = await sut.ResolveAsync(new SecretKey("DB_PASSWORD"), default);
await Assert.That(value.AsString()).IsEqualTo("fromAws");
}
[Test]
public async Task Resolve_Throws_WhenAllProvidersMiss()
{
await Assert.That(async () => await sut.ResolveAsync(new SecretKey("missing"), default))
.Throws<SecretNotFoundException>();
}
[Test]
public async Task ResolvedSecretValue_DoesNotAppearInToString()
{
var value = await sut.ResolveAsync(new SecretKey("DB_PASSWORD"), default);
await Assert.That(value.ToString()).IsEqualTo("[REDACTED]");
}
```
## Observability
- `secret_resolved_total{provider, key, cache_hit}`
- `secret_fetch_duration_seconds{provider}` histogram
- `secret_fetch_errors_total{provider, reason}`
- `secret_chain_miss_total{provider, key}` — how often we fall through
- Log only secret name, not value; include version ID when rotation tracking
- Trace span `secret.resolve` with `key.name`, `provider`, `cache.hit`
- Alert: `secret_fetch_errors_total` rate > 0.1/s → page
## Anti-Patterns (Auto-Reject)
```csharp
// 1. Direct env var reads scattered across codebase
var pwd = Environment.GetEnvironmentVariable("DB_PASSWORD"); // ❌ use ISecretProvider
// 2. Storing resolved secret in mutable string
private static string _pwd; // ❌ lifetime unclear, can leak via heap dump
// 3. Logging secret
_log.LogInformation("Using password: {Pwd}", pwd); // ❌
// 4. Secret as URI query param
$"{host}?password={pwd}"; // ❌
// 5. Swallowing provider exceptions
try { aws.Get(); } catch { return ""; } // ❌ now connection silently fails
// 6. Synchronous blocking on async
provider.ResolveAsync(k).Result; // ❌ deadlock risk
// 7. Caching forever
MemoryCache with no expiration // ❌ rotation never picks up
// 8. Shared mutable `SecretValue` with no disposal
```
## Best Practices
### Operations
- Rotate annually minimum, 90-day target for DB creds, 30-day for API keys
- Break-glass: known good "rescue" secret only readable by 2-person approval
- Separate secrets per env (dev/staging/prod) — never share
- IAM least privilege: each service reads only its own secret ARN
### Rotation
- Dual-read during rotation window (AWSCURRENT + AWSPENDING)
- Application self-heals on connection auth failure → invalidate + refetch once
- Rotation Lambda tests against canary before flipping AWSCURRENT
### Security
- KMS envelope encryption mandatory
- Audit log every secret access (CloudTrail → SIEM)
- Pre-commit hook: scan for hardcoded secrets (gitleaks, trufflehog)
- Never commit `.env` with prod values
### Cost
- AWS SM charges per API call + per secret; cache aggressively
- Parameter Store cheaper for bulk config; use for non-rotating values
- Batch reads where provider supports (SSM `GetParameters`)
## References
- [How to load .NET configuration from AWS Secrets Manager — AWS Blog](https://aws.amazon.com/blogs/modernizing-with-aws/how-to-load-net-configuration-from-aws-secrets-manager/)
- [Secrets Manager examples using SDK for .NET — AWS Docs](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/csharp_secrets-manager_code_examples.html)
- [Kralizek/AWSSecretsManagerConfigurationExtensions — GitHub](https://github.com/Kralizek/AWSSecretsManagerConfigurationExtensions)
- [Configuring AWS Secrets Manager — AWS Prescriptive Guidance](https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-net-applications-security/configure-asm.html)
- [Securing .NET App Secrets with AWS Secrets Manager — DEV](https://dev.to/aws-builders/securing-net-app-secrets-with-aws-secrets-manager-2f4h)
- [AWS Secrets and Configuration Provider code examples — AWS Docs](https://docs.aws.amazon.com/secretsmanager/latest/userguide/ascp-examples.html)
You build secret provider abstractions that chain providers, cache safely, invalidate on rotation, and guarantee secrets never enter logs, metrics, traces, or exception chains.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!