Purpose: For operators and security engineers, documents the HMAC signing key that protects CLI audit log integrity, covering generation, storage, usage, and verification.
Overview
Every openCenter CLI installation maintains a local HMAC-SHA256 signing key. The CLI uses this key to sign each audit log entry at write time and to verify signatures when checking log integrity. If a log entry is modified after it was written, the signature check fails.
The key protects against post-hoc tampering of the audit trail. It does not encrypt anything and has no relationship to SOPS Age keys or SSH keys.
Key Details
| Property | Value |
| --- | --- |
| Algorithm | HMAC-SHA256 |
| Key size | 32 bytes (256 bits) |
| Format | Raw binary |
| File permissions | 0600 |
| Directory permissions | 0700 |
| Generation | crypto/rand (OS CSPRNG) |
File Location
Default path:
~/.config/opencenter/audit/audit.key
The path is derived from the CLI config directory:
<OPENCENTER_CONFIG_DIR>/audit/audit.key
If OPENCENTER_CONFIG_DIR is not set, the config directory defaults to ~/.config/opencenter.
Evidence: internal/security/audit_logger.go — GetDefaultAuditSigningKeyPath(), loadOrCreateSigningKey()
Generation
The key is created lazily on first audit log write. There is no separate generate command. The process:
-
CLI attempts to read the key file at the expected path.
-
If the file exists and contains exactly 32 bytes, the key is loaded.
-
If the file does not exist, the CLI:
-
Creates the parent directory (
audit/) with0700permissions. -
Generates 32 cryptographically random bytes via
crypto/rand. -
Writes the key file with
0600permissions.
-
-
If the file exists but has an unexpected length, the CLI returns an error.
This means the key is generated during whichever CLI operation first triggers audit logging — typically cluster init.
Evidence: internal/security/audit_logger.go — loadOrCreateSigningKey()
How Signing Works
Each AuditEvent is signed before it is written to the log. The signed payload is a pipe-delimited concatenation of six fields:
<timestamp>|<event_type>|<actor>|<resource>|<action>|<result>
The CLI computes HMAC-SHA256(signing_key, payload) and stores the hex-encoded result in the event’s signature field. The complete event (including signature) is then serialized as a single JSON line in the audit log.
Evidence: internal/security/audit_logger.go — signEvent()
Signed Event Types
The audit logger signs every event it records. Event types include:
| Event Type | Trigger |
| --- | --- |
| key_generated | Age or SSH key creation |
| key_accessed | Key file read (success or failure) |
| key_rotated | Age or SSH key rotation |
| key_revoked | Key revocation |
| key_expired | Key expiration detected |
| secret_decrypted | SOPS decryption operation |
| secrets_sync | Secrets synchronization |
| secrets_sync_failed | Secrets sync failure |
| secrets_validated | Secrets validation pass |
| drift_detected | Configuration drift found |
| validation_failed | Config validation failure |
| input_rejected | Malicious input blocked |
| template_validation_failed | Template validation failure |
Evidence: internal/security/audit_logger.go — Log*() methods
Integrity Verification
VerifyIntegrity() reads the audit log line by line, deserializes each JSON event, recomputes the HMAC, and compares it to the stored signature. Any mismatch is reported with the event ID and line number.
# Conceptual flow (no standalone CLI command today)
for each line in audit.log:
event = JSON.parse(line)
expected = HMAC-SHA256(signing_key, event fields)
if event.signature != expected:
report "integrity check failed for event <id> at line <n>"
A single invalid signature causes the verification to return an error with the count of tampered entries.
Evidence: internal/security/audit_logger.go — VerifyIntegrity(), verifySignature()
Audit Log Location
The audit log itself is stored separately from the key:
~/.local/state/opencenter/audit/audit.log
The log path follows the state directory precedence:
-
OPENCENTER_STATE_DIRenvironment variable -
CLI config
paths.stateDir -
${XDG_STATE_HOME:-~/.local/state}/opencenter
Evidence: internal/security/audit_logger.go — GetDefaultAuditLogPath()
Log Rotation
| Setting | Value |
| --- | --- |
| Max file size | 100 MB |
| Retention | 30 days |
| Rotated file pattern | audit.log.<timestamp> |
When the log exceeds 100 MB, the current file is renamed with a timestamp suffix and a new file is created. Files older than 30 days are deleted during rotation.
Evidence: internal/security/audit_logger.go — MaxLogSize, LogRetentionDays, rotateLog(), cleanupOldLogs()
Security Considerations
-
The key provides tamper detection, not tamper prevention. An attacker with access to both the key file and the log can re-sign modified entries.
-
The key is local to the machine. It is not shared across hosts and is not committed to any repository.
-
If the key is lost, existing log signatures cannot be verified. The CLI will generate a new key on next use, but old entries become unverifiable.
-
The key file should be included in workstation backups if audit log integrity verification is required for compliance.
Troubleshooting
"unexpected signing key length"
The key file exists but does not contain exactly 32 bytes. This can happen if the file was corrupted or manually edited.
Fix: Delete the key file and let the CLI regenerate it. Note that signatures on existing log entries will no longer verify.
rm ~/.config/opencenter/audit/audit.key
# Next CLI operation that triggers audit logging will create a new key