You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Audit-event signing (#220) loads a static Ed25519 key from FORGE_AUDIT_SIGNING_KEY_B64 (env) or a file, in-process. That is the correct baseline: self-rooted, tamper-evident, zero external dependencies.
For fleet-scale enterprise identity + key custody, hand-registering an OIDC app per agent (inbound) and an M2M app per agent (outbound) does not scale, and Okta has no agent-native construct. The answer is SPIFFE/SPIRE: the agent consumes a short-lived SVID from the local Workload API — one credential for inbound and outbound — issued by an Initializ-managed SPIRE plane whose CA is rooted in the customer's KMS (see initializ/console-next#6). The agent never holds an Okta client or a static secret.
This issue is the forge agent-side work: a Certifier abstraction with a local default and a spiffe backend, plus the epoch record and verifier changes. Scope: Kubernetes (k8s_psat node attestation) only for this iteration.
Proposed control
Certifier interface yielding the current signing identity ({key/handle, kid, cert/epoch, notBefore, notAfter}), rotating before expiry. Two backends:
local (DEFAULT — unchanged). Loads/generates the key from FORGE_AUDIT_SIGNING_KEY_B64 (env), self-rooted, no network. Stays the default so tamper-evident signing has no external dependency; when unconfigured, signing is off and the wire shape is byte-identical to pre-feat(audit): Ed25519 per-event signing + JWKS endpoint #220.
spiffe (managed identity route). Reads the agent's SVID from the SPIFFE Workload API Unix socket (no Okta client, no secret — the local SPIRE Agent workload-attests the process via K8s selectors: ServiceAccount / namespace / pod). The SVID (or an ephemeral key certified by it) becomes the audit-signing epoch key. On each rotation:
emit an audit_key_epoch record carrying the SVID/cert + epoch pubkey, so the log is self-verifying offline,
rotate when the SVID rotates.
Node churn: the SPIFFE ID is workload-scoped, not node-scoped — when the agent is rescheduled to a new node, it re-attests and receives a fresh SVID with the same SPIFFE ID, new key. That simply opens a new epoch under the same identity; the hash chain (#212) continues and verification uses point-in-time epoch validity. No identity loss, no manual re-provisioning, no broken audit trail.
VerifyAuditLog gains: resolve each event's kid -> its audit_key_epoch, verify the epoch against the SPIRE trust bundle (rooted in the customer KMS, published by the platform), and check point-in-time validity (event.ts inside the epoch window — expired epochs still verify historical events).
Acceptance criteria
Certifier interface with local and spiffe backends; selected via audit.signing.mode (default local).
spiffe reads the SVID from the Workload API socket (no Okta client / no static secret), signs locally, and rotates with the SVID — no per-event network call.
Node change produces a new epoch, not a failure: re-scheduling the agent to a new node yields the same SPIFFE ID + a new key; a new audit_key_epoch is emitted and the stream still verifies end-to-end.
VerifyAuditLog verifies events against the epoch + SPIRE trust bundle with point-in-time validity (an expired epoch still verifies events signed inside its window).
Fail-closed knob: if the Workload API/SVID is unavailable in spiffe mode, behavior is configurable (queue+retry / refuse) rather than silently emitting unsigned.
Conformance test
TestAuditCertifier_LocalMatchesStaticSigning
TestAuditCertifier_SpiffeEpochRotationVerifiesOffline (SVID rotation -> new epoch -> point-in-time verify across an expired epoch)
TestAuditCertifier_NodeChangeContinuesChain (same SPIFFE ID, new key, chain intact)
Non-Kubernetes node attestation (VMs / bare metal / cloud IID) — future iteration; the spiffe backend consumes the Workload API regardless, so no agent-side change is expected when VM attestors land server-side.
An Okta M2M client path — the Workload API replaces it; the agent holds no Okta client.
Identity/attestation policy itself — the agent consumes its SVID; it does not evaluate attestation.
References
Builds on #220 (Ed25519 signing) and #212 (hash chain). Platform counterpart: initializ/console-next#6 (managed SPIFFE/SPIRE rooted in customer KMS, K8s).
Context
Audit-event signing (#220) loads a static Ed25519 key from
FORGE_AUDIT_SIGNING_KEY_B64(env) or a file, in-process. That is the correct baseline: self-rooted, tamper-evident, zero external dependencies.For fleet-scale enterprise identity + key custody, hand-registering an OIDC app per agent (inbound) and an M2M app per agent (outbound) does not scale, and Okta has no agent-native construct. The answer is SPIFFE/SPIRE: the agent consumes a short-lived SVID from the local Workload API — one credential for inbound and outbound — issued by an Initializ-managed SPIRE plane whose CA is rooted in the customer's KMS (see initializ/console-next#6). The agent never holds an Okta client or a static secret.
This issue is the forge agent-side work: a
Certifierabstraction with alocaldefault and aspiffebackend, plus the epoch record and verifier changes. Scope: Kubernetes (k8s_psatnode attestation) only for this iteration.Proposed control
Certifierinterface yielding the current signing identity ({key/handle, kid, cert/epoch, notBefore, notAfter}), rotating before expiry. Two backends:local(DEFAULT — unchanged). Loads/generates the key fromFORGE_AUDIT_SIGNING_KEY_B64(env), self-rooted, no network. Stays the default so tamper-evident signing has no external dependency; when unconfigured, signing is off and the wire shape is byte-identical to pre-feat(audit): Ed25519 per-event signing + JWKS endpoint #220.spiffe(managed identity route). Reads the agent's SVID from the SPIFFE Workload API Unix socket (no Okta client, no secret — the local SPIRE Agent workload-attests the process via K8s selectors: ServiceAccount / namespace / pod). The SVID (or an ephemeral key certified by it) becomes the audit-signing epoch key. On each rotation:SetSigner/Emit),audit_key_epochrecord carrying the SVID/cert + epoch pubkey, so the log is self-verifying offline,Node churn: the SPIFFE ID is workload-scoped, not node-scoped — when the agent is rescheduled to a new node, it re-attests and receives a fresh SVID with the same SPIFFE ID, new key. That simply opens a new epoch under the same identity; the hash chain (#212) continues and verification uses point-in-time epoch validity. No identity loss, no manual re-provisioning, no broken audit trail.
VerifyAuditLoggains: resolve each event'skid-> itsaudit_key_epoch, verify the epoch against the SPIRE trust bundle (rooted in the customer KMS, published by the platform), and check point-in-time validity (event.tsinside the epoch window — expired epochs still verify historical events).Acceptance criteria
Certifierinterface withlocalandspiffebackends; selected viaaudit.signing.mode(defaultlocal).localreproduces current feat(audit): Ed25519 per-event signing + JWKS endpoint #220 behavior exactly (env key, self-rooted, off-by-default, byte-identical unsigned).spiffereads the SVID from the Workload API socket (no Okta client / no static secret), signs locally, and rotates with the SVID — no per-event network call.audit_key_epochis emitted and the stream still verifies end-to-end.VerifyAuditLogverifies events against the epoch + SPIRE trust bundle with point-in-time validity (an expired epoch still verifies events signed inside its window).spiffemode, behavior is configurable (queue+retry / refuse) rather than silently emitting unsigned.Conformance test
TestAuditCertifier_LocalMatchesStaticSigningTestAuditCertifier_SpiffeEpochRotationVerifiesOffline(SVID rotation -> new epoch -> point-in-time verify across an expired epoch)TestAuditCertifier_NodeChangeContinuesChain(same SPIFFE ID, new key, chain intact)Out of scope
spiffebackend consumes the Workload API regardless, so no agent-side change is expected when VM attestors land server-side.References
Builds on #220 (Ed25519 signing) and #212 (hash chain). Platform counterpart: initializ/console-next#6 (managed SPIFFE/SPIRE rooted in customer KMS, K8s).