Skip to content

Key Retention

Tayra includes a background service that automatically enforces data retention policies by shredding encryption keys past their configured time-to-live (TTL). This helps you comply with GDPR Article 5(1)(e), which requires that personal data is kept no longer than necessary.

How It Works

The KeyRetentionBackgroundService runs as a .NET BackgroundService and periodically:

  1. Queries the key store for keys created before the earliest possible expiry cutoff.
  2. Evaluates each candidate key against the IKeyRetentionPolicy to determine if it has expired.
  3. Deletes expired keys, effectively crypto-shredding all data encrypted with those keys.
  4. Emits KeyExpired audit events for each deleted key.

Setup

Register the key retention service with AddTayraKeyRetention:

cs
// Register the key retention background service.
// It periodically scans for and deletes expired encryption keys.
var retentionServices = new ServiceCollection();
retentionServices.AddTayra(opts => opts.LicenseKey = licenseKey);
retentionServices.AddTayraKeyRetention(options =>
{
    options.DefaultRetentionPeriod = TimeSpan.FromDays(365);
    options.ScanInterval = TimeSpan.FromHours(1);
    options.BatchSize = 100;
    options.PrefixRetentionPeriods = new Dictionary<string, TimeSpan>
    {
        ["temp-"] = TimeSpan.FromDays(30),
        ["session-"] = TimeSpan.FromHours(24),
    };
});
anchor

This registers:

  • IKeyRetentionPolicy (default implementation: DefaultKeyRetentionPolicy)
  • KeyRetentionBackgroundService as a hosted service

INFO

The background service requires a host to run (e.g., ASP.NET Core, .NET Generic Host). It will not execute in a simple console application without a host.

Configuration Options

The KeyRetentionOptions class controls all aspects of the retention service:

cs
// KeyRetentionOptions properties:
var retentionOptions = new KeyRetentionOptions
{
    // Default retention period for keys that don't match any prefix rule.
    // Null means indefinite retention (no auto-expiry).
    DefaultRetentionPeriod = TimeSpan.FromDays(365),

    // How often the background service scans for expired keys. Default: 1 hour.
    ScanInterval = TimeSpan.FromHours(1),

    // Maximum number of keys to delete per scan cycle. Default: 100.
    BatchSize = 100,

    // Prefix-specific retention periods.
    // Keys whose IDs start with a matching prefix use the corresponding period.
    PrefixRetentionPeriods = new Dictionary<string, TimeSpan>
    {
        ["temp-"] = TimeSpan.FromDays(30),      // Temporary data: 30 days
        ["session-"] = TimeSpan.FromHours(24),   // Session data: 24 hours
        ["patient-"] = TimeSpan.FromDays(3650),  // Medical data: 10 years
    },
};
Console.WriteLine($"Default retention: {retentionOptions.DefaultRetentionPeriod}");
Console.WriteLine($"Scan interval: {retentionOptions.ScanInterval}");
Console.WriteLine($"Batch size: {retentionOptions.BatchSize}");
anchor

Options Reference

PropertyTypeDefaultDescription
DefaultRetentionPeriodTimeSpan?nullDefault TTL for keys that don't match any prefix rule. Null means indefinite retention.
ScanIntervalTimeSpan1 hourHow often the background service scans for expired keys.
BatchSizeint100Maximum number of keys to delete per scan cycle.
PrefixRetentionPeriodsDictionary<string, TimeSpan>EmptyPrefix-specific TTLs. Keys matching a prefix use that TTL instead of the default.

Prefix-Specific TTLs

Different data categories often have different retention requirements. Use PrefixRetentionPeriods to set TTLs per key prefix:

csharp
options.PrefixRetentionPeriods = new Dictionary<string, TimeSpan>
{
    ["temp-"] = TimeSpan.FromDays(30),        // Temporary data: 30 days
    ["session-"] = TimeSpan.FromHours(24),     // Session tokens: 24 hours
    ["patient-"] = TimeSpan.FromDays(3650),    // Medical records: 10 years
    ["cust-"] = TimeSpan.FromDays(730),        // Customer data: 2 years
};

When multiple prefixes match a key ID, the longest matching prefix wins. For example, a key ID of "cust-premium-001" would match "cust-" but also "cust-premium-" if both are configured — the longer prefix takes precedence.

WARNING

Key retention is irreversible. Once a key is deleted by the retention service, all data encrypted with that key becomes permanently unreadable. There is no undo. Test your retention configuration thoroughly in a staging environment before deploying to production.

Key Store Requirements

The retention service requires the key store to support the GetKeysCreatedBeforeAsync method, which queries keys by creation timestamp. Not all key stores support this:

Key StoreSupported
PostgreSQLYes
In-MemoryFor testing only
HashiCorp VaultNo (metadata limitations)
Azure Key VaultNo (metadata limitations)
AWS KMSNo (metadata limitations)

If the key store does not support creation-time queries, the retention service will log a warning and stop gracefully.

Custom Retention Policy

You can implement IKeyRetentionPolicy to create custom retention logic beyond prefix matching:

csharp
public class CustomRetentionPolicy : IKeyRetentionPolicy
{
    public TimeSpan? GetRetentionPeriod(string keyId)
    {
        // Custom logic based on key ID patterns, external rules, etc.
        if (keyId.Contains(":session:"))
            return TimeSpan.FromHours(1);

        return TimeSpan.FromDays(365); // Default: 1 year
    }
}

Register it before calling AddTayraKeyRetention to replace the default:

csharp
services.AddTayra(opts => opts.LicenseKey = licenseKey)
    .UsePostgreSqlKeyStore(connectionString);
services.AddSingleton<IKeyRetentionPolicy, CustomRetentionPolicy>();
services.AddTayraKeyRetention();

See Also