Skip to content

Security: The-Tech-Idea/Beep.StreamingEvents

Security

Docs/Security.md

help_file encryption-security.html

Beep Streaming — Security Guide

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).


1. Encryption Architecture

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.

2. IEncryptionProvider

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.


3. IKeyStore

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);
}

3.1 FileKeyStore

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);

3.2 Custom Key Store (e.g. Azure Key Vault)

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*')
        ...
    }
}

4. Wire-Up

// 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.


5. OIDC Token Validation

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: OidcTokenValidator previously accepted tokens with an expired nbf claim. Fixed: ClockSkew = TimeSpan.Zero is now enforced.


6. Key Rotation Workflow

  1. Call IKeyStore.StoreNewKeyAsync with a freshly generated 256-bit key.
  2. The provider picks up the new version on the next call to EncryptAsync.
  3. Old ciphertext is not re-encrypted immediately — the version prefix ensures the correct historic key is retrieved during DecryptAsync.
  4. 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}");

6.1 Re-Encryption

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.


7. Access-Control Summary

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

There aren't any published security advisories