Runbook: Policy Engine - OPA Evaluation Failure
For platform on-call. Use this when an OPA-backed evaluation fails in Stella Ops — a Rego attestation gate erroring out, or embedded-OPA Rego interop returning a subprocess failure. Read the reconciliation note first: Stella’s Policy Engine is not an OPA deployment. OPA appears only in two narrow, optional, currently-unwired surfaces (an embedded opa subprocess for Rego interop, and an external attestation-gate adapter). Native DSL/IR policy evaluation — the default path — is unaffected by any OPA outage. If neither OPA surface appears in your logs, this is the wrong runbook: see Evaluation latency high or Compilation errors.
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 assumed the StellaOps Policy Engine is an Open Policy Agent (OPA) deployment whose long-runningopaprocess can crash. That is not how the engine works. The core Policy Engine (src/Policy/StellaOps.Policy.Engine) is a first-party .NET service with its own DSL (StellaOps.PolicyDsl), IR compiler, and deterministic evaluator (PolicyRuntimeEvaluator). It does not embed or manage a persistent OPA server, and no OPA container/sidecar ships indevops/compose. OPA appears only in two narrow, optional roles:
- Embedded OPA binary (offline Rego interop) —
StellaOps.Policy.Interop.Evaluation.EmbeddedOpaEvaluatorshells out to a bundledopa/opa.exebinary as a short-lived subprocess for Rego bundle/source evaluation andopa checksyntax validation. It is not a daemon; it auto-discovers the binary and falls back gracefully when the binary is absent. A “crash” here is a non-zero subprocess exit code or a 30s timeout, not a downed service.Verified (dormant surface):
EmbeddedOpaEvaluator/IEmbeddedOpaEvaluatorare referenced only inside theStellaOps.Policy.Interoplibrary (the class, its interface, and a DI stub). Its DI registrarAddEmbeddedOpaEvaluatoris an empty stub (“Implementation registered in TASK-05” — body justreturn services;), so nothing registers or resolves it, and no shipping CLI command or service invokes it. Thestella policy validate / evaluate / importcommands do not route through it (see below). Treat this surface as present-but-unwired today; a subprocess “crash” only manifests if custom code constructsEmbeddedOpaEvaluatordirectly.- External OPA gate (attestation verification) —
StellaOps.Policy.Gates.Opa.OpaGateAdapter+HttpOpaClientcan call an operator-provided external OPA HTTP server (defaulthttp://localhost:8181) to evaluate a Rego attestation gate. Stella does not ship or supervise this server.Verified (library capability, not wired):
OpaGateAdapter,HttpOpaClient,OpaClientOptions, andOpaGateOptionsare referenced only within their own definition files inStellaOps.Policy(plus that library’s tests). There is no DI registration, noOpaClientOptions/OpaGateOptionsconfiguration binding, and no gate-registry wiring anywhere insrc/outside tests. The gate is a building block that requires custom operator/integrator wiring; it is not on any release-blocking path by default.This runbook has been rewritten to reflect those two real surfaces. Commands, metrics, alerts, and check IDs that do not exist in
src/are flagged inline as NOT IMPLEMENTED. Where the original draft attributed CLI commands to components they do not actually call, the attribution is corrected inline.
Metadata
| Field | Value |
|---|---|
| Component | Policy Engine (OPA interop / attestation gate) |
| Severity | High (Critical only when an OPA attestation gate is on the release-blocking path) |
| 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.opa-healthDoctor check does not exist. The Policy Engine plugin registers exactly one check,check.policy.engine, which probes the configured policy-engine URL (Policy:Engine:UrlorPolicyEngine:BaseUrl, defaulthttp://localhost:8181) using OPA-style endpoints (/health,/v1/policies,/v1/data,/v1/status) and recordsengine_typein its evidence.
Symptoms
- [ ] Attestation/Rego gate evaluations failing with “OPA evaluation error: …” or “HTTP error: …” / “OPA returned
: …” ( OpaGateAdapter/HttpOpaClientlogs — applies only where an integrator has wired the OPA gate) - [ ] Code that uses
EmbeddedOpaEvaluatordirectly returns “OPA binary not found.” or “OPA evaluation failed (exit N):” / “OPA bundle evaluation failed (exit N): ”, or throws TimeoutException(“OPA process timed out after 30s”). NB: the stockstella policy validate / evaluate / importcommands do not produce these — they never callEmbeddedOpaEvaluator. - [ ]
stella doctor --check check.policy.enginereports Fail withconnection_error_type: refused/timeoutagainst the engine URL - [ ] Release gates that include an OPA-backed gate flip to fail-closed (when
OpaGateOptions.FailOnError = true) or silently pass-open (whenfalse)
Flag (NOT IMPLEMENTED): The original symptoms referenced an alert
PolicyOPACrashedand a metricpolicy_opa_restarts_total. Neither exists anywhere insrc/. Do not page on them. If you need restart/health alerting for an external OPA server, it must be defined against that external deployment’s own metrics, not against a Stella-emitted series.
Impact
| Impact Type | Description |
|---|---|
| User-facing | Only flows that depend on OPA interop are affected, and only if an operator has explicitly wired them (neither surface ships wired — see the reconciliation note): (a) custom code that constructs EmbeddedOpaEvaluator for offline Rego evaluation, and (b) any release gate an integrator has wired through OpaGateAdapter. The stock CLI interop commands (stella policy validate / evaluate / import) do not touch OPA — they use the native JSON importer and native gate evaluation. Native DSL/IR policy evaluation (the default path) is unaffected by an OPA outage. |
| Data integrity | No data loss. Subprocess and HTTP-gate failures are surfaced as errors/denials; nothing is persisted incorrectly. |
| SLA impact | Only relevant where an integrator has wired an OPA attestation gate. If such a gate sits on the release-blocking path with FailOnError = true (the default for OpaGateOptions), releases through that gate block until OPA recovers. With FailOnError = false the gate passes open (evidence quality degrades but the pipeline proceeds). |
Diagnosis
Quick checks
Check Doctor diagnostics (verified check ID):
stella doctor --check check.policy.engineLook for:
engine_url,engine_type,connection_error_type,compilation_status/evaluation_status/storage_statusin the evidence.Confirm which OPA surface is involved. Read the relevant logs and identify the failing component:
EmbeddedOpaEvaluator→ offline Rego subprocess, only from custom code that constructs it (not reachable from the stock CLI).OpaGateAdapter/HttpOpaClient→ external OPA HTTP server, only where an integrator has wired the gate. These have different fixes; do not conflate them. If neither component appears in logs, the failure is almost certainly in native DSL/IR evaluation, not OPA — use the sibling runbooks (policy-evaluation-slow.md,policy-compilation-failed.md).
Flag (NOT IMPLEMENTED):
stella policy statusandstella policy opa logs(used by the original draft) are not CLI commands. Verifiedstella policysubcommands are:simulate, activate, lint, edit, test, new, history, explain, init, compile, version, submit, review, publish, rollback, sign, verify-signature, lattice, verdicts, promote, validate-yaml, install, list-packs, push, pull, export-bundle, import-bundle, export, import, validate, evaluate. (Confirmed againstCommandFactory.BuildPolicyCommand,PolicyCommandGroup, andPolicyInteropCommandGroup.) For service logs, use your container/host log tooling (e.g.docker logs,journalctl) against thepolicy-enginecontainer.
Deep diagnosis (external OPA gate)
Reach the external OPA server directly at the configured URL (
OpaClientOptions.BaseUrl, defaulthttp://localhost:8181):curl -s http://localhost:8181/healthHttpOpaClient.HealthCheckAsyncperforms exactly thisGET /health.Inspect the gate configuration (
OpaGateOptions):PolicyPath(defaultstella/policy/allow),FailOnError,TrustedKeyIds. AFailOnError = truegate is the one that blocks releases on OPA outage.Check the request timeout.
OpaClientOptions.TimeoutSecondsdefaults to 10s;HttpOpaClientreturns “Request timed out after N seconds” when exceeded.
Deep diagnosis (embedded OPA binary)
Scope note (corrected):
EmbeddedOpaEvaluatoris not invoked by any shipping CLI command or service (its DI registrar is an empty stub). This section therefore applies only to custom code that constructsEmbeddedOpaEvaluatordirectly. The stockstella policy validate / evaluate / importcommands do not exercise it, so they cannot be used to reproduce an embedded-OPA failure.
Verify the binary is discoverable.
EmbeddedOpaEvaluator.DiscoverOpaBinarysearches, in order:<app>/tools/opa[.exe],<app>/opa[.exe],~/.stella/tools/opa[.exe], then eachPATHentry (the first existing file wins).IsAvailableAsyncreturnsfalsewhen none resolves, and the evaluator returns “OPA binary not found.” for every operation.Reproduce in the calling code, not via the CLI. When
EmbeddedOpaEvaluatoris invoked directly, a non-zero subprocess exit surfaces as “OPA evaluation failed (exit N):” (or “OPA bundle evaluation failed (exit N): ” for EvaluateBundleAsync), carrying the raw OPA stderr; aTimeoutExceptionafter the 30s default indicates a runaway/large evaluation. The underlying commands it runs areopa eval --data … --input … <query> --format json,opa eval --bundle … --input … <query> --format json, andopa check <file> --format json.
Flag (NOT IMPLEMENTED):
stella policy stats --opa-metrics,stella policy list --last-error,stella policy opa config show, andstella policy analyze --detect-loopsdo not exist. The 30s subprocess timeout inEmbeddedOpaEvaluator(the constructor default; configurable only via itstimeoutconstructor argument) is the only “infinite loop” guard; there is no loop-detection command.
Resolution
Immediate mitigation
External OPA gate down (release-blocking path):
Recover the external OPA server (restart it via whatever supervises it — it is operator-managed, not a Stella container). Stella does not expose a
stella policy opa restartcommand (NOT IMPLEMENTED).If the gate must not block while OPA is down, set the gate’s
FailOnError = falsesoOpaGateAdapterpasses open on OPA errors, then redeploy/reload the Policy Engine configuration through your normal config path.Warning: Passing an attestation gate open degrades evidence quality. Only do this if compliance allows it, and re-enable
FailOnError = trueonce OPA recovers.
Embedded OPA binary missing/failing:
- Provide the binary at one of the discovered locations (e.g.
<app>/tools/opa[.exe]or onPATH). The interop path falls back gracefully when absent — native DSL/IR evaluation continues to work without it, so this only blocks Rego import/validate/evaluate.
Flag (NOT IMPLEMENTED):
stella policy opa start --safe-mode,stella policy config set failopen true, andstella policy reloaddo not exist. Fail-open behaviour is the per-gateOpaGateOptions.FailOnErrorflag, not a globalfailopenconfig key.
Root cause fix
If the external OPA server was OOM-killed or overloaded:
Size and tune the external OPA deployment per its own operational docs (OPA memory limits and GC are configured on the OPA process, not via Stella). Stella exposes none of
stella policy opa config set memory_limit|gc_*(NOT IMPLEMENTED).Reduce gate evaluation cost: trim the input payload built by
OpaGateAdapter.BuildOpaInput(merge result, context, supply-chain evidence) and the Rego atOpaGateOptions.PolicyPath.
If a Rego policy itself is faulty (interop path):
Validate the policy with the verified interop command (note: this uses
--file, not a positional argument):stella policy validate --file <policy.rego>Flag (corrected):
stella policy validatedoes not runopa checkand does not callEmbeddedOpaEvaluator.ValidateSyntaxAsync. It runsJsonPolicyImporter.ImportFromStringAsyncwithValidateOnly = true(structural validation against the PolicyPack v2 schema, with format auto-detection viaFormatDetector). For a.regofile it validates the importer’s mapping of the Rego, not the Rego’s own syntax via OPA. If you need genuineopa checksyntax validation, run theopabinary directly (opa check <file>).Fix the policy, then re-validate and re-import (both use
--file):stella policy validate --file <fixed-policy.rego> stella policy import --file <fixed-policy.rego> # JsonPolicyImporter (see note)Flag (corrected):
stella policy importtakes--file/-f(required), not a positional path. It also does not useRegoPolicyImporter: the command constructs aJsonPolicyImporterfor both the JSON and Rego branches and reports the mapping. (RegoPolicyImporterexists as a library class but is not the type theimportcommand instantiates.) Use--validate-onlyor--dry-runto avoid persisting.
Flag (NOT IMPLEMENTED):
stella policy list --status error,stella policy disable/enable/update <policy-id>are not CLI commands. Policy lifecycle (draft → submit → review → publish → activate → rollback) is managed through the verifiedstella policy submit|review|publish|activate|rollbacksubcommands, notdisable/enable.
If a policy-pack bundle failed to load (interop path):
- Re-export/re-import the offline bundle with the verified commands:
stella policy export-bundle --policy <file> --output bundle.tar.gz stella policy import-bundle --bundle bundle.tar.gz --verifyCaution (corrected): these commands operate on Stella policy-pack bundles — a gzip stream of
FILE:<path>:<len>records plus anindex.jsonmanifest (ExportBundleAsync/ImportBundleAsync). This is not an OPA bundle. The unrelatedEmbeddedOpaEvaluator.EvaluateBundleAsync(opa eval --bundle …) consumes an OPAbundle.tar.gzand is not invoked by any CLI command. Do not feed an OPA bundle toimport-bundleor vice versa.
Flag (NOT IMPLEMENTED):
stella policy bundle verify|build|loadare not CLI commands; the verified bundle commands areexport-bundle/import-bundle(and the OCIpush/pull).
If the configuration is wrong:
- Verify the engine/gate URL Stella is actually using:
- Doctor reads
Policy:Engine:UrlorPolicyEngine:BaseUrl(defaulthttp://localhost:8181). - The gate client reads
OpaClientOptions.BaseUrl(sectionOpa, defaulthttp://localhost:8181). Correct these in the Policy Engine configuration and restart the service via your normal deployment path.
- Doctor reads
Flag (NOT IMPLEMENTED):
stella policy opa config resetandstella policy opa config set workers|decision_log ...do not exist.
Verification
# Re-run the verified Doctor check
stella doctor --check check.policy.engine
# (External gate) confirm the OPA server answers its health endpoint
curl -s http://localhost:8181/health
# (Interop) confirm policy validate/evaluate now succeed (note option syntax;
# these run the native importer + native gate evaluation, NOT OPA)
stella policy validate --file <policy.rego>
stella policy evaluate --policy <policy-file> --input <evidence.json>
# Confirm native DSL/IR evaluation is unaffected (independent of OPA)
stella policy test <policy-dsl-file>
Flag (corrected / NOT IMPLEMENTED):
stella policy validateandstella policy evaluateuse--file/--policy+--inputoptions (not positional arguments) and exercise the native JSON importer and nativeEvaluateGatelogic — they are not OPA health probes.stella policy status,stella policy opa health,stella policy evaluate --test, andstella policy stats --watchare not CLI commands. Usecheck.policy.enginefor engine health andstella policy testfor DSL fixture coverage.
Prevention
- [ ] Architecture clarity: Treat OPA as an optional interop/gate surface, not the core engine. Native DSL/IR evaluation has no OPA dependency.
- [ ] External OPA monitoring: If you run an external OPA server for the attestation gate, monitor it on its own metrics/health (Stella emits no
policy_opa_*series). - [ ] Gate fail-mode review: Decide per gate whether
OpaGateOptions.FailOnErrorshould betrue(fail-closed, blocks releases) orfalse(fail-open) and document the choice for compliance. - [ ] Binary provisioning (only if you wire
EmbeddedOpaEvaluatoryourself): the stock CLI never calls the embedded evaluator, so the bundledopabinary is not required for normal use. If custom code constructsEmbeddedOpaEvaluator, place the binary at a discovered path (<app>/tools/opa[.exe],<app>/opa[.exe],~/.stella/tools/opa[.exe], or onPATH); otherwise it degrades to “OPA binary not found.” - [ ] Validation in CI: Run
stella policy validate --file <policy>before import (structural PolicyPack v2 validation via the native importer — notopa check). For true Rego syntax checking, runopa check <file>against theopabinary directly.
Related Resources
- Architecture:
docs/modules/policy/architecture.md - Related runbooks:
policy-evaluation-slow.md,policy-compilation-failed.md,policy-storage-unavailable.md,policy-version-mismatch.md,policy-incident.md - Doctor check:
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Policy/Checks/PolicyEngineHealthCheck.cs - Embedded OPA evaluator:
src/Policy/__Libraries/StellaOps.Policy.Interop/Evaluation/EmbeddedOpaEvaluator.cs - External OPA gate:
src/Policy/__Libraries/StellaOps.Policy/Gates/Opa/OpaGateAdapter.cs,HttpOpaClient.cs - Native evaluator (no OPA):
src/Policy/StellaOps.Policy.Engine/Services/PolicyRuntimeEvaluator.cs - Policy scopes:
src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs(policy:read,policy:simulate,policy:publish,policy:activate,policy:operate, …) - OPA documentation (external project): https://www.openpolicyagent.org/docs/latest/
