Production signing runbook (compliance evidence)
Audience. The release approver / operator cutting a real (non-lab) release who must turn the compliance-evidence signing surfaces from the lab’s clearly-marked dev-stable software seed into a sovereign-provider-provisioned production key, and publish the matching verification key so air-gapped operators verify evidence fully offline.
Claim boundary (read first). The lab signs with a software dev-stable key that is not a secret and is clearly marked as such — it exists only so the golden-path demo produces reproducible signatures. This runbook is operator support: it tells you how to provision real signing. It does not make Stella (or your estate) “NIS2/CRA/DORA compliant” or eIDAS-qualified — the operator remains the regulated party, and “qualified” (QES) is an explicit, separate opt-in (§6).
On-prem first (non-negotiable). The production key backend is a sovereign provider — HashiCorp Vault (flagship KEK/secret backend), an in-process PKCS#11 HSM, or a software sovereign provider plugin (
eu.eidas.soft,fips.ecdsa.soft, GOST, SM). A cloud-managed KMS (AWS/Azure/GCP) is never a default and is only ever reachable as an explicit, operator-selectedProviderOverride— the selection routine never picks one for you (SignatureProviderSelection.Resolvereturns a non-nullProviderHintonly when you pass an explicit override). See MEMORYfeedback_on_prem_first_no_cloud_defaults.
This runbook is the production-signing on-ramp referenced from the release checklist. It consolidates the scattered “Production hardening” notes into one authoritative procedure for the two compliance-evidence signing surfaces:
| # | Surface | Signs | Purpose | Live lab default | Verification key |
|---|---|---|---|---|---|
| A | EvidenceLocker decision-capsule seal | DSSE Ed25519 envelope over the capsule manifest | capsule-seal | bouncycastle.ed25519 + dev-stable seed | JWKS endpoint (§4) |
| B | ExportCenter NIS2 SoA export | DSSE ES256 bundle (Statement of Applicability) | soa-export | in-process DefaultCryptoProvider ES256 + dev-stable PEM | embedded trust root (§5) |
Everything below is a configuration / key-provisioning change. No signer code changes between the lab and production — that is the entire point of the provider-selection routine.
1) How a signer chooses its algorithm + provider
Both surfaces resolve their signing algorithm and provider through one routine:
src/__Libraries/StellaOps.Cryptography/SignaturePurpose.cs— the well-known purposes (capsule-seal,soa-export,evidence-bundle,verdict-attestation,regulatory-payload,generic-dsse).src/__Libraries/StellaOps.Cryptography/SignatureProviderSelection.cs— the routine:SignatureProviderSelection.Resolve( new SignatureSelectionRequest( Purpose: SignaturePurpose.CapsuleSeal, // or SoaExport ProfileId: "<active compliance profile>", // optional AlgorithmOverride: "<configured Signing:Algorithm>",// optional ProviderOverride: "<configured Signing:Provider>"))// optional => SignatureSelection(AlgorithmId, ProviderHint?)
A call site requests a signer by purpose. The routine resolves the canonical algorithm for (profile + purpose) (honouring any explicit override), and the ICryptoProviderRegistry then resolves the concrete provider from the active profile’s preferred order. So swapping the lab software key for a sovereign provider/HSM is a selection/config change, never a code change.
1.1 Three distinct config inputs (do not conflate)
These are three separate keys with three separate jobs (verified in src/):
| Input | Config key (env form) | Picks | Source |
|---|---|---|---|
| Compliance profile | Crypto:Compliance:ProfileId (STELLAOPS_CRYPTO_COMPLIANCE_PROFILE); world/fips/gost/sm/kcmvp/eidas | the algorithm family for a purpose | CryptoComplianceOptions.cs (SectionKey = "Crypto:Compliance", env override in ApplyEnvironmentOverrides) |
| Registry active profile / preferred providers | StellaOps:Crypto:Registry:ActiveProfile + :PreferredProviders | which ICryptoProvider signs | CryptoRegistryProfiles.cs |
| Regional bundle | STELLAOPS_CRYPTO_PROFILE (default/china/russia/eu) | the regional compose overlay (which provider gates + profile env it sets) | devops/compose/docker-compose.compliance-{eu,china,russia}.yml (default in devops/compose/.env) |
STELLAOPS_CRYPTO_PROFILE(regional bundle:default/china/russia/eu) is not the same asSTELLAOPS_CRYPTO_COMPLIANCE_PROFILE(algorithm family:world/fips/gost/sm/kcmvp/eidas). The regional compose overlay typically sets the compliance profile for you (e.g. theeuoverlay), but they are distinct keys — verify both when wiring a region.
1.2 Per-purpose algorithm defaults (behaviour-preserving)
From SignatureProviderSelection.cs (WorldDefaults + the regional switch):
| Purpose | world / international / kcmvp | eidas | fips | gost | sm | pq |
|---|---|---|---|---|---|---|
capsule-seal | Ed25519 | ES256 | ES256 | GOST12-256 | SM2 | Dilithium3 |
soa-export (+ all others) | ES256 | ES256 | ES256 | GOST12-256 | SM2 | Dilithium3 |
So under the default/world profile the lab seals capsules with Ed25519 and signs the SoA with ES256 — exactly what the live compose sets. Switching to a regional profile changes the algorithm family without touching signer code.
1.3 Provider ids and their gates (verified in src/)
The software sovereign providers are gated off by default and must be explicitly allowed (do not enable a profile you are not certifying). Verified against src/__Libraries/StellaOps.Cryptography/CompliancePolicyCryptoProviders.cs:
| Provider id | Env gate | Algorithms | Notes |
|---|---|---|---|
bouncycastle.ed25519 | (always available) | Ed25519 | dev / international default; lab capsule signer |
eu.eidas.soft | EIDAS_SOFT_ALLOWED=1 | ES256 / ES384 | real ECDSA; software-only, non-certified, QSCD not enforced |
fips.ecdsa.soft | FIPS_SOFT_ALLOWED=1 | ES256 / ES384 / ES512 | software-only, non-certified |
cn.sm.soft | SM_SOFT_ALLOWED=1 | SM2 | software-only; remote variant via src/SmRemote/ |
| GOST (OpenSSL) | STELLAOPS_CRYPTO_ENABLE_RU_OPENSSL (default on Linux) | GOST12-256/512 | regional |
| PQ soft | PQ_SOFT_ALLOWED=1 | Dilithium3 / Falcon512 | post-quantum |
| HSM (PKCS#11) | provider-plugin specific | per device | in-process; key never leaves the device |
sim.crypto.remote | STELLAOPS_ALLOW_UNSAFE_CRYPTO_SIM=1 | HMAC sim labels | TEST-ONLY; rejected by production signing — never a release-evidence authority |
These software providers are non-certified (no QSCD / FIPS module enforced). Making any of them “qualified eIDAS” / certified-FIPS is a separate decision (§6).
2) Surface A — EvidenceLocker decision-capsule key
2.1 What the lab does today
The live stack (devops/compose/docker-compose.stella-services.yml, evidence-locker-web and evidence-locker-worker) seals capsules with a stable Ed25519 key on the bouncycastle.ed25519 provider, selected through the routine but seeded from a dev-stable base64 seed in config:
EvidenceLocker__Signing__Enabled: "true"
EvidenceLocker__Signing__UseCryptoRegistry: "true" # routes through the abstraction
EvidenceLocker__Signing__Algorithm: "ED25519"
EvidenceLocker__Signing__KeyId: "evidence-locker-capsule-key"
EvidenceLocker__Signing__Provider: "bouncycastle.ed25519"
EvidenceLocker__Signing__AllowEphemeralKeyMaterial: "false" # no fresh-key fallback
# DEV-STABLE seed — NOT a secret, lab/golden-path only:
EvidenceLocker__Signing__KeyMaterial__Ed25519PrivateKeyBase64: "${STELLAOPS_EVIDENCELOCKER_CAPSULE_ED25519_SEED_B64:-REDACTED-ROTATED-2026-07-30}"
Config keys verified in EvidenceLockerOptions.cs (SigningOptions / SigningKeyMaterialOptions). The signer is CryptoRegistryCapsuleSigner (src/EvidenceLocker/StellaOps.EvidenceLocker/Capsules/CryptoRegistryCapsuleSigner.cs): it calls SignatureProviderSelection.Resolve(CapsuleSeal, …) with Signing:Algorithm and Signing:Provider as explicit overrides, then asks the registry for the key by KeyId. In the lab the key is provisioned in-process from the stable seed (LoadSigningKeyMaterial); in production the same KeyId is pre-provisioned on the sovereign provider, so ResolveSigner succeeds first and LoadSigningKeyMaterial is never reached.
2.2 Production wiring
- Keep
UseCryptoRegistry=true. The legacy raw-crypto capsule signers (Ed25519CapsuleSigner/EcdsaCapsuleSigner) bypass the abstraction and are reachable only whenUseCryptoRegistry=falseAND raw key material is configured. Production deployments MUST keepUseCryptoRegistry=trueso no compliance signing path bypasses the abstraction (see signing-provider-selection.md §3). - Pick the profile + provider for your region (§1.1, §1.3): set the compliance profile, enable the chosen provider’s env gate, and put it first in the registry preferred order. For the
euprofile useeu.eidas.soft(EIDAS_SOFT_ALLOWED=1); forfipsusefips.ecdsa.soft(FIPS_SOFT_ALLOWED=1); etc. - Provision the key on the provider — not raw bytes in config. Pre-provision the
evidence-locker-capsule-keyKeyIdon the chosen provider/HSM (Vault transit or PKCS#11). PointEvidenceLocker:Signing:KeyIdat it and (optionally) pin the provider withEvidenceLocker:Signing:Provider. LeaveProviderempty to let the registry pick from the profile order. - Remove the dev-stable seed and keep the ephemeral fallback off:
- Delete
EvidenceLocker__Signing__KeyMaterial__Ed25519PrivateKeyBase64(do not ship the lab seed to production). EvidenceLocker__Signing__AllowEphemeralKeyMaterial: "false"— already false in the lab; keep it false. With no inline seed and no ephemeral fallback, the signer requires the key to be pre-provisioned on the provider (it will throw on a missing key rather than silently mint a fresh one that would break offline verify).EvidenceLocker__Signing__AllowUnsignedCapsules: "false"(or omit). The production startup gate refuses unsigned capsules (§2.3).
- Delete
- Set the algorithm to match the profile (or leave it pinned). Under a regional profile the routine fills the per-purpose default; if you pin
Signing:Algorithm, the registry validates the resolved provider supports it.
2.3 The production startup gate
EvidenceLockerRuntimeConfigurationValidator (verified in …/Infrastructure/Hosting/) fails fast on non-local-harness startup unless the capsule signer has durable key material. ValidateCapsuleSigner passes when any of: UseCryptoRegistry=true, an Ed25519 seed is configured, an EC PEM is configured, or AllowEphemeralKeyMaterial=true. ValidateAllowUnsignedCapsules throws if AllowUnsignedCapsules=true outside the local harness. The local-harness escape (EvidenceLocker:LocalHarness:Enabled=true) only takes effect in Development/Testing (or the TestingLocalHarness environment) — a production host ignores it.
Production gotcha. Because
UseCryptoRegistry=truealone satisfies the gate, the validator will not catch a missing provider key — it only guards against the NullCapsuleSigner. The real safety net isAllowEphemeralKeyMaterial=false(no fresh key) plus the published JWKS (§4): if the provider key is absent, the signer throws on sign and the JWKS endpoint returns503rather than silently signing with the wrong key. Verify §7 round-trips before you sign release evidence.
3) Surface B — ExportCenter NIS2 SoA export key
3.1 What the lab does today
The live stack signs the NIS2 SoA bundle with a stable ES256 key provisioned on an in-process DefaultCryptoProvider from a dev-stable EC P-256 PEM.
Host and file moved 2026-09-13 (SPRINT_20260722_025 OK-10). These keys are carried by
offlinekit-webindevops/compose/docker-compose.offlinekit-consolidated.yml, not byexport-webindocker-compose.stella-services.yml— verified at HEAD: nineteenExport__Nis2Soa__*keys in the former, zero in the latter, and theexport-webcontainer is stopped. One key also changed shape: the overlay setsExport__Nis2Soa__SigningPrivateKeyPemFile(a mounted path) rather than the inlineExport__Nis2Soa__SigningPrivateKeyPemshown below, so the §2.12 half of the hardening step in §7.2 is already taken.
Export__Nis2Soa__AuthorityBaseUrl: "https://stella-ops.local" # the GATEWAY, not the internal authority alias
Export__Nis2Soa__StorageRoot: "/var/lib/stella/exportcenter/nis2-soa"
Export__Nis2Soa__SigningKeyId: "nis2-soa-export-key"
Export__Nis2Soa__SigningAlgorithmId: "ES256"
Export__Nis2Soa__SigningProviderHint: "default"
Export__Nis2Soa__ExpectedSignerKeyId: "nis2-soa-export-key"
Export__Nis2Soa__SigningPrivateKeyPem: | # DEV-STABLE — lab only
-----BEGIN PRIVATE KEY-----
…
-----END PRIVATE KEY-----
Export__Nis2Soa__TrustRootMode: "AirGap"
Export__Nis2Soa__TrustRoots__0__TrustRootId: "stellaops-nis2-soa-dev-stable-2026"
Export__Nis2Soa__TrustRoots__0__KeyId: "nis2-soa-export-key"
Export__Nis2Soa__TrustRoots__0__PublicKeyPem: |
-----BEGIN PUBLIC KEY-----
…
-----END PUBLIC KEY-----
Export__Nis2Soa__TrustRoots__0__Fingerprint: "sha256:8940…5459"
Export__Nis2Soa__TrustRoots__0__NotBeforeUtc: "2026-01-01T00:00:00Z"
Export__Nis2Soa__TrustRoots__0__NotAfterUtc: "2027-01-01T00:00:00Z"
Export__Nis2Soa__TrustRoots__0__SignerProfiles__0: "tenant-regulatory-export"
Config keys verified in Nis2SoaExportOptions.cs. The live signed run is POST /v1/exports/nis2/soa/runs (required scope export:operate, verified in ExportSurfacingClient.cs / ExportSurfacingClientTests.cs). The registry + key provisioning happens in AddNis2SoaExportServices (Nis2SoaExportServiceCollectionExtensions.cs): it provisions the SigningKeyId from SigningPrivateKeyPem only when no ICryptoProviderRegistry is already registered — i.e. the inline PEM is purely the lab/dev-stable path.
3.2 Production wiring
- Pre-register a sovereign-provider registry.
AddNis2SoaExportServicesexplicitly skips provisioning from the PEM when anICryptoProviderRegistryis already registered (verified:if (services.Any(… == typeof(ICryptoProviderRegistry))) return;). In production register your sovereign-provider registry (Vault / PKCS#11 HSM /eu.eidas.soft/fips.ecdsa.soft) so it owns the key. - Provision the
nis2-soa-export-keyKeyIdon that provider and leaveExport__Nis2Soa__SigningPrivateKeyPemunset (delete the inline PEM). The SoA signer (Nis2SoaCryptoProviderSigner) resolves the key by id through the abstraction either way. - Set
SigningAlgorithmIdto the algorithm your provider exposes (ES256 by default; the EU offline verifier supports ECDSA/RSA trust roots, not Ed25519 — keep an EC curve for the SoA surface). SetSigningProviderHintto the production provider id (e.g.eu.eidas.soft) instead ofdefault. - Publish the real trust root. Replace the dev-stable
TrustRoots__0block with the production key’s public-key PEM + sha256Fingerprint+ validity window + the signer profile (tenant-regulatory-export). KeepTrustRootMode: "AirGap"so the bundle carries the trust root inline and verifies offline (§5). - Keep
ExpectedSignerKeyIdaligned withSigningKeyIdso a run that signs with the wrong key fails verification. - Authority read path (unchanged for production):
AuthorityBaseUrlmust point at the gateway, andExport__Nis2Soa__ServiceAccount__*supplies a client-credentials account (authority:tenants.read) so ExportCenter can mint its own token to read the tenant compliance profile when the run arrives over the messaging transport (the caller’s bearer is a signed identity envelope, not a forwardable header). Replace the lab’s seededstellaops-cli-automationclient/secret with a production service account.
4) Surface A verification key — capsule JWKS (offline verify)
The capsule-seal public key is published as JWKS so an air-gapped operator verifies a sealed capsule’s DSSE signature fully offline:
- Endpoint:
GET /api/v1/evidence/capsules/signing-keys/jwks(anonymous — a public verification key is by definition distributable; no private material is exposed). Verified inCapsuleSigningKeyEndpoints.cs(JwksPath). - How it stays correct across providers: the endpoint resolves the signer through the same
SignatureProviderSelection.Resolve(CapsuleSeal, …)selection as signing, then callsICryptoSigner.ExportPublicJsonWebKey(). So whichever sovereign provider the routine selects (devbouncycastle.ed25519in the lab; HSM / Vault /eu.eidas.soft/fipsin production), the publishedx(andcrv/alg/kid) always matches the key that signs. You do not republish anything when you swap providers — the endpoint re-derives the public JWK from the active provider. - Honest degradation: if
UseCryptoRegistry=false,KeyIdis unset, or the provider key is not yet provisioned, the endpoint returns503(capsule_signing_key_unavailable) instead of a 500 — the offline-verify contract degrades honestly rather than publishing a wrong key.
Air-gap offline-verify procedure (operator):
- Fetch the JWKS once from a connected boundary:
GET …/signing-keys/jwks→ save thekeys[0]public JWK (kty/crv/x/kid/alg). - Export the sealed capsule:
POST /api/v1/evidence/capsules/{id}/export→ a zip withmanifest.json,manifest.dsse.json, … - Offline: recompute the canonical content hash from
manifest.json, then DSSE-verify the Ed25519 signature inmanifest.dsse.jsonagainst the publishedxpublic key — no call back into the stack. Capsule DSSE signatures are reproducible across restarts for a fixed input + key, so this is a genuine air-gap deliverable.
5) Surface B verification key — SoA embedded trust root (offline verify)
The NIS2 SoA bundle carries its trust root inline (TrustRootMode: "AirGap"), so it verifies offline with no JWKS fetch:
The signed run returns
verification.isValid=truewithverifiedKeyId,verifiedTrustRootId, andverifiedTrustRootFingerprint.The
stellaCLI verifies the bundle offline:dotnet src/Cli/StellaOps.Cli/bin/Debug/net10.0/StellaOps.Cli.dll \ export nis2-soa --since <date> \ --snapshot snapshot.json --tenant-profile tenant-profile.json \ --signing-key-file <key> --signing-key-id nis2-soa-export-key \ --output nis2-soa-bundle.json --overwrite --json # -> "offlineVerified": trueIn production, the
TrustRoots__0__PublicKeyPem+Fingerprintyou publish (§3.2) is the production provider’s public key — the bundle remains byte-identical across runs for a fixed input + key.
6) eIDAS QES (qualified) opt-in posture
eu.eidas.soft performs genuine ECDSA ES256/ES384 signing but is software-only, non-certified, QSCD not enforced. Under the eidas compliance profile the routine resolves ES256 for every purpose and the registry routes it to the first preferred provider — so the soft production on-ramp for the eu profile is three settings, no code change (see eidas-sim-crypto-demo.md §eIDAS production-signing on-ramp):
STELLAOPS_CRYPTO_COMPLIANCE_PROFILE=eidas(algorithm family → ES256/ES384).EIDAS_SOFT_ALLOWED=1and puteu.eidas.softfirst in the registry preferred order.- Provision the signing
KeyIdoneu.eidas.soft(Vault-wrapped / PKCS#11) and point the signer’sKeyId/Providerat it — no raw private bytes in app config.
soft → qualified (QES). Making it a qualified eIDAS seal is a separate, deliberate step (intentionally not built speculatively). To upgrade — still no signer code change, because every signer only requests purpose → ES256 + a KeyId:
- Swap
eu.eidas.softfor a QTSP/QSCD-backed provider (smart card / HSM) registered under the same registry preferred order. - Flip
Compliance.StrictValidation=trueandCompliance.EnforceJurisdiction=trueinetc/appsettings.crypto.eu.yaml(today both are off with aTODO: Replace with eidas.soft plugin when available). - Supply qualified-trust evidence: QSCD attestation, QTSP qualified timestamps (RFC 3161 over a qualified TSA), TSL/LOTL trust list (offline
eu-lotl.xmlfor air-gap), OCSP/CRL, and AdES (XAdES/PAdES/CAdES) packaging — per ETSI EN 319 412-1/2, ETSI TS 119 312, and the eIDAS QSCD/QTSP Bridge Contract.
Until a customer requires QES, the eu profile signs with the soft provider through the routine (production posture for the software lane) while the qualified posture stays an explicit, documented opt-in.
7) Before / after config diff (lab dev-stable → production sovereign)
7.1 EvidenceLocker capsule (evidence-locker-web + evidence-locker-worker)
EvidenceLocker__Signing__Enabled: "true"
EvidenceLocker__Signing__UseCryptoRegistry: "true"
EvidenceLocker__Signing__KeyId: "evidence-locker-capsule-key"
EvidenceLocker__Signing__AllowEphemeralKeyMaterial: "false"
- # DEV-STABLE seed — lab/golden-path only:
- EvidenceLocker__Signing__Algorithm: "ED25519"
- EvidenceLocker__Signing__Provider: "bouncycastle.ed25519"
- EvidenceLocker__Signing__KeyMaterial__Ed25519PrivateKeyBase64: "${STELLAOPS_EVIDENCELOCKER_CAPSULE_ED25519_SEED_B64:-4OtFcrPU+…NP3s=}"
+ # PRODUCTION: key pre-provisioned on the sovereign provider; no inline seed.
+ # Algorithm follows the active compliance profile (e.g. ES256 under eidas/fips);
+ # pin it only if you must. Provider = your sovereign provider id.
+ EvidenceLocker__Signing__Algorithm: "ES256"
+ EvidenceLocker__Signing__Provider: "eu.eidas.soft" # or fips.ecdsa.soft / an HSM provider
+# (provision the KeyId on the provider; e.g. EIDAS_SOFT_ALLOWED=1 + registry preferred order)
+# STELLAOPS_CRYPTO_COMPLIANCE_PROFILE: "eidas"
Keep
AllowEphemeralKeyMaterial=false. Removing the inline seed with the fallback off forces the key to come from the provider — the correct production failure mode. Under the default/world profile (capsule-seal → Ed25519) you would instead keepAlgorithm: "ED25519"and provision an Ed25519KeyIdon the sovereign provider; the JWKS endpoint then publishes that provider’s Ed25519 public key unchanged.
7.2 ExportCenter NIS2 SoA (offlinekit-web, formerly export-web)
Export__Nis2Soa__SigningKeyId: "nis2-soa-export-key"
Export__Nis2Soa__SigningAlgorithmId: "ES256"
Export__Nis2Soa__ExpectedSignerKeyId: "nis2-soa-export-key"
Export__Nis2Soa__TrustRootMode: "AirGap"
- Export__Nis2Soa__SigningProviderHint: "default"
- # DEV-STABLE inline PEM — lab only; AddNis2SoaExportServices provisions it
- # in-process when no ICryptoProviderRegistry is already registered:
- Export__Nis2Soa__SigningPrivateKeyPem: |
- -----BEGIN PRIVATE KEY-----
- …
- -----END PRIVATE KEY-----
- Export__Nis2Soa__TrustRoots__0__TrustRootId: "stellaops-nis2-soa-dev-stable-2026"
- Export__Nis2Soa__TrustRoots__0__PublicKeyPem: |
- -----BEGIN PUBLIC KEY----- # dev-stable public key
- …
- -----END PUBLIC KEY-----
- Export__Nis2Soa__TrustRoots__0__Fingerprint: "sha256:8940…5459"
+ # PRODUCTION: register a sovereign-provider crypto registry (Vault / PKCS#11 /
+ # eu.eidas.soft) that OWNS nis2-soa-export-key; leave SigningPrivateKeyPem UNSET so
+ # AddNis2SoaExportServices skips inline provisioning (registry-present short-circuit).
+ Export__Nis2Soa__SigningProviderHint: "eu.eidas.soft" # or fips.ecdsa.soft / HSM
+ Export__Nis2Soa__TrustRoots__0__TrustRootId: "<your-prod-trust-root-id>"
+ Export__Nis2Soa__TrustRoots__0__PublicKeyPem: |
+ -----BEGIN PUBLIC KEY----- # the PRODUCTION provider public key
+ …
+ -----END PUBLIC KEY-----
+ Export__Nis2Soa__TrustRoots__0__Fingerprint: "sha256:<production-key-fingerprint>"
Export__Nis2Soa__TrustRoots__0__NotBeforeUtc: "<prod validity start>"
Export__Nis2Soa__TrustRoots__0__NotAfterUtc: "<prod validity end>"
Export__Nis2Soa__TrustRoots__0__SignerProfiles__0: "tenant-regulatory-export"
+ # Replace the seeded lab service account with a production one:
+ Export__Nis2Soa__ServiceAccount__ClientId: "<prod-export-service-account>"
+ Export__Nis2Soa__ServiceAccount__ClientSecret: "<prod-secret>"
8) Production preconditions checklist
Re-verify these per surface before approving a release that produces signed evidence. (Source of truth for each item: src/ paths cited above.)
- [ ] Profiles set for the region.
STELLAOPS_CRYPTO_COMPLIANCE_PROFILE(algorithm family) + registryActiveProfile/PreferredProviders; regional compose overlay (STELLAOPS_CRYPTO_PROFILE) if used. All three reconciled (§1.1). - [ ] Chosen provider’s env gate enabled (
EIDAS_SOFT_ALLOWED/FIPS_SOFT_ALLOWED/SM_SOFT_ALLOWED/ GOST / PQ / HSM) and the provider appears first in the registry preferred order.sim.crypto.remoteis not enabled. - [ ] No cloud-managed KMS configured as a default — sovereign / Vault / HSM only.
- [ ] Capsule key
evidence-locker-capsule-keyprovisioned on the provider;UseCryptoRegistry=true; inlineEd25519PrivateKeyBase64removed;AllowEphemeralKeyMaterial=false;AllowUnsignedCapsules=false. - [ ] SoA key
nis2-soa-export-keyowned by a pre-registered sovereign registry;SigningPrivateKeyPemunset; productionTrustRoots__0(PEM + fingerprint + validity + signer profile) published;ExpectedSignerKeyIdaligned. - [ ] JWKS
GET /api/v1/evidence/capsules/signing-keys/jwksreturns200and thekeys[0].xmatches the provider’s public key (not the lab seed-derived key). - [ ] Round-trip both surfaces: seal a capsule and verify it fully offline against the JWKS key (no service call); run
POST /v1/exports/nis2/soa/runsand confirmverification.isValid=true+ offline CLI verify green. - [ ] eIDAS posture explicit (soft vs qualified per §6); if QES is claimed, the
StrictValidation/EnforceJurisdictionflags and QTSP/QSCD/TSL evidence are in place — otherwise the bundle is not presented as qualified.
9) Related docs
- Release checklist — Production signing
- Compliance signing: provider-selection routine & production handoff
- Compliance Golden-Path Demo — the residual “Production hardening” rows formalized here.
- eIDAS sim-crypto round-trip demo + production on-ramp
- Crypto profile configuration — region/profile selection + the
stella crypto profiles select <profile>CLI surface. - Cryptography module architecture — abstraction & purpose model.
