Signature Verification Modes
A tri-state trust gate — off, warn, enforce — so you can preview enforcement before turning it on.
Volt verifies the signature of the content it loads — manifests, images, and any signed artifact a service running on Volt fetches. Verification has three modes, modeled on SELinux warn and AppArmor complain. Every Volt component — and any software you build on Volt — should expose all three so operators get one consistent mental model across the whole stack.
The three modes
| Mode | Behavior |
|---|---|
off | Skip signature-trust verification. Content-integrity hashing still hard-fails. |
warn | Always run the crypto verify. On a would-deny → count it, durably record it, then allow. (Alias: permissive.) |
enforce | Fail-closed — deny on any verification failure. |
Why warn exists: it lets you preview the impact of enforcement before you turn it on. Traffic keeps flowing while every "this would have been denied" is recorded — so you reach confidence from data instead of a leap of faith, and flip to enforce only once the record is clean.
The contract
Whether you use the built-in gates or add your own, five rules hold:
- One vocabulary. Canonical values are
off/warn/enforce. Accept aliases (disabled/none→ off;warn/complain/audit→ warn;enforcing/required→ enforce). A blank or unknown value is a fatal error — a malformed security mode must never silently degrade open. - Infra vs. policy. A verifier that cannot run (key material unreadable, crypto library missing, artifact unfetchable) is an infrastructure error and fails closed in every mode, including warn. Only a completed "no" verdict (unsigned, bad signature, untrusted key, bad encoding) is downgradable in warn.
- Permissive still fully verifies. Run the whole crypto pipeline; only suppress the final deny. Never promote a warn-allowed-but-unverified artifact into a trusted or fast-path cache — quarantine it so a later read can't silently trust it.
- Durable record. Append every would-deny to a persistent log. The decision to enforce is made against that history — not an in-memory counter that resets on restart.
- No silent flips. Any legacy on/off toggle maps
true→ enforce andfalse→ no change. Only an explicitoffever skips the crypto.
Adding it to your service
Building software that runs on Volt and verifies its own signed content? Expose the same three modes. It is about a screen of code; port this recipe to your language.
1 — A mode setting, fatal on garbage
Read an environment variable VOLT_<COMPONENT>_VERIFY_MODE, normalize aliases, and refuse to start on an unknown value. Log the resolved mode at startup so an unintended off is visible.
parse_mode(s):
t = lower(trim(s))
if t in {off, disabled, none, false}: return "off"
if t in {warn, permissive, complain, audit, observe}: return "warn"
if t in {enforce, enforcing, strict, require, required, true}: return "enforce"
# never default to off — a bad security mode must fail loudly
fatal("VERIFY_MODE must be off|warn|enforce")
2 — Classify each outcome: verdict vs. infrastructure
# VERDICT — the verifier ran and reached a "no" (downgradable in warn):
# unsigned, bad-signature, untrusted-key, bad-encoding, key-set-empty
# INFRA — the verifier could NOT run (fail-closed in EVERY mode):
# key unreadable/absent, crypto lib missing, crash, artifact unfetchable
3 — The decision funnel
Route every verification site through one function:
decide(mode, verdict, infra_err):
if infra_err: # verifier couldn't run
record(); raise # FAIL-CLOSED in all modes
if mode == "off": return allow # not consulted
if verdict.ok: return allow # quiet success
if mode == "enforce":
record(decision="deny"); raise # deny
if mode == "warn":
record(decision="allow") # if the record write fails, raise (fail-closed)
return allow # ALLOW, but it's on the record
Content-integrity checks — the content-address hash, path-traversal guards, schema/type gates — stay hard-fail in every mode, including off. Integrity is not authenticity.
4 — Never poison trusted state
In warn (and off) you may return the artifact, but write it to a quarantine namespace, never the trusted fast-path. If you cache verified results, re-verify at the point of use — treat a cache location or a stored "verified" flag as a hint, never as proof.
5 — Record and expose it
Append a would-deny event to a durable log and increment a metric. Emit this shape (the enforce deny record is identical except two fields, so one dashboard serves both):
{ "event": "sig_verify_would_deny", "ts": "<RFC3339>", "mode": "warn",
"would_have_denied": true, "decision": "allow",
"component": "<who>", "phase": "pull|deploy|load",
"digest": "sha256…", "key_id": "…",
"sig_status": "unsigned|bad-signature|untrusted-key|bad-encoding|verifier-error",
"reason": "…" }
Expose per-outcome counters and a gauge of the active mode. Page only on verifier-errors and on a component that has lingered in warn past your SLA — would-deny events are observational and never page.
6 — Flip on measured confidence
A scope is ready for enforce when its durable would-deny log has been empty across a full, representative window — including the long tail (scheduled jobs, backups, failover, rarely-touched tenants). Remediate each would-deny first (sign the artifact, or add its key to your trust set). Roll out per scope: development → staging → one production tenant → fleet. Never flip a scope to enforce before its log is clean.
Invariants
| Never… | Because… |
|---|---|
let warn skip the crypto | that is off — warn must still verify |
| fail open on an infrastructure error | an unrunnable verifier is not a "pass" |
| trust a cache location or a "verified" flag | re-run the crypto at the point of use |
coerce a blank/unknown mode to off | a malformed security mode must fail loudly |
flip to enforce on an un-clean log | enforcement without evidence causes outages |