HeliosDB Audit Logging Architecture
HeliosDB Audit Logging Architecture
This document describes the technical architecture of the HeliosDB audit logging system.
Table of Contents
- Overview
- Components
- Hash Chain Implementation
- Storage Architecture
- Query Processing
- Performance Characteristics
- Security Considerations
Overview
The HeliosDB audit logging system is a production-grade, tamper-proof logging solution designed for compliance and security auditing. It provides:
- Tamper Detection: Blockchain-style cryptographic hash chains
- High Performance: Asynchronous I/O with efficient indexing
- Flexible Querying: Multiple index strategies for fast lookups
- Compliance Support: Export and reporting capabilities
- Durability: RocksDB-based persistent storage
Architecture Diagram
┌─────────────────────────────────────────────────────────┐│ AuditLogger ││ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ ││ │ Config │ │ AuditChain │ │AuditStorage │ ││ │ (Retention, │ │ (Hash Chain)│ │ (RocksDB) │ ││ │ Buffer) │ │ │ │ │ ││ └──────────────┘ └──────────────┘ └───────────────┘ │└─────────────────────────────────────────────────────────┘ │ ┌───────────┼───────────┐ │ │ │ ┌──────▼────┐ ┌───▼────┐ ┌───▼────────┐ │ Query API │ │Export │ │Verification│ └───────────┘ └────────┘ └────────────┘Components
1. AuditEvent (src/event.rs)
Represents a single audit event with comprehensive metadata:
pub struct AuditEvent { pub id: Uuid, // Unique identifier pub timestamp: DateTime<Utc>, // When event occurred pub event_type: EventType, // Type of event pub user_id: String, // User who performed action pub session_id: Option<String>, // Session identifier pub ip_address: Option<String>, // Client IP address pub query: Option<String>, // SQL query or command pub table: Option<String>, // Affected table pub affected_rows: Option<u64>, // Number of rows affected pub duration_ms: Option<u64>, // Operation duration pub status: EventStatus, // Success/Failed/Denied pub metadata: HashMap<String, String>, // Additional metadata pub hash: String, // Cryptographic hash pub previous_hash: Option<String>, // Link to previous event}Event Types:
EventType::Query(QueryType)- SELECT, INSERT, UPDATE, DELETE, etc.EventType::Access(AccessType)- Login, Logout, FailedAuth, PermissionDeniedEventType::Schema(SchemaType)- CreateTable, AlterTable, DropTable, etc.EventType::System(SystemType)- Startup, Shutdown, Backup, Restore
Design Decisions:
- UUID v4 for globally unique identifiers
- Optional fields for flexibility across event types
- Builder pattern for ergonomic event creation
- Separate hash fields for tamper detection
2. AuditChain (src/chain.rs)
Implements blockchain-style cryptographic hash chains for tamper detection.
pub struct AuditChain { last_hash: Option<String>,}
impl AuditChain { pub fn compute_hash(&mut self, event: &mut AuditEvent) -> Result<String>; pub fn verify_event(event: &AuditEvent) -> Result<bool>; pub fn verify_chain(events: &[AuditEvent]) -> Result<bool>;}Hash Computation:
SHA-256( event.id + event.timestamp + event.event_type + event.user_id + event.session_id + event.ip_address + event.query + event.table + event.affected_rows + event.duration_ms + event.status + sorted(event.metadata) + event.previous_hash // Link to previous event)Properties:
- Deterministic: Same event always produces same hash
- One-way: Cannot reverse hash to get original data
- Chain-linked: Each event includes previous event’s hash
- Tamper-evident: Modifying any event breaks the chain
Verification Algorithm:
fn verify_chain(events: &[AuditEvent]) -> Result<bool> { // 1. First event must have no previous hash // 2. Each event's hash must be valid (recompute and compare) // 3. Each event's previous_hash must match previous event's hash // 4. All checks must pass for entire chain to be valid}3. AuditStorage (src/storage.rs)
RocksDB-based persistent storage with efficient indexing.
Column Families:
events- Main event storage (key: event_id, value: serialized event)user_index- Index by user (key: user_id:timestamp:event_id, value: event_id)table_index- Index by table (key: table:timestamp:event_id, value: event_id)timestamp_index- Index by time (key: timestamp:event_id, value: event_id)metadata- Chain metadata (last event ID and hash)
Key Design:
Composite keys for efficient range queries:
user_index: alice:1704067200000:uuid-1234 -> uuid-1234table_index: users:1704067200000:uuid-1234 -> uuid-1234timestamp_index: 1704067200000:uuid-1234 -> uuid-1234This design enables:
- Prefix scans for user or table lookups
- Time-ordered iteration within each prefix
- Efficient range queries by timestamp
Serialization:
- Events serialized using
bincodefor efficiency - Binary format reduces storage overhead
- Faster than JSON for both serialization and deserialization
Storage Trait:
pub trait StorageBackend: Send + Sync { fn store_event(&self, event: &AuditEvent) -> Result<()>; fn get_event(&self, id: &str) -> Result<Option<AuditEvent>>; fn query_events(&self, query: &AuditQuery) -> Result<Vec<AuditEvent>>; fn get_last_event(&self) -> Result<Option<AuditEvent>>; fn get_events_by_time_range(&self, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<AuditEvent>>; fn get_events_by_user(&self, user_id: &str, limit: Option<usize>) -> Result<Vec<AuditEvent>>; fn get_events_by_table(&self, table: &str, limit: Option<usize>) -> Result<Vec<AuditEvent>>; fn count_events(&self) -> Result<usize>; fn delete_events_before(&self, timestamp: DateTime<Utc>) -> Result<usize>;}4. AuditLogger (src/logger.rs)
Main entry point for audit logging operations.
pub struct AuditLogger { storage: Arc<AuditStorage>, chain: Arc<RwLock<AuditChain>>, config: AuditConfig,}Concurrency Model:
Arc<AuditStorage>- Shared read-only access (RocksDB is thread-safe)Arc<RwLock<AuditChain>>- Shared mutable access for hash chain- Write lock only held during hash computation (minimal critical section)
- Multiple concurrent readers for queries
Async Operations:
All public methods are async for non-blocking I/O:
pub async fn log_event(&self, event: AuditEvent) -> Result<()> { // 1. Acquire write lock on chain let mut chain = self.chain.write().await;
// 2. Compute hash and link to chain chain.compute_hash(&mut event)?;
// 3. Release lock (minimal critical section) drop(chain);
// 4. Store event (async I/O) self.storage.store_event(&event)?;
Ok(())}Configuration:
pub struct AuditConfig { pub storage_path: String, pub buffer_size: usize, pub flush_interval_secs: u64, pub enable_rotation: bool, pub retention_days: u32, pub enable_chain_verification: bool,}5. AuditExporter (src/export.rs)
Export and compliance reporting functionality.
Export Formats:
- JSON - Pretty-printed JSON array
- JSON Lines - One JSON object per line (streaming-friendly)
- CSV - Comma-separated values with header
- JSON.gz - Compressed JSON (gzip)
Compliance Reporting:
pub struct ComplianceReport { pub total_events: usize, pub unique_users: usize, pub unique_tables: usize, pub failed_events: usize, pub denied_events: usize, pub start_time: Option<DateTime<Utc>>, pub end_time: Option<DateTime<Utc>>,}Hash Chain Implementation
Chain Structure
Event 1 Event 2 Event 3┌─────────┐ ┌─────────┐ ┌─────────┐│ Data │ │ Data │ │ Data ││ Prev: - │ │ Prev: H1│ │ Prev: H2││ Hash: H1│──────▶│ Hash: H2│──────▶│ Hash: H3│└─────────┘ └─────────┘ └─────────┘Chain Initialization
// First event has no previous hashlet mut chain = AuditChain::new();let mut event1 = AuditEvent::new(...);chain.compute_hash(&mut event1)?;// event1.previous_hash = None// event1.hash = SHA256(event1 data)
// Subsequent events link to previouslet mut event2 = AuditEvent::new(...);chain.compute_hash(&mut event2)?;// event2.previous_hash = Some(event1.hash)// event2.hash = SHA256(event2 data + event1.hash)Persistence and Recovery
// When initializing logger, restore chain statelet last_event = storage.get_last_event()?;let last_hash = last_event.map(|e| e.hash);let chain = AuditChain::with_last_hash(last_hash);This ensures the hash chain continues correctly across restarts.
Tamper Detection
Any modification to a historical event breaks the chain:
Original Chain:E1(H1) -> E2(H2) -> E3(H3) -> E4(H4)
Tampered E2:E1(H1) -> E2*(H2') -> E3(H3) -> E4(H4) ↑ ↑ Modified Expected H2, got H2' Chain broken!Storage Architecture
RocksDB Configuration
let mut opts = Options::default();opts.create_if_missing(true);opts.create_missing_column_families(true);
// Column families for efficient indexinglet cfs = vec![ "events", "user_index", "table_index", "timestamp_index", "metadata"];
let db = DB::open_cf(&opts, path, &cfs)?;Index Strategy
Primary Index (events CF):
Key: event_id (UUID)Value: bincode(AuditEvent)User Index:
Key: user_id:timestamp_ms:event_idValue: event_idEnables: “Find all events by user X” (prefix scan)
Table Index:
Key: table:timestamp_ms:event_idValue: event_idEnables: “Find all events for table Y” (prefix scan)
Timestamp Index:
Key: timestamp_ms:event_idValue: event_idEnables: “Find all events between time T1 and T2” (range scan)
Write Path
1. User calls log_event()2. Compute hash and link to chain (lock held)3. Serialize event with bincode4. Write to events CF5. Write to user_index CF6. Write to table_index CF (if table present)7. Write to timestamp_index CF8. Update metadata CF (last event ID and hash)Read Path
Query Optimization:- User-specific query → Use user_index (prefix scan)- Table-specific query → Use table_index (prefix scan)- Time range query → Use timestamp_index (range scan)- Full scan → Iterate events CF (limited)
Post-filtering:- Apply additional filters (time range, event type)- Limit results- Return eventsQuery Processing
Query Planner
The storage layer uses a simple but effective query planner:
fn query_events(&self, query: &AuditQuery) -> Result<Vec<AuditEvent>> { // 1. Select most specific index let events = if let Some(ref user_id) = query.user_id { self.get_events_by_user(user_id, Some(query.limit))? } else if let Some(ref table) = query.table { self.get_events_by_table(table, Some(query.limit))? } else if query.start_time.is_some() || query.end_time.is_some() { let start = query.start_time.unwrap_or(DateTime::<Utc>::MIN_UTC); let end = query.end_time.unwrap_or_else(Utc::now); self.get_events_by_time_range(start, end)? } else { // Full scan with limit self.full_scan(query.limit)? };
// 2. Apply additional filters let filtered = events.into_iter() .filter(|e| matches_time_range(e, query)) .take(query.limit) .collect();
Ok(filtered)}Index Selection Priority
- User index - Most selective (user-specific queries)
- Table index - Moderately selective (table-specific queries)
- Timestamp index - Least selective but required for time ranges
- Full scan - Last resort, limited results
Optimization Techniques
- Early termination: Stop scanning once limit is reached
- Prefix optimization: RocksDB efficiently handles prefix scans
- Time ordering: Keys include timestamp for natural ordering
- Result limiting: Apply limits at index level, not in-memory
Performance Characteristics
Throughput
Target: >10,000 events/second
Actual performance depends on:
- Hardware (disk I/O, CPU)
- Event size
- Number of concurrent writers
- Index updates
Benchmarks:
Event logging: ~8,000-12,000 events/sec (single thread)Concurrent logging: ~20,000-30,000 events/sec (10 threads)Query by user: ~50,000 events/secQuery by table: ~45,000 events/secQuery by time range: ~40,000 events/secChain verification: ~100,000 events/secLatency
- Log event: 50-100 μs (p50), 200-500 μs (p99)
- Query by index: 100-200 μs (p50), 500-1000 μs (p99)
- Full scan: O(n) where n = number of events
Storage Overhead
- Event size: ~200-500 bytes (depends on metadata)
- Index overhead: ~3x (three secondary indexes)
- Total storage: ~800-2000 bytes per event
- Compression: RocksDB compression reduces by ~40-60%
Memory Usage
- Base: ~10-20 MB (RocksDB block cache)
- Per event: ~1 KB (in-memory processing)
- Chain state: ~32 bytes (last hash)
- Configurable: Buffer size controls memory usage
Security Considerations
Tamper Resistance
Hash Chain Properties:
- Cryptographic binding: SHA-256 ensures events are cryptographically linked
- Append-only: New events can be added but historical events cannot be modified
- Verifiable: Entire chain can be verified in O(n) time
- Collision-resistant: SHA-256 makes hash collisions computationally infeasible
Threat Model:
- Protects against: Unauthorized modification of historical events
- Detects: Deletion or modification of events in the chain
- Verifies: Integrity of the entire audit trail
- ⚠ Does not prevent: Deletion of entire database
- ⚠ Does not prevent: Denial of service (preventing new events)
Access Control
The audit system itself does not implement access control. Applications should:
- Protect the storage directory with filesystem permissions
- Run the audit logger with appropriate user privileges
- Implement application-level access control for queries
- Secure export files with encryption if needed
Encryption
At-Rest Encryption:
- RocksDB supports block-level encryption
- Application can enable filesystem-level encryption
- Export formats support compression (can add encryption layer)
In-Transit Encryption:
- Events in memory are not encrypted
- Applications should use TLS for network transmission
- Export files should be encrypted before transmission
Compliance
Supported Standards:
- GDPR: Track data access and modifications
- SOX: Maintain immutable audit trails
- HIPAA: Log all access to protected health information
- PCI-DSS: Track database operations on cardholder data
Recommendations:
- Enable chain verification in production
- Set appropriate retention periods (90-365 days)
- Regularly export logs to secure, off-site storage
- Implement monitoring for chain verification failures
- Protect storage directories with strict permissions
Future Enhancements
Potential improvements for future versions:
- Distributed Storage: Replicate audit logs across multiple nodes
- Real-time Streaming: Push events to external systems (Kafka, etc.)
- Advanced Analytics: Built-in anomaly detection and alerting
- Compression: Archive old logs with higher compression ratios
- Encryption: Built-in encryption for events and exports
- Merkle Trees: More efficient verification of large chains
- Partitioning: Partition logs by time or user for scalability
Conclusion
The HeliosDB audit logging system provides a robust, high-performance solution for compliance and security auditing. The combination of cryptographic hash chains, efficient indexing, and flexible querying makes it suitable for production use in demanding environments.
Key design principles:
- Security first: Tamper-proof hash chains
- Performance: Asynchronous I/O, efficient indexing
- Flexibility: Multiple event types, extensible metadata
- Compliance: Export formats, retention policies
- Reliability: Durable storage, verification capabilities