Anchoring Runbook

This page is the operator runbook for the audit-log anchoring rail added on top of the SHA-256 hash chain described in the Security Policy and implemented in spotforecast2_safe.manager.logger. It is deliberately separate from that page: anchoring is something an operator of a deployed system does to their own retained logs, not something spotforecast2-safe itself does automatically, ships pre-configured, or can verify on its own authority.

The two make targets

make anchor LOG=~/spotforecast2_safe_models/logs/sf2-safe-logger_20260810_090000.log \
            TSA_URL=https://timestamp.digicert.com

make anchor-verify LOG=~/spotforecast2_safe_models/logs/sf2-safe-logger_20260810_090000.log

LOG accepts either a single audit log file or a directory; a directory is anchored as one AuditAnchorSet document (audit-anchor-set.json) covering every *.log file found in it, which is what closes the rotation gap left by a per-file anchor (setup_logging starts a fresh file, and therefore a fresh chain, on every process start).

make anchor refuses immediately, before writing anything, if the named chain is not intact (verify_audit_log(...).chain_intact is False), and again if TSA_URL is unset — there is no default timestamping authority; see Choosing a witness below for why and what to pick instead. On success it writes the canonical anchor document next to the log(s), gets an RFC 3161 token for it from TSA_URL via openssl ts -query and a stdlib HTTP POST, and stores the token (.tsr) and the TSA’s certificate chain (.tsa-chain.pem) alongside the document. make anchor-verify never touches the network: it re-verifies the stored token against the stored chain with openssl ts -verify, then hands the document’s AuditAnchor to verify_audit_log and prints AuditChainReport.summary().

Neither target is a prerequisite of make verify, make release, or make ship. Anchoring belongs to the deployer operating spotforecast2-safe in production, on logs that do not exist yet at release time; it is not a step in shipping this package.

Choosing a witness

An anchor is only as good as the place it points to. make anchor never picks one for you — endorsing a specific commercial or free service is not this Makefile’s call to make. The options, roughly cheapest first:

Witness What it gives you Caveat
RFC 3161 timestamp, free TSA (e.g. freetsa.org) An independent “this hash existed at time T” claim, verifiable offline forever with the stored token and chain No legal presumption; free services can (and have) disappeared
RFC 3161 timestamp, commercial TSA (DigiCert, Sectigo, GlobalSign, …) Same as above, backed by an organisation with an SLA Still no legal presumption unless the TSA is eIDAS-qualified
RFC 3161 timestamp, eIDAS-qualified EU Trust Service Provider (browse the EU Trusted List) Everything a free TSA gives you, plus a legal presumption of the accuracy of the date/time and the integrity of the timestamped data, under eIDAS Regulation (EU) No 910/2014, Article 41(2) Usually a paid, registered service
Any git remote (push the anchor document, or a commit containing it, to a second control domain) A second party that would have to collude with whoever holds the log to backdate it Force-pushable; only as strong as who controls that remote
PyPI digest immutability Free, and PyPI never allows re-uploading a version Only proves something about a released package, not an arbitrary log file — project-side only, not a deployer tool
A second organisation’s log (e.g. a monitoring vendor mirrors the anchor document) Often the cheapest real option for a deployer who already has such a relationship Only as strong as that organisation’s own retention practice
Printed or notarised record Withstands a purely digital compromise Manual, does not scale to a daily anchoring cadence

Rejected: Sigstore/Rekor (the project retired Sigstore in 2026-07 when the hosted CI that provided its OIDC identity went away — see Security Policy) and OpenTimestamps (a Bitcoin-anchored scheme with no maintained, audited Python or CLI tooling this project could adopt without a new dependency). Both were considered and set aside when this rail was designed.

openssl gotchas

  • LibreSSL vs OpenSSL. macOS ships LibreSSL as /usr/bin/openssl. Both LibreSSL 3.x and Homebrew’s OpenSSL 3.x accept ts -query -sha256 -cert and ts -verify -CAfile despite LibreSSL’s usage banner advertising only md4/md5/sha1/ripemd160 — SHA-256 timestamping is not blocked in practice. What LibreSSL’s ts -verify does not have is -attime: archival verification of a token after the TSA’s own signing certificate has expired needs that flag, and therefore needs Homebrew OpenSSL (brew install openssl@3), not the system openssl. Point make anchor / make anchor-verify at it explicitly:

    make anchor-verify LOG=... OPENSSL=/opt/homebrew/opt/openssl@3/bin/openssl
  • The certificate chain must be stored, not just the token. openssl ts -verify needs the TSA’s certificate chain to check the token’s signature; a .tsr file alone is not self-verifying. make anchor extracts the chain from the TSA’s response and writes it next to the token (<doc>.tsa-chain.pem) precisely because that chain, not just the token, has to survive as long as the log does — potentially far longer than the TSA’s own certificate remains valid or its website stays up. Losing the chain file makes a stored token unverifiable even though nothing about the log or the token itself changed.

What make cannot provide

None of this promotes make anchor from a convenience into a control on its own:

  • It cannot be the witness. make anchor automates the round trip to an independent service; it is not itself independent of the machine or operator running it. A purely local anchor (no TSA_URL, or a TSA under the same operator’s control) proves nothing beyond what the log file already proves about itself.
  • It cannot compel cadence. How often an operator anchors is entirely their choice; verify_audit_log’s prefix matching (AnchorStatus. MATCHES_PREFIX) means an infrequently anchored log is still gracefully handled rather than falsely flagged, but the records appended after the last anchor remain unanchored until the next one runs.
  • It cannot survive being skipped. Nothing forces an operator to run make anchor at all; unlike make verify, it is not wired into any release or CI gate (there is no CI - see Security Policy). What is mitigated: the offline half of this rail — comparing a presented AuditAnchor against a recomputed chain — runs under make verify on every commit (via the docstring examples make doc executes, and the tests make test runs), so that code path cannot silently rot even in a year nobody anchors anything. The decision to anchor a given production log remains entirely the deployer’s.
  • It cannot establish authorship. Anchoring binds a chain head to an external witness at a point in time; it says nothing about who wrote the records. The chain is, and remains, unkeyed — see AuditCaveat. AUTHORSHIP_NOT_ESTABLISHED, one of the two STANDING_LIMITATIONS every AuditChainReport carries regardless of anchoring status.

See also

  • Security Policy — the hash chain itself, and what this package does and does not provide overall.
  • spotforecast2_safe.manager.logger (API reference: Manager section) — AnchorStatus, AuditAnchor, AuditAnchorSet, AuditCaveat, AuditChainReport, verify_audit_log.
  • scripts/anchor_audit_log.py — the script make anchor / make anchor-verify invoke; run it directly for options these two targets do not expose (--ca-file, --timeout, …).