Runbook: Policy Engine - Policy Storage Backend Down
For platform on-call. Use this when Stella Ops policy operations fail with storage errors, the Policy Engine crash-loops on startup migrations, or Doctor reports storage_status: FAILED. The Policy Engine persists exclusively to a PostgreSQL policy schema (with a Valkey cache) — there is no MongoDB store and no second backend to fail over to — so “storage unavailable” almost always means PostgreSQL on the Policy DSN is unreachable, misconfigured, or out of capacity. Triage it like any other Stella service whose Postgres is down.
Sprint: SPRINT_20260117_029_DOCS_runbook_coverage Task: RUN-003 - Policy Engine Runbooks
Reconciliation note (verify against
src/Policy): The original draft of this runbook described a generic “policy storage backend” with a primary/backup failover model, an OPA bundle loader, and a large family ofstella policy storage …/stella db …commands. That does not match the implementation. The Policy Engine (src/Policy/StellaOps.Policy.Engine) persists exclusively to a PostgreSQL schema namedpolicy(plus apolicy_approle schema), owned byStellaOps.Policy.Persistenceand applied via forward-only startup migrations (AddStartupMigrations("policy", "Policy.Persistence", …)wired inPolicyPersistenceExtensions; schema name forced topolicyinPolicyDataSource.DefaultSchemaName). There is no MongoDB store, no second “backup storage” to fail over to, and OPA is not the storage backend — OPA is only an optional Rego interop/attestation-gate surface (seepolicy-opa-crash.md). EPSS/risk score rows are durable in the Policy DSN underpolicy.*. The evaluation/overlay/exception caches go to a pluggable distributed cache (IDistributedCache<T>;MessagingPolicyEvaluationCachesupports InMemory, Valkey, or PostgreSQL transports) — Valkey is the default service cache per the module README (src/Policy/README.md: “Valkey for cache”). Only when the messaging Postgres transport is selected do those caches land in PostgreSQL, where the transport names themmessaging.cache_<name>(default schemamessaging; seePostgresCacheStore.cs). So “policy storage unavailable” almost always means PostgreSQL on the Policy DSN is unreachable, misconfigured, or out of capacity — handle it like any other Stella service whose Postgres is down.Commands, alerts, and check IDs that do not exist in
src/are flagged inline as NOT IMPLEMENTED. Verified sources are listed under Related Resources.
Metadata
| Field | Value |
|---|---|
| Component | Policy Engine (PostgreSQL policy schema + Valkey cache) |
| Severity | Critical |
| On-call scope | Platform team |
| Last updated | 2026-01-17 (reconciled against src/Policy) |
| Doctor check | check.policy.engine (source: src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Policy/Checks/PolicyEngineHealthCheck.cs) |
Flag (corrected): The original
check.policy.storage-healthDoctor check does not exist. The Policy Engine plugin registers exactly one check,check.policy.engine. Its evidence includes astorage_statussub-result (OK/FAILED) and astorage_errorfield, so it is the verified way to observe Policy storage health from Doctor. (Note: that check probes a policy-engine URL with OPA-style endpoints —/v1/datafor the storage probe — so it reflects the engine answering, not a direct Postgres ping. Usestella db statusfor the database view.)
Symptoms
- [ ] Policy operations failing with “storage unavailable” / “failed to connect to policy store” / “database connection refused” in
policy-enginelogs - [ ] Startup migrations fail or the service crash-loops on boot (
AddStartupMigrationscannot reach PostgreSQL on the Policy DSN) - [ ] Policy pack create/activate/publish, exception writes, or risk-score persistence fail; reads of already-compiled packs may still work from cache
- [ ]
stella doctor --check check.policy.enginereports Fail withstorage_status: FAILED(and astorage_error) - [ ]
stella db statusreports the database disconnected or migrations not up-to-date
Flag (NOT IMPLEMENTED): The original symptom “Alert
PolicyStorageUnavailablefiring” references an alert that is not defined anywhere insrc/ordevops/. Do not page on it. If you want alerting, wire it against PostgreSQL availability for the Policy DSN (your Postgres monitoring) and/or thecheck.policy.engineDoctor result — not a Stella-emittedPolicyStorage*series.
Flag (corrected): “OPA unable to load bundles from storage” does not apply to the core engine. Policy packs are reconstructed by recompiling the persisted DSL source from PostgreSQL (
policy.packs/policy.pack_versions) on reload — there is no OPA bundle store in the storage path. OPA bundle loading is a separate interop concern covered bypolicy-opa-crash.md.
Impact
| Impact Type | Description |
|---|---|
| User-facing | Policy authoring/lifecycle writes (pack create/activate/publish, exceptions, receipts) fail while Postgres is unreachable. Evaluation of already-activated packs can continue from recompiled-source/IR and cached EPSS/risk rows until those need refresh. |
| Data integrity | No corruption from an outage alone: writes fail loudly rather than persisting partially. Risk is operating on stale cached EPSS/risk values (rows carry expires_at, default NOW() + 7 days) if the outage is prolonged. |
| SLA impact | Policy management is blocked; release gates that require a fresh policy evaluation or exception write will block (or fail-open per their own gate config) until storage recovers. |
Diagnosis
Quick checks
Check Doctor diagnostics (verified check ID):
stella doctor --check check.policy.engineLook for
storage_status,storage_error, andengine_urlin the evidence. Astorage_status: FAILEDpoints here; aconnection_error_type: refused/timeouton the whole check means the engine itself is unreachable.Check database connectivity from the CLI:
stella db status # add --verbose for the connection-pool view, --format json for scriptingstella db statusqueriesGET /api/v1/health/databaseand reports connection state, database type/version, schema/migration status, and (with--verbose) pool counters. Source:src/Cli/StellaOps.Cli/Commands/DbCommandGroup.cs.Caveat (verify against
DbCommandGroup.cs): if the API call itself fails (HttpRequestException), the CLI falls back to a synthetic “healthy” status (GenerateSyntheticStatus(), lines ~138/155) rather than surfacing the failure. A clean “Connected / up-to-date” fromstella db statuswhen the service is clearly down may be this fallback, not a real probe. Cross-check with the service health endpoint and container logs before trusting a green result.Inspect the Policy Engine service logs with your container/host tooling (there is no Stella log subcommand — see flag below):
docker logs --since 30m stellaops-policy-engine # or: journalctl -u <policy-engine-unit> --since '30 min ago'Look for migration errors, Npgsql connection failures, or pool-exhaustion timeouts on the
Postgres:PolicyDSN.
Flag (NOT IMPLEMENTED):
stella policy storage status,stella db status --component policy, andstella db status --database policydo not exist.stella db statustakes only--format,--server, and--verbose(no--component/--database); it reports the platform database health, not a per-module store.stella policy logs --filter storage …is also not a command — use container/host logs.
Deep diagnosis
Confirm the Policy DSN and pool settings. The engine binds
Postgres:Policy(PostgresOptions):ConnectionString,MaxPoolSize,MinPoolSize,CommandTimeoutSeconds,ConnectionIdleLifetimeSeconds,Pooling,AutoMigrate. The schema is forced topolicyregardless of the shared options slot (PolicyDataSource.CreateOptions). Verify the connection string resolves and the role can reach thepolicyschema.Check PostgreSQL itself with standard Postgres tooling against the Policy DSN host (server up? accepting connections? disk full? too many connections? role/permissions intact?). This is ordinary Postgres triage — there is no Stella-specific ping/pool command (see flag).
Check migration state.
stella db statusreportsmigrationStatusandpendingMigrations. A non-up-to-datestatus with the service crash-looping on boot indicates the startup migrator could not applyMigrations/001_initial_schema.sql … 015_*.sql.001_initial_schema.sqlis idempotent (CREATE … IF NOT EXISTS) and converges on reused volumes, so a genuine failure here is usually connectivity/permissions, not a half-applied schema.
Flag (NOT IMPLEMENTED):
stella db ping --database policy,stella db pool-status --database policy, andstella policy storage disk-usagedo not exist. Usestella db status --verbosefor pool counters and your Postgres/host tooling for ping and disk usage.
Resolution
Immediate mitigation
Restore PostgreSQL on the Policy DSN. The Policy Engine has a single durable store; there is no second backend to fail over to. Bring Postgres back (restart the database, free connections, free disk) — that is the mitigation.
Flag (NOT IMPLEMENTED):
stella policy storage failover --to backupand a “switch to backup storage” model do not exist. There is no configured backup store; recovery is restoring the primary PostgreSQL instance (HA/replica promotion, if you run one, is handled at the PostgreSQL layer, not via Stella).Let cached evaluation carry read traffic while Postgres recovers. Activated packs evaluate from recompiled source/IR and cached EPSS/risk rows; you do not need a read-only toggle to keep evaluating already-active policy.
Flag (NOT IMPLEMENTED):
stella policy config set storage.read_only trueandstella policy reloadare not commands. There is no globalstorage.read_onlyconfig key. (Pack reload happens automatically on engine restart by recompiling persisted DSL source.)Restart the Policy Engine to reconnect once Postgres is healthy, using your normal deployment tooling:
docker restart stellaops-policy-engineOn boot the engine re-runs startup migrations (idempotent) and re-establishes the connection pool.
Flag (NOT IMPLEMENTED):
stella service restart policy-engineis not a CLI command. Restart thestellaops-policy-enginecontainer/service with your orchestration tooling (compose/systemd/etc.).
Root cause fix
If it is a database connection / pool issue:
Inspect status and pool counters:
stella db status --verboseTune pooling via configuration, not a CLI mutation. Adjust
Postgres:Policy(MaxPoolSize,MinPoolSize,CommandTimeoutSeconds) in the Policy Engine configuration and restart the service to apply.Flag (NOT IMPLEMENTED):
stella db pool-restart --database policyandstella db config set policy.max_connections 50are not commands. Pool sizing is configuration (Postgres:Policy.MaxPoolSize) applied on restart; Postgres-sidemax_connectionsis tuned on PostgreSQL itself.
If disk space is exhausted (on the PostgreSQL host):
Free space on the database host with standard PostgreSQL/OS tooling (
VACUUM/pg_repackas appropriate, log/WAL cleanup, volume growth). Policy tables of interest are under thepolicyschema.Flag (NOT IMPLEMENTED):
stella policy storage disk-usage [--verbose]andstella policy versions cleanup --older-than 30ddo not exist. Pack version retention is managed through the verified lifecycle commands (stella policy rollback,stella policy history) and at the database/data layer — there is noversions cleanupcommand.
If the schema/migrations are out of step:
Confirm
stella db statusshows the expectedmigrationStatus. If the service could not apply migrations because of permissions, grant the role access to thepolicyschema and restart soAddStartupMigrationscan converge. Platform policy is forward-only (see ADR-004); recover a bad downgrade by PostgreSQL snapshot restore (see the migration-recovery runbook), not by hand-editing tables.Flag (NOT IMPLEMENTED):
stella policy storage verifyandstella policy storage restore --from-backup latestdo not exist. There is no Stella-side storage verify/restore for the Policy schema; integrity recovery is PostgreSQL backup/restore at the database layer.
Verification
# Database reports connected and migrations up-to-date
stella db status --verbose
# Doctor's Policy check passes (storage_status: OK in evidence)
stella doctor --check check.policy.engine
# A real write path works end to end (lifecycle, exercises Postgres):
stella policy publish <args> # or: stella policy activate <args>
# No fresh storage errors in the service log
docker logs --since 10m stellaops-policy-engine
Flag (NOT IMPLEMENTED):
stella policy storage status,stella policy storage test-write, andstella policy update --testare not CLI commands. Verify storage viastella db status+ thecheck.policy.enginestorage_statusevidence, and exercise a real write with a verified lifecycle subcommand (publish/activate/submit). The verified, registeredstella policysubcommands (fromCommandFactory.BuildPolicyCommandplusPolicyCommandGroup.AddPolicyPackCommandsandPolicyInteropCommandGroup.RegisterSubcommands) are:simulate, activate, lint, edit, test, new, history, explain, init, compile, version, submit, review, publish, rollback, sign, verify-signature, lattice, verdicts, promote(core),validate-yaml, install, list-packs(pack), andexport, import, validate, evaluate(interop).Flag (corrected):
findingsis not astella policysubcommand — it is a top-level command (stella findings,CommandFactory.BuildFindingsCommand). Flag (NOT IMPLEMENTED / dead code):stella policy push,pull,export-bundle, andimport-bundleare defined inPolicyCommandGroup.AddDistributionCommandsbut that method is never called, so those four subcommands are not actually wired into the CLI. Do not rely on them for verification.
Prevention
- [ ] Monitoring: Alert on PostgreSQL availability for the Policy DSN (your Postgres monitoring) and on
check.policy.enginestorage_status; there is no built-inPolicyStorageUnavailablealert to enable. - [ ] Capacity: Monitor the PostgreSQL host disk and connection count; size
Postgres:Policy.MaxPoolSizeto fitmax_connectionsacross all services on the instance. - [ ] Backups: Take regular PostgreSQL backups/snapshots of the Policy database; recovery is database-layer restore (forward-only migrations, ADR-004), not an application-level “restore from backup” command.
- [ ] Cache hygiene: EPSS/risk rows carry
expires_at(default 7 days); ensure feeds refresh them so a storage blip does not leave evaluation on stale scores. - [ ] Migration convergence: Treat boot-time migration failures as connectivity/ permission problems first;
001_initial_schema.sqlis idempotent and converges on reused volumes.
Related Resources
- Architecture:
docs/modules/policy/architecture.md(storage, schema, caches) - Related runbooks:
policy-opa-crash.md,postgres-ops.md,policy-compilation-failed.md,policy-version-mismatch.md,policy-incident.md - Migration policy / recovery: ADR-004 forward-only migrations, migration-recovery runbook
- Doctor check:
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Policy/Checks/PolicyEngineHealthCheck.cs - Persistence + migrations:
src/Policy/__Libraries/StellaOps.Policy.Persistence/Extensions/PolicyPersistenceExtensions.cs,…/Postgres/PolicyDataSource.cs,…/Migrations/001_initial_schema.sql - CLI surfaces:
stella db status(src/Cli/StellaOps.Cli/Commands/DbCommandGroup.cs),stella policy …(CommandFactory.BuildPolicyCommand+PolicyCommandGroup.cs) - Policy scopes:
src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs(policy:read,policy:write,policy:publish,policy:activate,policy:operate, …)
Flag (corrected): The original draft linked Architecture:
docs/modules/policy/storage.mdand Database setup:docs/operations/database-configuration.md. Neither file exists. Policy storage is documented indocs/modules/policy/architecture.md; database/migration operations are covered bypostgres-ops.md, ADR-004, and the migration-recovery runbook.
