| help_file | encryption-security.html |
|---|
Authoritative source for the AES-256-GCM encryption pipeline, key-store implementations, OIDC token validation, and key rotation workflow. Updated to reflect Sprint-01 security fixes (SEC-1 / SPRINT01-SEC).
Beep Streaming encrypts event payloads at rest using AES-256-GCM (authenticated encryption). Each stored message carries:
[version : 4 bytes][nonce : 12 bytes][auth-tag : 16 bytes][ciphertext : variable]
- Version — big-endian uint32; identifies the active key so older ciphertext can still be decrypted after a key rotation.
- Nonce — randomly generated per message (never reused).
- Auth-tag — GCM MAC; detected tampering causes decryption to throw
CryptographicException.
public interface IEncryptionProvider
{
/// Encrypts plaintext bytes; returns version-prefixed ciphertext.
Task<byte[]> EncryptAsync(byte[] plaintext, CancellationToken ct = default);
/// Decrypts version-prefixed ciphertext; reads version prefix to select the
/// correct key version automatically.
Task<byte[]> DecryptAsync(byte[] ciphertext, CancellationToken ct = default);
/// Current key version (incremented after each RotateKeyAsync call).
int CurrentKeyVersion { get; }
}Default implementation: AesGcmEncryptionProvider in Security/AesGcmEncryptionProvider.cs.
public interface IKeyStore
{
/// Retrieve the 256-bit key bytes for the given version.
Task<byte[]> GetKeyAsync(int version, CancellationToken ct = default);
/// Write a new 256-bit key under the next sequential version number.
Task<int> StoreNewKeyAsync(byte[] keyBytes, CancellationToken ct = default);
/// Returns the highest stored key version.
Task<int> GetCurrentVersionAsync(CancellationToken ct = default);
}Development / single-node key store; stores keys as AES-256 blobs on the local file system. Not suitable for production.
var keyStore = new FileKeyStore(keyDirectory: "/var/beep/keys");
var provider = new AesGcmEncryptionProvider(keyStore);public class KeyVaultKeyStore : IKeyStore
{
private readonly SecretClient _client;
public KeyVaultKeyStore(SecretClient client) => _client = client;
public async Task<byte[]> GetKeyAsync(int version, CancellationToken ct)
{
var secret = await _client.GetSecretAsync($"beep-key-v{version}", cancellationToken: ct);
return Convert.FromBase64String(secret.Value.Value);
}
public async Task<int> StoreNewKeyAsync(byte[] keyBytes, CancellationToken ct)
{
int next = await GetCurrentVersionAsync(ct) + 1;
await _client.SetSecretAsync($"beep-key-v{next}", Convert.ToBase64String(keyBytes), ct);
return next;
}
public async Task<int> GetCurrentVersionAsync(CancellationToken ct)
{
// Resolve highest version stored in Key Vault
// (implementation enumerates secrets matching 'beep-key-v*')
...
}
}// Register via the builder
var node = new BeepStreamingCluster.Builder()
.WithEncryption(sp =>
{
var keyStore = sp.GetRequiredService<IKeyStore>();
return new AesGcmEncryptionProvider(keyStore);
})
.Build();The encryption provider is invoked by StorageInterceptor before bytes are
written to the WAL and after bytes are read back.
Incoming gRPC and HTTP API calls present a JWT bearer token. The validator is
configured via ITokenValidator:
public interface ITokenValidator
{
Task<ClaimsPrincipal> ValidateTokenAsync(string bearerToken, CancellationToken ct = default);
}Register with the cluster builder:
.WithTokenValidation(sp =>
{
var opts = sp.GetRequiredService<IOptions<OidcOptions>>().Value;
return new OidcTokenValidator(
authority: opts.Authority, // https://login.microsoftonline.com/{tenant}
audience: opts.Audience, // api://beep-streaming
issuer: opts.Issuer
);
})Sprint-01 fix:
OidcTokenValidatorpreviously accepted tokens with an expirednbfclaim. Fixed:ClockSkew = TimeSpan.Zerois now enforced.
- Call
IKeyStore.StoreNewKeyAsyncwith a freshly generated 256-bit key. - The provider picks up the new version on the next call to
EncryptAsync. - Old ciphertext is not re-encrypted immediately — the version prefix
ensures the correct historic key is retrieved during
DecryptAsync. - Schedule background re-encryption of old messages at a maintenance window
using the built-in
KeyRotationService.
// Trigger a manual rotation
await keyRotationService.RotateNowAsync(cancellationToken);
Console.WriteLine($"Active version is now {provider.CurrentKeyVersion}");After rotation, older messages on the WAL still carry the old version prefix.
KeyRotationService iterates topics in batches and re-writes each segment
with the new version. Progress is checkpointed so rotation can be resumed
after a restart.
| Layer | Mechanism | Configuration Key |
|---|---|---|
| gRPC streaming | JWT bearer (ITokenValidator) | security.oidc.authority |
| HTTP Admin API | JWT bearer (ITokenValidator) | security.oidc.authority |
| Tenant scoping | TenantIsolationInterceptor | security.enforce.tenant |
| Data at rest | AES-256-GCM (IEncryptionProvider) | security.encryption.enabled |
| Key storage | IKeyStore implementation | security.keystore.path |