Expiry and Rollback Protection
A signature proves a bundle is authentic. It does not prove the bundle is current. APF adds two freshness controls: a monotonic revision counter that refuses older bundles, and a validity window that bounds when a bundle may be accepted.
The Revision Counter
Every bundle carries a policy_revision. The policy service keeps the highest revision it has accepted, called the high-water mark, and the digest of the payload it accepted at that revision.
The counter binds to the payload digest, not the artifact digest, so re-signing identical content does not look like a change. A revision is recorded only after the bundle verifies and compiles.
On a fresh counter store the high-water mark is 0, and revision 0 is rejected as a regression. Start every bundle at revision 1 or higher.
The Counter Store
The default counter store is a pair of files at --counter-store, /var/lib/apf/counter by default:
<path>holds the high-water mark as a single decimal line.<path>.digestholds<revision> <sha256>for the accepted payload.
Writes go to a temporary file that is then renamed, so a crash leaves either the old or the new value. If the two files disagree after a crash, or the digest file is missing, the digest is treated as unknown rather than causing a spurious rejection. The next bundle accepted at the current revision is then accepted whatever its content, normally the reload at startup, and its digest is recorded again.
The file store suits one policy service per host. It is not shared between policy services.
Counter Scope
- Each policy service has its own counter. Two hosts can accept the same revision independently.
- All bundles a policy service loads share one counter, whatever their
id. Use one revision sequence per policy service. apf-compile compileuses a temporary counter, so it cannot detect a revision that a running service would reject.- The policy gateway keeps no counter. It trusts the policy service.
Resetting the Counter
Removing the counter files resets the high-water mark to 0 and lets any revision load again. That reopens the window for rollback, so treat it as a privileged recovery action, for example when you rebuild a host for a new environment. The counter directory should be writable only by the apf account.
The Validity Window
not_before and expires_at bound when a bundle may be accepted. Both are RFC 3339 times.
Because expiry is checked at ingest, plan renewals ahead of time: publish a re-signed bundle with a higher revision and a later expires_at well before the current one expires. If a policy service restarts after its only bundle has expired, it does not start, and with fail_closed registrations OpenShell refuses the operations APF governs.
Document Validity
Separately from the bundle’s validity window, the policy service can limit how long a served document may be cached. With --document-ttl-secs set, each GetDocument response carries valid_until_ms, and the provenance token carries a matching exp. The interceptor rejects an expired provenance token. APF’s sandbox policy refresher arms a single refresh 30 seconds before the deadline it learns from a policy set change. It does not arm one at gateway startup, and it fires once, so do not rely on it to keep long-running sandboxes refreshed. The default, 0, sets no document expiry.
What These Controls Stop
Next Steps
- To publish a new revision, refer to Update Policies.
- To see how these checks fail in practice, refer to Reject a Tampered Bundle.