Runtime State
Configuration and ownership
authcrunch.Config.State (state) opts the root runtime into persistence with
*state.Config. Omission keeps the previous volatile behavior. A present empty
configuration is invalid. The only setting is Directory (directory), an
absolute non-root path. Validation normalizes it without filesystem writes.
pkg/state/parser.NewStateConfigFromDirectives(statements) parses exactly one
encoded directory PATH body statement. Hosts own traversal, placeholders and
cfgutil.EncodeArgs; the public parser owns grammar and typed validation.
Empty blocks, duplicates, unknown settings and extra arguments fail. Apply the
result to Config.State before NewServer. There are no Docker, Caddy, HTTP,
environment-variable or global-directory dependencies.
Read configuration and operations
for retained state, restart guarantees, backups and deployment limits. Hosts
adapt their configuration syntax to the public parser and coordinate runtime
ownership. Do not change or run tooling in sibling repositories.
Storage invariants
pkg/state/{store,catalog,codec}.go owns Store and Record. state.Open
creates private 0700 local storage, holds an exclusive advisory owner.lock,
and reuses a private 0600 AES-256 key in master.key. Record filenames are
SHA-256 hashes of trusted component names. AES-GCM authenticates the envelope
version, binding and payload with the filename as associated data. A separately
authenticated catalog detects missing committed records. Reject corruption,
missing key/catalog, unsafe permissions, symlinks, and concurrent owners.
Use local Unix filesystems with reliable locks, atomic replacement and directory
sync. Windows persistence fails closed: secure ACL and durable directory support
are not implemented. Do not weaken commits to make an unsupported platform pass.
When creating a state directory hierarchy, sync the containing directories too;
syncing records alone does not persist newly created directory entries.
Save a complete private snapshot under the component mutex. Commit an intent
marker, synced temporary record, atomic rename and directory sync; remove/sync
the marker before acknowledging success. An interrupted marked record is retired
on reopen, sacrificing its sessions rather than resurrecting a possible
revocation. Failed disk commits poison the store. No failed write may publish a
credential, redirect, successful logout or subsequent state-backed cached authentication.
Successful mutations are durable before Close; Close only releases resources.
Never accept persistence DTOs from HTTP. Record.Encode/Decode uses gob to retain
server-only proof fields whose public JSON/XML/YAML tags remain -. The encrypted
envelope is format version 1; evolving DTO/security semantics requires a reviewed
migration or explicit invalidation. Bounded snapshots reject oversized records.
Keep plaintext snapshots, keys and credentials out of errors and logs.
Transactional consumers can call Record.PrepareEncode before committing a
candidate and then Save the prepared bytes. state.ErrCapacity from preparation
leaves the record and store unchanged: reject or undo the candidate without
discarding existing authority. Ordinary Encode/Save retain fail-closed behavior
for oversized mutations. This is an encoded-payload limit, not a hard bound on
gob's internal staging allocation; retain component input and count limits.
Count and request-size limits do not bound the aggregate snapshot. All session
admissions and rotations must prepare their candidate and undo it on capacity
refusal. Preserve previous sessions, replacement targets, and complete replay
history. A full component must not disable unrelated persistent consumers.
Deletion and terminal revocation must remain possible at the record limit.
Runtime integration
server_persistent_state.go binds sessions to a random configuration epoch stored
under the digest of the normalized security configuration, excluding the storage
path and diagnostic-only Logging settings. Logging rule additions, changes,
and removal must preserve valid persisted sessions; server_logging_e2e_test.go
exercises that invariant through real TLS restarts. Every other observed configuration transition within that
directory changes the epoch, including removal of a component; returning to an older configuration cannot
restore that component's old authority. Reordering or unrelated configuration
changes can conservatively require login. Stable generated KMS keys use separate
bindings and survive these session invalidations.
Root construction opens storage first and owns it last during reverse cleanup.
It injects KMS storage before constructing keys, restores local identity epochs,
then attaches session state before publication. Server.Close leaves durable
records intact. Each directory has one live owner: drain and close the old runtime
before opening its replacement. Opening new-before-closing-old must fail, not
fall back to memory. This is not a distributed store or overlapping reload API.
| Owner | Persistent integration |
|---|
pkg/kms | CryptoKeyStoreConfig.SetStateStore; generated ES512/EdDSA/Ed25519 material is bound to generation tag and algorithm family; no process-global key sharing in persistent mode |
pkg/user | MarshalSession / UnmarshalSession retain canonical identity and private login proof, rebuild derived maps, and exclude unfinished authenticator secrets |
pkg/authn/cache | SessionCache.ConfigurePersistentState restores only unexpired completed users; Add/Delete commit synchronously |
pkg/authn/token_refresh | MemoryStore.ConfigurePersistentState retains complete families, original authentication time/deadlines, current digest and every spent digest |
pkg/authz | Gatekeeper.ConfigurePersistentState restores completed direct OAuth sessions; origin binding and current ACL evaluation remain mandatory |
pkg/identity, pkg/ids/local | ConfigurePersistentState retains the backend epoch only for the exact recorded identity-file digest; transactions update that digest under the existing file lock |
pkg/oidc | Provider.ConfigurePersistentState retains browser sessions, consent, codes and complete access/refresh families; issuance commits inside the current-identity transaction |
pkg/authn | Portal.ConfigurePersistentState attaches its owned components and rejects HTTP/BasicAuth after storage failure |
Standalone consumers own the state.Store, choose trusted names and bindings,
configure components before exposing them, and close storage after draining all
consumers. A custom binding must cover issuer, providers, policies, clients,
identity and signing configuration. Directly configured standalone components
must also invalidate removed/reintroduced namespaces; root NewServer supplies
that configuration-epoch behavior automatically.
Preserve spent credentials until no descendant remains live. Restoring only
current refresh hashes or clearing replay history at restart is a security bug.
Validate restored sessions and grants in detached maps, then publish them together.
A failed restore must leave no partially restored authority, including through
standalone OAuth gatekeeper and OIDC provider APIs.
Refresh gob snapshots retain typed provider source, provider trust digest and
bounded captured claim bytes as well as complete replay history. Restore rejects
unknown sources, mixed local/provider evidence, invalid JSON, unsupported kinds
and oversized snapshots before publishing detached state. Current provider
trust and canonical claim checks still run at renewal. Keep provider configuration
in custom portal bindings; persistence does not turn captured claims into live
upstream revalidation. Provider cached users retain no local credential proof.
TestPersistentProviderEvidenceRejection and the provider portal TLS restart
journey cover these boundaries alongside existing local identity epochs.
The portal response guard strips staged headers/cookies when a later composite
login commit fails; preserve this even when the earlier session write succeeded.
Check component health as well as the backing store: a session serialization
failure can poison the cache without a failed disk write. Both HTTP and BasicAuth
must reject that portal state.
OIDC HTTP responses are buffered: a failed commit must discard success headers,
body, redirects and cookies. Hosts composing OIDC logout/login must use
LogoutWithError / ClearSessionWithError and stop on error. Existing void
methods emit an unavailable response for compatibility.
Recoverable OIDC capacity errors also require composite login cleanup: remove
new portal sessions, revoke undelivered refresh families with a bounded context
independent of request cancellation, and restore pre-credential response headers.
Do not undo earlier completed revocations. Consult the OIDC skill for candidate
rollback and revocation-size invariants.
Local identity files remain the source of current account status and credential
versions. Persist their epoch after an atomic identity write and before granting
authority from current identity. Keep persistence attached to transaction targets,
including self-service mutations; Database.Copy must not modify the source's
state record. File mismatch chooses a fresh epoch, never trusts serialized
loaded_at as authentication evidence. During attachment, adopt the identity
snapshot read under the file lock before binding its digest to an epoch; the
constructor's earlier in-memory snapshot may already be stale.
Rolling back both the identity file and
its entire matching state backup requires external rollback protection.
Validation
Keep parser tests external with an executable constructor example. Use the
parser in root TLS consumers and the actual authdb executable fixture.
pkg/state/*_test.go: configuration, private authenticated storage, locking,
corruption/missing files, independent snapshots, interrupted writes, process
exit without Close, nested directory creation, partial first initialization,
and failure poisoning.
pkg/kms/persistent_state_test.go: all generated algorithms, old JWT verification
after reopen, and independent-directory key isolation.
pkg/user/session_state_test.go, pkg/authn/cache/session_state_test.go:
private-proof fidelity, derived authorization fields, expiry/deletion/failure.
pkg/authn/token_refresh/persistent_test.go, pkg/oidc/persistent_state_test.go:
rotation, complete spent history, replay revocation, expiry, commit failure,
and capacity refusal that preserves existing credentials and unrelated records.
pkg/authn/oidc_completion_e2e_test.go: real browser login rejects recoverable
OIDC capacity refusal without delivering credentials or leaking session slots;
healthy retry succeeds at the configured refresh capacity.
pkg/authz/oauth_test.go, pkg/oidc/persistent_state_test.go: atomic restoration
rejection through unit and real TLS consumers.
pkg/identity/persistent_state_test.go: unchanged-file continuity, durable
security-version revocation, self-service transactions, identity-file rollback,
and file replacement between database construction and persistence attachment.
server_persistent_state_e2e_test.go: TLS portal-free OAuth, lost pending login,
ACL checks, logout, config serialization and removal/reintroduction; portal
session/profile, native refresh, OIDC consent/code/access/refresh and replay;
interrupted commit recovery, failed final login commits, and failed construction
releasing exclusive ownership.
cmd/authdb/persistent_state_e2e_test.go: built executable, real browser login,
SIGKILL, same-address restart, identical JWKS and surviving old credentials.
make test TEST_DIR='./pkg/state/... ./pkg/user ./pkg/kms ./pkg/authn ./pkg/authn/cache ./pkg/authn/token_refresh ./pkg/authz ./pkg/identity ./pkg/oidc . ./cmd/authdb ./internal/tag' TEST='State|Store|RecordCodec|ConfigValidation|Persistent|SessionState|Capacity|TagCompliance|TestE2EAuthdb' COVERAGE_DIR='.coverage/runtime-state'
make ci-check
The testing contract defines diagnostics and artifact
interpretation. The default
full suite must retain volatile-mode replacement tests alongside persistent-mode
journeys; opt-in storage must not change existing omission semantics.