Tier 1 — Production store bootstrap¶
Status: implemented (inert, unwired). No production application file imports this mechanism. Related: key_lifecycle.md, whole_store_anti_rollback.md, anti_rollback_tpm_host_witness.md.
Purpose¶
Give an operator a real, deterministic, non-ad-hoc way to configure,
inspect, and one-time-provision SqliteRecoveryContractStore on the host
that actually runs pfsense-mcp-server, without needing to guess a
database path, open a Python shell to construct the store class directly,
manually edit SQLite, or reimplement any HMAC/insert-only provisioning
logic already built in anti_rollback.py/store.py.
This spec deliberately does not decide whether/when the running MCP server process itself should construct this store — see "Explicitly deferred decision" below.
Security goals¶
- G1: No production application code path (
application.py/factory.py/server.py/tools/) can reach a mutation endpoint through this mechanism, regardless of whether it is ever wired in — verified structurally, not by convention (see Required tests). - G2: The store's location is never guessed, never falls back to an undocumented default, and is not affected by the current working directory of whatever process reads it.
- G3: Configuring the store's location never itself creates state on disk. Only an explicit, later, separate call creates or opens the store.
- G4: The integrity key file and the store file are provably in
different directories (
key_lifecycle.mdInvariant I1), enforced by code, not merely documented. - G5: Every file-safety property
SqliteRecoveryContractStorealready enforces (symlink rejection, owner-only permissions, malformed-file rejection, atomic creation) continues to apply unchanged — this layer adds configuration validation above that, never a parallel or weaker check. - G6: No secret material (key bytes, TPM auth secrets) is ever read, logged, or printed by the operator-facing status tool.
Invariants¶
- I1:
load_production_store_config()performs zero filesystem I/O. It validates only the shape of the two configured paths (absolute, distinct file, distinct parent directory). Existence, permissions, and symlink-ness of either path are validated later, byopen_production_store()/key_lifecycle.load_key_material(), not duplicated here. - I2: Tier 1 being unconfigured (
PFSENSE_TIER1_STORE_PATHandPFSENSE_TIER1_STORE_KEY_FILEboth unset) is the default, expected, safe state —load_production_store_config()returnsNone, not an error. Tier 1 remains exactly as inert as it has been throughout this project. - I3: Partial configuration (exactly one of the two variables set) is
always a
Tier1ConfigurationError, never guessed or defaulted. - I4: The store identifier is a fixed, documented constant
(
PRODUCTION_STORE_ID = "tier1-production-anchor"), not another path an operator could get wrong — there is exactly one production anchor store per deployment, no multi-tenancy. - I5:
open_production_store()is the only function in this mechanism that touches the filesystem. Calling it against a path that has never held a store creates one, throughSqliteRecoveryContractStore's own already-tested atomic-creation path (store.py::_prepare_path) — never through code duplicated here. - I6:
provision_production_anchor_baseline()performs no HMAC, insert-only, or ordering logic itself — it composesopen_production_store()with the already-implemented, tested Slice B primitiveSqliteRecoveryContractStore.provision_anchor_baseline().
Explicitly deferred decision — not resolved by this spec¶
Whether the running pfsense-mcp-server process itself (via
application.py/factory.py) should ever construct this store is a
separate, not-yet-authorized decision, deliberately left open here.
Every phase of this project's own roadmap
(docs/tier1/IMPLEMENTATION_ROADMAP.md) has treated "a non-tier1
production file imports pfsense_mcp.tier1" as the single largest
threshold in the whole project — bigger than implementing the sealed
executor itself, which remains "never constructed by production" even
after Phase 3's completion. tests/tier1/test_isolation.py::
test_tier1_is_not_imported_outside_its_inert_package currently enforces,
as a hard, 100%-passing structural test, that this has never happened.
The roadmap's own Phase 5 entry gate (the first phase that touches
Application/factory.py/ToolRegistry) requires both Milestone 0
naming (done — ADR-020) and Phase 4's live disposable-lab evidence
(not done).
This mechanism therefore ships as importable only from outside
src/pfsense_mcp/ — specifically from
scripts/tier1_store_bootstrap.py, which lives outside the isolation
test's scan root and is free to import inert Tier 1 code, exactly the
same pattern already established for scripts/build_endpoint_catalogue.py
importing pfsense_mcp.api_surface. This fully satisfies every concrete
operator-facing requirement (deterministic path selection, safe
construction, a real provisioning entrypoint) without requiring the
Phase-5-scoped decision to be made first. If a future session wants the
running server itself to validate or expose this configuration (e.g. in
pfsense_mcp_info), that is its own explicit future authorization,
informed by this spec but not granted by it.
Trust boundaries¶
| Boundary | Trusted side | Untrusted side | Enforcement point |
|---|---|---|---|
| Configured path vs. guesswork | Two explicit, required-together env vars | Any undocumented fallback/default path | load_production_store_config() (I2/I3) |
| Store file vs. filesystem | Process holding a validated, non-symlink, owner-only descriptor | Any other process/user on the host | SqliteRecoveryContractStore._prepare_path() (unchanged, reused) |
| Key file vs. store file | Two distinct directories | An attacker who can read one directory learning the other's location | I1 same-parent-directory check (new) |
| Operator tool vs. pfSense | scripts/tier1_store_bootstrap.py, zero pfSense-capable imports |
Any accidental mutation reachability | tests/test_tier1_store_bootstrap_isolation.py (AST-based, new) |
State ownership¶
src/pfsense_mcp/tier1/production_store.py(new) owns:ProductionStoreConfig,load_production_store_config(),open_production_store(),provision_production_anchor_baseline(),PRODUCTION_STORE_ID.SqliteRecoveryContractStore.anchor_provisioning_status()(new, instore.py) andProvisioningRecord.read()(new, inanti_rollback.py) are read-only accessors for operator tooling — never called by provisioning or execution logic itself.scripts/tier1_store_bootstrap.py(new) owns the CLI surface only; it contains no independent logic beyond argument parsing and print formatting.
Interfaces¶
# src/pfsense_mcp/tier1/production_store.py
PRODUCTION_STORE_ID: str # "tier1-production-anchor"
@dataclass(frozen=True)
class ProductionStoreConfig:
store_path: Path
key_file: Path
store_id: str = PRODUCTION_STORE_ID
def load_production_store_config(env: dict[str, str] | None = None) -> ProductionStoreConfig | None: ...
def open_production_store(config: ProductionStoreConfig) -> SqliteRecoveryContractStore: ...
def provision_production_anchor_baseline(config: ProductionStoreConfig, *, value: int, handle: str) -> None: ...
# environment variables
PFSENSE_TIER1_STORE_PATH absolute path to the SQLite store file
PFSENSE_TIER1_STORE_KEY_FILE absolute path to the HMAC integrity key
file (key_lifecycle.py format), in a
different directory than the store file
# scripts/tier1_store_bootstrap.py
tier1_store_bootstrap.py # read-only status
tier1_store_bootstrap.py --provision VALUE --handle H --yes-i-understand
Failure modes¶
| Failure | Detection | Resulting state | Automatic retry |
|---|---|---|---|
| Neither env var set | load_production_store_config() |
Returns None; Tier 1 stays inert |
N/A — not a failure |
| Exactly one env var set | load_production_store_config() |
Tier1ConfigurationError, nothing touched |
No |
| Relative path | load_production_store_config() |
Tier1ConfigurationError, nothing touched |
No |
| Store path == key file, or same parent directory | load_production_store_config() |
Tier1ConfigurationError, nothing touched |
No |
| Store parent directory missing | SqliteRecoveryContractStore._prepare_path() |
ContractValidationError (fixed this session — previously a raw unhandled FileNotFoundError), nothing created |
No |
| Store parent directory unsafe (not 0700 / wrong owner) | _prepare_path() |
ContractValidationError, nothing created |
No |
| Store path is a symlink, or an existing file is unsafe | _prepare_path() |
ContractValidationError, nothing created/opened |
No |
| Existing store file is malformed/not SQLite | _connect() |
ContractIntegrityError, nothing further happens |
No |
| Key file missing/unsafe/malformed | key_lifecycle.load_key_material() |
KeyMaterialError, store is never constructed |
No |
| Store already provisioned | provision_anchor_baseline() (Slice B, unchanged) |
AnchorAlreadyProvisionedError, nothing overwritten |
No |
Recovery behavior¶
- Backup:
_connect()setsPRAGMA journal_mode = DELETE(no-wal/-shmsidecar files), so a filesystem-level copy of the single store file, taken while thepfsense-mcp-serverprocess (or, currently, any ad-hoc script invocation) is not actively writing, is a complete, consistent backup.sqlite3 <path> ".backup <dest>"is an equivalent, safe-while-open alternative. - Restore: stop any process holding the store open, replace the file,
restart.
SqliteRecoveryContractStore.__init__'s existing schema/ integrity verification (_verify_schema(), HMAC checks on every read) detects a restored file that does not match the configuredintegrity_key/store_idand fails closed (ContractIntegrityError) rather than silently accepting it. - Restart/reopen: constructing
SqliteRecoveryContractStoreagainst an existing, valid store file is idempotent and side-effect-free beyond opening a connection — already covered bytest_store.py's existingtest_create_load_and_restart_preserve_authoritative_contract; this spec's own tests add the same coverage specifically throughopen_production_store(). - Directory creation: deliberately not this mechanism's job. The
parent directory of both the store path and the key file must already
exist, owned by the effective user, mode
0700, before eitheropen_production_store()orkey_lifecycle.load_key_material()is called — provisioned by deployment tooling (e.g. a systemdStateDirectory=/ConfigurationDirectory=directive, or a manualinstall -d -m 0700), not auto-created here. Auto-creating directories from inside a security-sensitive store-construction path would introduce exactly the kind of race/permission-guessing risk this mechanism exists to avoid.
Non-goals¶
- Does not decide the deferred production-wiring question above.
- Does not implement key rotation, backup automation, or a systemd unit
file — those are deployment/operational concerns, out of scope here
(see
key_lifecycle.md's own equivalent non-goal for the encryption/ integrity keys themselves). - Does not touch the TPM, the host-witness daemon, or any Proxmox/VM
configuration —
value/handlepassed toprovision_production_anchor_baseline()must already have been obtained from the real anchor by the caller (seeanti_rollback_tpm_host_witness.md).
Required tests¶
- No configuration →
None, zero filesystem access (tests/tier1/ test_production_store.py). - Partial configuration (either direction) →
Tier1ConfigurationError. - Relative store path / relative key file →
Tier1ConfigurationError. - Store path == key file →
Tier1ConfigurationError. - Store path and key file share a parent directory →
Tier1ConfigurationError(Invariant I1/G4). - Valid configuration → deterministic
ProductionStoreConfig, confirmed to touch no filesystem state. open_production_store()creates a store on first open; reopens an existing one; rejects unsafe parent directory, symlinked store path, malformed existing file, and unsafe key file — reusingSqliteRecoveryContractStore's andkey_lifecycle's own existing, tested checks, not duplicating them.- Missing parent directory now fails closed with
ContractValidationError, not a rawFileNotFoundError(tests/tier1/test_store.py:: test_store_rejects_nonexistent_parent_directory_cleanly, new regression test). provision_production_anchor_baseline()seeds and marks complete; refuses a second call; two independently configured stores cannot cross-contaminate each other's provisioning state.AnchorProvisioningStatus/anchor_provisioning_status()/ProvisioningRecord.read(): before any provisioning, after full provisioning, and in the seeded-but-not-complete intermediate state; corrupted marker fails closed on read (tests/tier1/ test_anti_rollback.py).scripts/tier1_store_bootstrap.py: status reporting for unconfigured/ configured-but-unprovisioned/provisioned states, creating nothing in status mode;--provisionrequires both--handleand--yes-i-understand; a full provision-then-status-then-refused-second- call cycle (tests/test_tier1_store_bootstrap.py).- Structural: the script cannot import anything capable of reaching
pfSense and calls no forbidden attribute name; no production
application file imports this mechanism
(
tests/test_tier1_store_bootstrap_isolation.py).
Activation requirements¶
- [x] Implemented and tested per "Required tests" above.
- [ ] The deferred production-wiring decision (see above) — its own, separate, future authorization, informed by but not granted by this spec.
- [ ] An operational runbook for initial deployment (creating the parent
directories with correct ownership/mode, generating the integrity
key via
key_lifecycle.py's existing tooling) — documentation, not code, not yet written.
Implementation checklist¶
- [x]
src/pfsense_mcp/tier1/production_store.py:ProductionStoreConfig,load_production_store_config(),open_production_store(),provision_production_anchor_baseline(). - [x]
Tier1ConfigurationErroradded toerrors.py. - [x]
SqliteRecoveryContractStore.anchor_provisioning_status()andProvisioningRecord.read()(read-only status accessors). - [x]
store.py::_prepare_path()'s missing-parent-directory case fixed to fail closed withContractValidationErrorinstead of a rawFileNotFoundError. - [x]
scripts/tier1_store_bootstrap.pyoperator CLI. - [x]
tests/tier1/test_production_store.py,tests/test_tier1_store_bootstrap.py,tests/test_tier1_store_bootstrap_isolation.py, plus additions totests/tier1/test_anti_rollback.py/test_store.py.
Review checklist¶
- [ ] Confirm no committed test fixture or default value anywhere in this
slice uses the real TPM baseline (
2) or the real NV handle (0x01500000) as a literal — only synthetic values. - [ ] Confirm
load_production_store_config()truly performs zero filesystem I/O (grep foros./Path.exists/Path.statcalls inside it — there should be none beyondPath()construction itself).
Security checklist¶
- [ ] No key material or TPM secret is ever read, printed, or logged by
scripts/tier1_store_bootstrap.pyin either mode. - [ ]
tests/test_tier1_store_bootstrap_isolation.pypasses: the script cannot importrest_api_client/transport/tools/write_api_client/pfsense_client, and calls no forbidden attribute name. - [ ]
tests/tier1/test_isolation.py:: test_tier1_is_not_imported_outside_its_inert_packagestill passes unchanged — no production application file importspfsense_mcp.tier1.
Test checklist¶
- [ ] Every failure mode in the table above has a dedicated test.
- [ ]
make quick/make validateboth green. - [ ] Full pytest suite green; 42-tool public MCP contract byte-identical; WRITE 0/3 active; WRITE allow-list empty.