Edit

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:

  1. CLI attempts to read the key file at the expected path.

  2. If the file exists and contains exactly 32 bytes, the key is loaded.

  3. If the file does not exist, the CLI:

    • Creates the parent directory (audit/) with 0700 permissions.

    • Generates 32 cryptographically random bytes via crypto/rand.

    • Writes the key file with 0600 permissions.

  4. 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:

  1. OPENCENTER_STATE_DIR environment variable

  2. CLI config paths.stateDir

  3. ${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

"failed to load audit signing key"

The CLI cannot read or create the key file. Check directory permissions:

ls -la ~/.config/opencenter/audit/
# Expected: drwx------ (0700) for directory
# Expected: -rw------- (0600) for audit.key

Verifying log integrity after key loss

Not possible. Without the original 32-byte key, HMAC signatures cannot be recomputed. If the key is regenerated, only events written after regeneration will have verifiable signatures.

  • ../concepts/security-model.md[Security Model] — Defense-in-depth architecture

  • file-locations.md[File Locations] — All CLI file paths

  • environment-variables.md[Environment Variables] — OPENCENTER_CONFIG_DIR, OPENCENTER_STATE_DIR