Skip to content

Audit Trail

Essentials + Compliance

This page covers two related features. The default ITayraAuditLogger (observability-grade structured logging) ships with Essentials. The persistent hash-chained audit trail (durable, queryable, tamper-evident) ships with Compliance - covered in the "Persistent Hash-Chained Audit Trail" section below. See Licensing for the edition comparison.

Tayra provides a built-in audit trail system for GDPR Article 30 compliance. Every encryption, decryption, key creation, key deletion, and crypto-shredding operation is logged as a structured audit event. This gives you a complete record of all processing activities involving personal data encryption keys.

GDPR Article 30 - Records of Processing Activities

Article 30 of the GDPR requires organizations to maintain records of processing activities that involve personal data. Tayra's audit trail satisfies this requirement by recording:

  • What happened (event type)
  • When it happened (timestamp)
  • Which key was involved (key ID)
  • Whose data was affected (subject ID)
  • What entity type was processed (entity type)
  • How many fields were involved (field count)

Setup

The DefaultTayraAuditLogger is registered automatically when you call AddTayra(). It writes structured log entries via ILogger and emits ActivityEvent entries on the current OpenTelemetry activity:

cs
var auditServices = new ServiceCollection();
auditServices.AddLogging();
auditServices.AddTayra(opts => opts.LicenseKey = licenseKey);

// The DefaultTayraAuditLogger is registered automatically by AddTayra().
// It logs structured audit events via ILogger and emits ActivityEvents.
// To replace it with a custom implementation:
auditServices.AddSingleton<ITayraAuditLogger, ConsoleAuditLogger>();
anchor

To disable audit logging, register the NullTayraAuditLogger:

csharp
services.AddSingleton<ITayraAuditLogger>(
    Tayra.Audit.NullTayraAuditLogger.Instance);

Custom Audit Logger

Implement the ITayraAuditLogger interface to send audit events to your compliance system:

cs
/// <summary>
/// Custom audit logger that writes events to the console.
/// In production, you might write to a database, message queue,
/// or external audit system (e.g., Splunk, Datadog, Azure Monitor).
/// </summary>
public class ConsoleAuditLogger : ITayraAuditLogger
{
    public void LogEvent(TayraAuditEvent auditEvent)
    {
        Console.WriteLine(
            $"  [AUDIT] {auditEvent.Timestamp:O} {auditEvent.EventType} " +
            $"KeyId={auditEvent.KeyId} SubjectId={auditEvent.SubjectId} " +
            $"EntityType={auditEvent.EntityType} FieldCount={auditEvent.FieldCount}");
    }
}
anchor

The ITayraAuditLogger interface has a single synchronous method:

csharp
public interface ITayraAuditLogger
{
    void LogEvent(TayraAuditEvent auditEvent);
}

Synchronous by Design

The LogEvent method is synchronous to avoid adding async overhead to every encryption operation. Implementations should be fast and non-blocking. If you need to write to a database or external service, buffer events in memory and flush asynchronously (e.g., using a Channel<T> or background service).

Audit Event Model

The TayraAuditEvent record contains all the context for a single audit entry:

cs
// TayraAuditEvent is a record with the following properties:
var exampleEvent = new TayraAuditEvent
{
    EventType = TayraAuditEventType.DataEncrypted,
    Timestamp = DateTimeOffset.UtcNow,         // Auto-set to UtcNow
    KeyId = "patient-abc123",                   // The encryption key involved
    SubjectId = "abc123",                       // The data subject identifier
    EntityType = "PatientRecord",               // The .NET type being processed
    FieldCount = 3,                             // Number of PII fields encrypted
    Details = "Encrypted during save",          // Free-text details
};

Console.WriteLine($"  EventType: {exampleEvent.EventType}");
Console.WriteLine($"  Timestamp: {exampleEvent.Timestamp}");
Console.WriteLine($"  KeyId: {exampleEvent.KeyId}");
Console.WriteLine($"  SubjectId: {exampleEvent.SubjectId}");
Console.WriteLine($"  EntityType: {exampleEvent.EntityType}");
Console.WriteLine($"  FieldCount: {exampleEvent.FieldCount}");
Console.WriteLine($"  Details: {exampleEvent.Details}");
anchor

