Runbook: Attestor - Signature Generation Failures
Purpose: diagnose and clear failures of the Attestor sign endpoint (POST /api/v1/attestations:sign) so the release pipeline can produce new signed evidence again. Audience: Platform and Security on-call engineers responding to a blocked release, rising signing-failure metrics, or a failing key-material Doctor check.
Reconciled against source 2026-05-31. The Doctor check IDs, metrics, alerts, and CLI commands below were corrected to match
src/Attestorandsrc/Doctor/__Plugins/StellaOps.Doctor.Plugin.Attestor. Several commands the original draft referenced (stella attest test-sign,stella attest config set,stella attest retry,stella keys generate/activate,stella crypto hsm ...) do not exist in the CLI and have been removed or replaced. See “Verified facts” notes.Caveats verified in source (do not treat as fully live):
stella doctor run --plugin <id>is not a real flag —doctor/doctor runaccept--check,--category,--tag,--mode, etc., but no--plugin(src/Cli/StellaOps.Cli/Commands/DoctorCommandGroup.csCreateRunOptions). Use--tag attestationor--category Securityto run the Attestor group.check.attestation.keymaterial(SigningKeyExpirationCheck) is defined but is not registered inAttestorDoctorPlugin.GetChecks(), so it does not run via the Attestor Doctor plugin today. Onlycheck.attestation.cosign.keymaterial(+ the three Rekor checks and the transparency-consistency check) are wired.- The former unregistered
stella keys list|status|rotate|auditsample-data group was deleted on 2026-07-29.stella crypto keys generateremains a separate stub, andstella crypto profile validatetargets a Platform endpoint that returnsendpoint_not_implemented(exit 0) until RP-028-013 lands. These are forward-looking verbs; verify key health againstattestor.signingconfig + the sign endpoint, not these commands’ output.
Metadata
| Field | Value |
|---|---|
| Component | Attestor |
| Severity | Critical |
| On-call scope | Platform team, Security team |
| Last updated | 2026-05-31 |
| Doctor checks | check.attestation.cosign.keymaterial (registered, Fail). check.attestation.keymaterial exists in source but is not registered in the Attestor plugin (see Caveats). |
Symptoms
- [ ]
POST /api/v1/attestations:signreturns HTTP 400 with acodeextension (signing_failed,key_not_found,provider_route_drift,provider_key_drift,provider_algorithm_drift,payload_invalid_base64, …) - [ ] Release pipeline blocked because attestations cannot be signed
- [ ] Metric
attestor.sign_total{result="failure"}increasing (and/orattestor.errors_total{type="sign"}) - [ ] Alert
AttestorSignLatencyP95Highfiring (signing degraded/timing out) - [ ] Doctor check
check.attestation.cosign.keymaterialreporting Fail (key material missing/unreadable) - [ ] Service log:
Unexpected error while signing attestation.followed by the inner exception
Verified facts (source ground truth):
- There is no
attestor_signing_failures_totalmetric and noAttestorSigningFailedalert. Signing failures surface throughattestor.sign_total{result="failure"}(src/Attestor/.../Infrastructure/Signing/AttestorSigningService.csRecordFailureMetrics) and, for unexpected internal errors,attestor.errors_total{type="sign"}. The shipped attestor alerts areAttestorSignLatencyP95High,AttestorVerifyLatencyP95High,AttestorVerifyFailureRate, andAttestorKeyRotationStale(devops/telemetry/alerts/attestation-alerts.yaml).- The sign endpoint returns failures as RFC7807 problems with a
codeextension; the code values are defined inAttestorSigningExceptionusages (src/Attestor/.../Infrastructure/Signing/AttestorSigningService.cs,.../AttestorSigningKeyRegistry.cs).
Impact
| Impact Type | Description |
|---|---|
| User-facing | Releases blocked; new attestations cannot be signed |
| Data integrity | Existing attestations remain valid; only new signing is affected |
| SLA impact | Release SLO violated; evidence chain cannot advance until signing recovers |
Diagnosis
Quick checks
Run the Doctor key-material check:
stella doctor --check check.attestation.cosign.keymaterialProblem if: result is Fail (e.g. “Signing key file not found”, “Signing mode is ‘file’ but KeyPath not configured”, “Signing mode is ‘kms’ but KmsKeyRef not configured”, or “Unknown signing mode”).
Run the full Attestor diagnostic group (filter by tag or category — there is no
--pluginflag):stella doctor run --tag attestation # or, broader: stella doctor run --category SecurityThe Attestor plugin (
stellaops.doctor.attestor, categorySecurity) registerscheck.attestation.cosign.keymaterialplus the Rekor connectivity / clock-skew / verification-job and transparency-log-consistency checks (AttestorDoctorPlugin.GetChecks()). The key-expiration checkcheck.attestation.keymaterialis not registered today (see Caveats), so it will not appear in this run.Inspect configured signing keys through the Attestor API/configuration.
The former unregistered
stella keyssample-data group was deleted on 2026-07-29; it never represented live key state. The source of truth is theattestor.signing.keys[]configuration plus the live sign endpoint in Deep diagnosis below. The registeredstella keyrotation group also fails closed until its service client is configured.
Deep diagnosis
Reproduce the failure against the sign endpoint to capture the exact error
code:curl -sS --cert client.pem --key client.key \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -X POST https://attestor.internal:8444/api/v1/attestations:sign \ -d '{"keyId":"<key-id>","payloadType":"application/vnd.in-toto+json","payload":"<base64>","artifact":{"sha256":"<digest>","kind":"image"}}'The response problem body carries
code. Map it to a cause:codeMeaning (source) key_missingRequest omitted keyId(AttestorSigningService/AttestorSigningKeyRegistry).key_not_foundkeyIdis not present inattestor.signing.keys[].payload_missing/payload_type_missing/payload_invalid_base64Malformed request body. provider_route_driftResolved crypto provider differs from the key’s configured provider.provider_key_driftResolved provider key id differs from the key’s providerKeyId.provider_algorithm_driftResolved algorithm differs from the key’s configured algorithm.signing_failedUnexpected internal error (provider unreachable, key material unloadable); see logs. Inspect the configured key route in
attestor.signing(mode, provider, providerKeyId, algorithm): the drift codes above mean the running crypto provider no longer matches the static configuration — typically a provider/key/algorithm change that was applied to the crypto provider but not toattestor.signing.keys[](or vice-versa).Check signing-key expiration (separate Doctor check
check.attestation.keymaterial,SigningKeyExpirationCheck, default severity Warn):stella doctor --check check.attestation.keymaterialBy design it fails when a key is past expiry or within 7 days (
CriticalDays) and warns within 30 days (WarningDays). Caveat: this check is not registered in the Attestor Doctor plugin today (it is omitted fromAttestorDoctorPlugin.GetChecks()), and its key inventory (GetSigningKeysAsync) returns demo data rather than the live key store — so it may report “Skip”/sample results or not resolve at all. For an expired key, rely on theattestor.signingconfig and followattestor-key-expired.md.If the provider is a remote HSM/KMS, confirm reachability of the backing service before blaming Attestor. See
attestor-hsm-connection.mdfor the HSM/KMS connectivity path. Stella Ops is on-prem first: the default signing provider is the File-KMS provider (signing.kmswith a localrootPath) or a HashiCorp Vault-backed provider — there are no cloud-managed KMS defaults.
Resolution
Immediate mitigation
If the configured key route drifted (
provider_route_drift/provider_key_drift/provider_algorithm_drift): alignattestor.signing.keys[]with the active crypto provider, or point signing at a known-good key by setting the request’skeyIdto a healthy entry. Restart the Attestor service so it reloadsattestor.signing(there is no livereloadcommand — Attestor reads options at startup).If key material is missing/unreadable (Doctor
check.attestation.cosign.keymaterial= Fail forfilemode): restore the key file and repoint the configuration, then restart the service. Note two distinct config surfaces: the Doctor check keys off the flatAttestor:Signing:Mode/Attestor:Signing:KeyPath/Attestor:Signing:KmsKeyRefvalues (recognized modeskeyless | file | kms, else “Unknown signing mode”), whereas the runtime signing registry loads per-key material fromattestor.signing.keys[].MaterialPath(or inlineMaterial). The Doctor “file”/KeyPathmodel and the registry’s per-keyMaterialPathare not the same setting — reconcile both when repointing.If the active key is expired, switch signing to a healthy key in
attestor.signing.keys[]and followattestor-key-expired.mdfor rotation.
Verified facts: There is no
stella attest config set signing.mode software,stella attest reload,stella attest retry,stella attest test-sign, orstella attest status(src/Cli/StellaOps.Cli/Commands/AttestCommandGroup.csexposesbuild,attach,verify,verify-offline,list,fetch,predicates;fix-chain/patchare sibling command groups, not subcommands ofattest). Signing mode is per-key inattestor.signing.keys[].mode, not a global software/HSM toggle. In the runtime registry the mode defaults tokms(for KMS keys) orkeyfulotherwise (AttestorSigningKeyRegistry.CreateEntry); the broader signing-mode vocabulary recorded in bundle/watchlist metadata iskeyless | kms | hsm | fido2(AttestationBundle.cs,AttestorEntryInfo.cs). The registry itself dispatches on provider (default/ECDSA,bouncycastle.ed25519,cn.sm.soft/SM2,kms/File-KMS); there is no dedicatedhsmprovider path inAttestorSigningKeyRegistry. Configuration is applied via the YAML/config file and picked up on restart.
Root cause fix
Provider drift (key route mismatch):
Reconcile the crypto provider with the per-key configuration in
attestor.signing.keys[](provider,providerKeyId,algorithm,kmsVersionId). Confirm the active crypto provider with:stella crypto status stella crypto provider showValidate the active crypto profile (intended to catch plugin-reachability / FIPS-mode disagreements):
stella crypto profile validateRoadmap: the
validateverb exists, but it calls the Platform endpointGET /api/v1/admin/crypto/profile/validate, which currently returns anendpoint_not_implementedpayload with exit code 0 until RP-028-013 lands a real verdict (CryptoCommandGroup.BuildProfileCommand). Adopt it in CI now, but do not rely on it as a live gate yet.
Missing or rotated key material:
Generate a key pair with cosign-compatible tooling and register it in
attestor.signing.keys[]:cosign generate-key-pair --output-key-prefix stellaops # stella crypto keys generate exists but currently only prints a success message # (CryptoCommandGroup.BuildKeysCommand stub) — it does not yet write real key files.Update
attestor.signing.keys[]to reference the new key (id, provider, algorithm, material path), then restart Attestor.
Key rotation (preferred ongoing fix):
Use the Signer key-rotation HTTP API described in the module runbook. The former stella keys rotate sample-data handler was unregistered and was deleted on 2026-07-29. The registered singular stella key rotate command still fails closed until its real IKeyRotationService client is configured; it must not be treated as an applied rotation.
Verification
# 1. Confirm key material and route are healthy
stella doctor --check check.attestation.cosign.keymaterial
# 2. Re-issue a sign request and confirm HTTP 200 with a bundle + key metadata
curl -sS --cert client.pem --key client.key \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-X POST https://attestor.internal:8444/api/v1/attestations:sign \
-d '{"keyId":"<key-id>","payloadType":"application/vnd.in-toto+json","payload":"<base64>","artifact":{"sha256":"<digest>","kind":"image"}}'
# 3. Confirm the failure metric has stopped climbing (Prometheus)
# attestor.sign_total{result="failure"} and attestor.errors_total{type="sign"}
Prevention
- [ ] Key rotation: rotate signing keys through the Signer API with overlap; keep
AttestorKeyRotationStale(fires whenattestor_key_last_rotated_seconds> 30d) wired to alerting. The registeredstella key rotatepath must not be used until its service client is wired. - [ ] Monitoring: alert on the
attestor.sign_total{result="failure"}rate andattestor.errors_total{type="sign"}. (Thecheck.attestation.keymaterialDoctor check is not registered today — track key expiry viaAttestorKeyRotationStaleinstead until it is wired up.) - [ ] Config drift guard: keep
attestor.signing.keys[](provider/key/algorithm) in lockstep with the active crypto provider to avoidprovider_*_driftfailures. - [ ] Health check: include
stella doctor --check check.attestation.cosign.keymaterialin the scheduled Doctor run.
Related Resources
- Architecture:
docs/modules/attestor/architecture.md(§ Signing, § Observability & audit, § Configuration) - Related runbooks:
attestor-key-expired.md,attestor-hsm-connection.md,crypto-ops.md - Doctor checks:
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Attestor/Checks/CosignKeyMaterialCheck.cs(registered),.../SigningKeyExpirationCheck.cs(defined but not registered inAttestorDoctorPlugin.GetChecks()) - Doctor plugin registration:
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Attestor/AttestorDoctorPlugin.cs - Doctor CLI options:
src/Cli/StellaOps.Cli/Commands/DoctorCommandGroup.cs(--check/--category/--tag; no--plugin) - Sign endpoint:
src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/AttestorWebServiceEndpoints.cs(POST /api/v1/attestations:sign) - Sign service / error codes:
src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Signing/AttestorSigningService.cs - Alerts:
devops/telemetry/alerts/attestation-alerts.yaml - Dashboard: Grafana > Stella Ops > Attestor
