Compliance Golden-Path Demo
A single, reproducible, signed walk through the full compliance evidence journey — the SE/auditor demo that makes Stella’s “signed evidence / deterministic replay / fail-closed” claims tangible, and the regression check that keeps “compliance works” green as services change.
Claim boundary — read this first. This demo proves operator-support: Stella produces signed, replayable compliance evidence. It does not make Stella (or your estate) “NIS2/CRA/DORA compliant”. The operator/customer remains the regulated and reporting party. Every pack runs
evidence-only / operator-support; the demo never claims “compliant-in-a-box”.
Audience: solutions engineers and auditors running the live walkthrough, and maintainers who keep the regression lane green. For the product framing and canonical claim-boundary wording, start at Assurance and Compliance Packs.
What it walks
| Step | Action | Evidences (regulation) |
|---|---|---|
| 1 | Enable the NIS2 evidence-only pack | tenant opts into NIS2 evidence support (operator-owned config) |
| 2 | Author a NIS2 control + applicability | NIS2 Art. 21 risk-management control register (Policy-owned, canonical mappingHash) |
| 3 | Scan the demo image | software composition input for the SBOM/findings chain |
| 4 | Upload + round-trip the SBOM | CRA Annex I / NIS2 Art. 21(2)(d) software bill of materials, content-addressed |
| 5 | Create a release + digest-pinned component | supply-chain integrity — every component carries its sha256 (deploy-by-digest, not tag) |
| 6 | Record a release-evidence packet | DORA evidence retention — content-hashed, verifiable release truth |
| 7 | Create + seal a decision capsule | DORA / NIS2 signed, replayable decision evidence; attestor.verdict_ledger increments; DSSE Ed25519 signature verifies |
| 8 | SoA snapshot + signed SoA export | NIS2 Art. 21 Statement of Applicability — deterministic, signed, offline-verifiable bundle |
| 9 | Drive an Article 23 incident timeline | NIS2 Art. 23 early-warning (24h) / notification (72h) / final-report (1 month) milestones, signed + replayable, source-of-truth = Notify |
Run it (headless — one command)
# Linux / WSL / git-bash
bash devops/compose/scripts/compliance-golden-path.sh \
--evidence-dir docs/qa/feature-checks/runs/$(date +%F)-compliance-golden-path/evidence/run-1
# Windows
pwsh devops/compose/scripts/compliance-golden-path.ps1 `
-EvidenceDir docs/qa/feature-checks/runs/2026-05-30-compliance-golden-path/evidence/run-1
The script:
- mints an admin token (client-grantable scopes; auto-unlocks the admin user if a concurrent QA session locked it),
- reuses the proven idempotent
compliance-setup.shto enable the NIS2 pack, - asserts each step’s live HTTP/DB state and exits non-zero on any failure,
- is safe to re-run (idempotent — re-submitting the same scan/SBOM/control/ snapshot converges to the same canonical hashes),
- uses the internal registry alias (
registry:5000, never127.1.1.5), - writes per-step JSON + sha256 hashes to the evidence dir (when given).
Expected tail on a healthy stack:
[golden-path] ===== SUMMARY =====
[golden-path] passed assertions: 25
[golden-path] warnings: 3
[golden-path] FAILED assertions: 0
[golden-path] GOLDEN PATH OK — all assertions passed (3 warning(s)).
(The 3 warnings are the SoA live-run config gap — see Residual gaps.)
Signed artifacts + offline verification
Two signed artifacts are produced and verified:
Sealed decision capsule (step 7) — a DSSE Ed25519 envelope.
- Server-side verification:
POST /api/v1/evidence/capsules/{id}/verifyreturnssignatureValid:true+contentHashMatches:true. - Self-contained export bundle:
POST /api/v1/evidence/capsules/{id}/export→ a zip withmanifest.json,manifest.dsse.json,assembly-inputs.json,metadata.json. - Fully-offline verify (SPRINT_20260530_024): the capsule signing key is now a STABLE Ed25519 key selected through the
SignatureProviderSelectionroutine (EvidenceLocker__Signing__AllowEphemeralKeyMaterial=false+KeyMaterial__Ed25519PrivateKeyBase64dev-stable seed; in production the sameKeyIdis pre-provisioned on a sovereign provider — HSM / Vault-wrapped / eidas.soft / fips). The matching public key is published as JWKS atGET /api/v1/evidence/capsules/signing-keys/jwks(anonymous), so an air-gapped operator can DSSE-verify the sealed capsule fully offline (recompute the canonical content hash, then verify the Ed25519 signature in the exportedmanifest.dsse.jsonagainst the publishedxpublic key) without contacting the stack. Capsule DSSE signatures are now reproducible across restarts for a fixed input + key.
- Server-side verification:
NIS2 SoA bundle (step 8b) — a deterministic DSSE bundle generated and verified offline by the
stellaCLI:dotnet src/Cli/StellaOps.Cli/bin/Debug/net10.0/StellaOps.Cli.dll \ export nis2-soa --since 2026-05-01 \ --snapshot snapshot.json --tenant-profile tenant-profile.json \ --signing-key-file soa-hmac.key --signing-key-id <id> \ --output nis2-soa-bundle.json --overwrite --json # -> "offlineVerified": true (the CLI verifies the DSSE envelope offline)The bundle is byte-identical across runs for a fixed input + key — a genuine air-gap deliverable. The golden-path script runs this automatically when a built
StellaOps.Cli.dllis present.
Console walk-through (operator narrative)
The Console authoring UI for the NIS2 control register shipped in SPRINT_20260530_020: a compliance officer authors/edits/removes controls and publishes the SoA snapshot entirely from /assurance/nis2/control-register. The page shows a 13-area completeness indicator and blocks “Publish SoA snapshot” (listing the missing areas) until every thematic area carries an applicability decision. The publish action drives the Policy-owned POST /api/policy/control-register/nis2/snapshot endpoint (scope policy:operate, gated by the same SoA completeness rule) and surfaces the returned contentHash / soaExportHash. This Policy control:snapshot_published path is distinct from — and complementary to — the ExportCenter signed SoA run (step 8); the headless script above exercises the same chain via the API and does not depend on the UI.
Determinism boundaries
- STABLE (assert): image manifest digest, control
mappingHash, SoAbundleHash/soaExportHash/controlRegisterSnapshotHash, Article 23 timelinereplayHash. Two clean runs over the frozen fixture produce identical values. - REPRODUCIBLE (SPRINT_20260530_024): the decision-capsule DSSE signature is now reproducible across restarts (stable signing key) for a fixed canonical payload — it is no longer a per-run ephemeral value. The SoA bundle signature is likewise reproducible once the live ExportCenter SoA signing key is configured. The golden-path script still asserts canonical hashes, not raw signature bytes, so this change does not alter any assertion.
- VARIES (do not assert byte-for-byte): decision-capsule
capsuleId/sealedAtand all wall-clock timestamps (contentHashis a deterministic function of the canonical payload and is stable for a fixed input).
Full boundary table + frozen-fixture values: docs/qa/feature-checks/runs/2026-05-30-compliance-golden-path/README.md.
Residual gaps (honest)
All three “Production hardening” follow-ups below are consolidated into one authoritative operator runbook:
docs/ops/production-signing-runbook.md(per-surface provisioning, the dev-stable → sovereign before/after config diff, JWKS / trust-root offline-verify, and the eIDAS QES opt-in). The release checklist links it from its Production signing section.
| Gap | Impact | Follow-up |
|---|---|---|
export-web now carries Export:Nis2Soa:{AuthorityBaseUrl,SigningKeyId,SigningPrivateKeyPem,TrustRoots,StorageRoot}; the live POST /v1/exports/nis2/soa/runs produces a signed, offline-verifiable bundle. Set GP_REQUIRE_SOA_RUN=1 to assert it. | — | Production hardening (runbook §3): drop the dev-stable SigningPrivateKeyPem and pre-provision SigningKeyId on a sovereign provider (HSM / Vault-wrapped / eidas.soft), supplying the real trust-root chain. |
SignatureProviderSelection, and the public key is published via JWKS (GET /api/v1/evidence/capsules/signing-keys/jwks), so the sealed capsule verifies fully offline. | — | Production hardening (runbook §2): replace the dev-stable seed with a sovereign-provider-provisioned key (HSM / Vault-wrapped / eidas.soft / fips); the JWKS endpoint then publishes that provider’s public key unchanged. |
| Real HSM / QTSP (QES) backend not present in the lab. | The lab uses a software dev-stable key (clearly marked); the pattern (stable key via the routine + published verification key) is production-shaped. | Provision the signing KeyId on an HSM / Vault-wrapped / eu.eidas.soft provider and flip the eIDAS qualified posture — a selection/config change, not code (runbook §6; eidas-sim-crypto-demo.md §eIDAS production-signing on-ramp). |
CI regression lane (TASK-021-5)
The journey is wired as a CI lane — .gitea/workflows/compliance-golden-path.yml(“Compliance Golden Path”) — so “compliance still works” is guarded as services change. The lane runs the script unattended against the live stack and gates the job on the script’s exit code: a non-zero exit names the broken leg (pack → author → scan → SBOM → release → sealed capsule → SoA export → Article 23 timeline). The per-step JSON + hashes.txt evidence is uploaded as a workflow artifact (compliance-golden-path-<run_id>) via the script’s --evidence-dir.
Key lane decisions:
- Runner
apparmor-host— the script needs a real Docker daemon (DB assertions viadocker exec stellaops-postgres, admin-unlock, and the stella-ops compose stack). The default Gitea container runners have no Docker. - Triggers:
workflow_dispatch+ nightlyschedule(04:15 UTC), NOT a blockingpull_requestgate. The lane stands up the fulldocker-compose.stella-ops.ymlstack (50+ services) on the shared ~12 GBapparmor-host, which idles near its memory ceiling; standing the whole platform up on every PR risks an OOM (same rationale asfe-e2e-tests.yml). Promote to a scopedpull_requestgate once the host has headroom / a dedicated runner. - Canonical bring-up, no second stack — the lane runs a convergent
docker compose up -dfromdevops/compose(a no-op when the shared stack is already healthy), waits on gateway + Postgres health like the other live-e2e lanes, and does not tear the stack down (the script is idempotent). GP_REQUIRE_SOA_RUN=1by default — step 8’s live signed SoA run is no longer config-gated: SPRINT_20260530_024 wired the ExportCenterExport__Nis2Soa__*production-signing config intodocker-compose.stella-services.yml(part of the default stack), and aGP_REQUIRE_SOA_RUN=1run is recorded green (30 assertions / 0 warnings / 0 failures) indocs/qa/feature-checks/runs/2026-05-30-s024-production-signing/README.md. So a SoA regression now also fails the lane. Theworkflow_dispatchrequire_soa_runinput drops it back to0for a stack that has not (yet) received the 024 SoA config.
To run it locally / unattended outside CI, invoke the script directly with an --evidence-dir (see Run it above); a non-zero exit is the same signal the lane gates on.