Properties

PropertyTypeDescription
EventTypeTayraAuditEventTypeThe type of operation that occurred (required)
TimestampDateTimeOffsetWhen the event occurred (defaults to UtcNow)
KeyIdstring?The encryption key ID involved, if applicable
SubjectIdstring?The data subject identifier, if applicable
EntityTypestring?The .NET type name of the entity being processed
FieldCountint?The number of PII fields encrypted or decrypted
Detailsstring?Free-text additional details

Event Types

Tayra emits 16 distinct audit event types covering the full lifecycle of data protection operations:

cs
// All audit event types emitted by Tayra:
var allEventTypes = Enum.GetValues<TayraAuditEventType>();
foreach (var eventType in allEventTypes)
{
    Console.WriteLine($"  {eventType}");
}
// Output:
//   KeyCreated
//   KeyAccessed
//   KeyDeleted
//   BulkKeysDeleted
//   DataEncrypted
//   DataDecrypted
//   CryptoShreddingDetected
//   KeyExpired
//   DataSubjectAccessExported
//   DataSubjectPortableExported
//   BreachAssessed
//   BreachNotificationGenerated
//   DataMigrationCompleted
//   DataMigrationVerified
anchor

Complete Event Type Reference

Event TypeEmitted ByDescription
KeyCreatedDefaultCryptoEngine.GetOrCreateKeyAsync, RotateKeyAsyncA new encryption key was generated and stored
KeyAccessedDefaultCryptoEngine.GetOrCreateKeyAsync, GetKeyAsyncAn encryption key was retrieved (from cache or store)
KeyDeletedDefaultCryptoEngine.DeleteKeyAsyncA single encryption key was deleted (crypto-shredding)
BulkKeysDeletedDefaultCryptoEngine.DeleteAllKeysAsyncAll keys matching a prefix were deleted
DataEncryptedDefaultFieldEncrypter.EncryptAsyncPII fields on an entity were encrypted
DataDecryptedDefaultFieldEncrypter.DecryptAsyncPII fields on an entity were decrypted
CryptoShreddingDetectedDefaultFieldEncrypter (during decrypt)Decryption was attempted but the key was already shredded
KeyExpiredKeyRetentionBackgroundServiceA key was deleted by the retention policy
DataSubjectAccessExportedDefaultDataSubjectAccessServiceA GDPR Art. 15 data subject access report was generated
DataSubjectPortableExportedDefaultDataSubjectAccessServiceA GDPR Art. 20 data portability export was generated
BreachAssessedDefaultBreachNotificationServiceA data breach impact assessment was performed
BreachNotificationGeneratedDefaultBreachNotificationServiceA breach notification report was generated
DataMigrationCompletedData migration serviceA data migration batch was completed
DataMigrationVerifiedData migration serviceA data migration was verified
BlindIndexRecomputedBlind index recompute services (Marten, EF Core)Blind index companion columns were recomputed after an HMAC key rotation or transform change
IntegrityCheckFailedDefaultFieldEncrypter (during decrypt)Decryption failed authentication - the ciphertext was tampered with or its associated-data context does not match. Also logged at Warning level; the field value is left unchanged

Default Logger Behavior

The DefaultTayraAuditLogger performs two actions for each event:

1. Structured Logging via ILogger

Events are logged at Information level with structured properties:

Tayra audit: DataEncrypted KeyId=patient-abc123 SubjectId=abc123 EntityType=PatientRecord FieldCount=3 Details=null

This integrates with any ILogger provider (Console, Serilog, Application Insights, etc.) and supports structured log queries.

2. OpenTelemetry ActivityEvents

If there is a current Activity (distributed trace), the logger adds an ActivityEvent named "tayra.audit" with tags:

TagDescription
tayra.audit.event_typeThe event type as a string
tayra.audit.key_idThe key ID (if present)
tayra.audit.subject_idThe subject ID (if present)
tayra.audit.entity_typeThe entity type (if present)
tayra.audit.field_countThe field count (if present)

These appear as span events in your distributed tracing backend (Jaeger, Zipkin, Grafana Tempo), giving you a unified view of audit events within request traces.

Compliance Storage

The default logger writes to ILogger, which is typically ephemeral (console, rolling files). For GDPR compliance, either configure a persistent log sink (database, immutable object store, Splunk, Azure Monitor) or - much better - switch to the persistent hash-chained audit trail described below, which Tayra ships out of the box with the Compliance edition.

Persistent Hash-Chained Audit Trail (Compliance edition)

The Compliance edition adds a durable, queryable, tamper-evident audit trail backed by an IAuditTrailStore. Every event flows through a hash chain so an auditor can later prove the trail has not been modified since events were recorded.

What's different from the default logger

Default DefaultTayraAuditLogger (Essentials)PersistedAuditLogger (Compliance)
StorageILogger + OpenTelemetry breadcrumbsDurable IAuditTrailStore
Queryable per-subjectNo (depends on log backend)Yes, first-class QueryAsync API
Tamper-evidentNoYes, hash chain detects modification, deletion, reorder, or fabrication
Integrity verificationn/aVerifyIntegrityAsync walks the chain and reports the first failure
Cross-process retentionWhatever your log backend retainsPersisted alongside Marten documents (or your own IAuditTrailStore)

Hash chain construction

Each AuditTrailEntry records:

  • SequenceNumber - monotonic, gap-detectable
  • Event - the TayraAuditEvent itself
  • PreviousHash - hex SHA-256 of the previous entry (empty for the genesis entry)
  • EntryHash - hex SHA-256 of (sequence || canonical-event-json || previous-hash), length-prefixed to defeat concatenation collisions

Modifying any committed event invalidates the corresponding EntryHash and every subsequent hash. Reordering breaks the SequenceNumber invariant. Deletion creates a detectable gap. Fabricated entries break the hash chain.

Wiring the in-memory store (tests, demos)

csharp
services.AddTayra(opts => opts.LicenseKey = key);
services.AddTayraInMemoryAuditTrail();

The chain lives only as long as the host process - perfect for unit tests and one-off demos.

Wiring the Marten-backed store (production)

csharp
services.AddMarten(opts =>
{
    opts.Connection(connectionString);
    opts.AutoCreateSchemaObjects = AutoCreate.All;
}).UseLightweightSessions();

services.AddTayraMartenAuditTrail();

Each appended event is persisted as a MartenAuditTrailDocument in PostgreSQL with full Marten LINQ + indexing support. Append operations serialize through a per-store SemaphoreSlim so sequence numbers and the chain stay consistent under concurrent writers within a process. Production deployments should either run a single audit-writer instance per Marten store or layer Wolverine outbox / message-queue ordering on top of AppendAsync for safe multi-writer support.

Querying the trail

csharp
var store = serviceProvider.GetRequiredService<IAuditTrailStore>();

var alice = await store.QueryAsync(new AuditTrailQuery
{
    SubjectId = "cust-42",
    From = DateTimeOffset.UtcNow.AddDays(-365),
    Take = 1000,
});

Filter by SubjectId, KeyId, EventType, time window, plus Skip / Take pagination. Results are ordered by SequenceNumber ascending.

Verifying integrity

Run VerifyIntegrityAsync periodically (CI gate, monthly compliance task, ad hoc on regulator request):

csharp
var result = await store.VerifyIntegrityAsync();
if (!result.IsValid)
{
    logger.LogCritical(
        "Audit trail tampered: failed at sequence {Seq} ({Reason})",
        result.FailedAtSequenceNumber, result.Reason);
}

A clean result returns IsValid = true and the entry count. A failure returns the first sequence where the chain breaks plus a human-readable reason.

Custom backends

Implement IAuditTrailStore to back the trail with anything (EF Core, plain Postgres, S3 + WORM bucket, etc). Reuse AuditChainHasher.ComputeEntryHash so your store hashes events the same way as the in-memory and Marten backends - chains then verify correctly across stores in mixed deployments.

See Also