checkId: check.platform.identity-envelope plugin: stellaops.doctor.security severity: fail tags: [security, identity, auth, platform]
Identity Envelope Signing Key
This Doctor check verifies the shared HMAC signing key that the gateway uses to sign per-request identity envelopes and that every downstream service uses to verify them (Sprint 310). A missing, empty, too-short, default/placeholder, or otherwise unusable key silently breaks all service-to-service authorization and tenant isolation — so this is a platform-wide blast-radius check.
What It Checks
Resolves the key from the canonical manifest entry integration.identity.envelope-signing-key — config key Router:IdentityEnvelopeSigningKey (the colon form of the compose Router__IdentityEnvelopeSigningKey), with the env alias STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY accepted as a legacy fallback. The probe then evaluates, in order:
| Condition | Result |
|---|---|
| Key is absent / empty | fail (platform-wide — all cross-service auth is broken) |
Key is a known default/placeholder (CHANGE_ME_…, __GENERATE_…, etc.) | fail |
| Key UTF-8 byte length < 32 (HMAC-SHA256 strength floor) | fail |
| Key fails an in-process sign+verify round-trip | fail |
| Key is present, strong, not a default, and round-trips | pass |
The round-trip is byte-equivalent to the GatewayIdentityEnvelopeCodec authentication MAC under the default world compliance profile — HMAC-SHA256 over the UTF-8 key bytes, verified with a constant-time compare — so a key that cannot actually sign+verify an envelope is caught without touching live traffic (the probe signs a fixed, non-secret payload that is never sent anywhere).
For WebService multi-tenant deployments, Doctor enumerates active tenants from the platform shared.tenants catalog and evaluates this check per tenant. A default tenant pass therefore cannot hide a second cataloged tenant whose identity-envelope configuration is missing or broken. Deployments with no catalog, an empty catalog, or exactly one cataloged tenant keep the historical single-run evidence shape.
Secret safety
The key value is never logged, echoed, or embedded in evidence. Evidence carries only: Present, the configured key name, Utf8ByteLength, MeetsMinimumStrength, DecodeKind / DecodedByteLength (base64/hex decodability, for diagnostics), IsDefaultPlaceholder, RoundTripVerified, and a one-way Sha256Fingerprint prefix (12 hex chars — enough for two operators to confirm the gateway and service keys match, far too little to recover the key).
Why It Matters
The gateway signs an identity envelope per request in IdentityHeaderPolicyMiddleware, and every downstream service verifies it in AspNetRouterRequestDispatcher. If the key is absent the gateway cannot sign (or services cannot verify), so either every cross-service call is denied or — worse — trust is silently lost. A short or default key undermines the integrity guarantee the envelope exists to provide. This is the single shared secret behind platform-wide tenant isolation.
Common Causes
Router__IdentityEnvelopeSigningKey/STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEYis not set in the deployment- The key was set in the gateway env but not propagated to
x-router-microservice-defaults, so services verify with a different (or no) key - A fresh deployment shipped with the sample placeholder value (
CHANGE_ME_…/__GENERATE_…) - A truncated or hand-typed key shorter than 32 bytes was used instead of generated random material
How to Fix
Never auto-rotate this key on a running platform — rotating it invalidates every live envelope and breaks in-flight requests. Provision/rotate during a maintenance window and restart the gateway and services together.
Docker Compose
# Generate a strong key (>= 32 raw bytes) and set it in devops/compose/.env
openssl rand -base64 48
# -> STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY=<generated>
# Ensure it is wired into x-router-microservice-defaults so every service verifies with the same key
# (Router__IdentityEnvelopeSigningKey), then restart the gateway and services together:
docker compose -f devops/compose/docker-compose.stella-ops.yml up -d
Bare Metal / systemd
# Set the key in the service environment for the gateway AND every microservice
# (e.g. /etc/stellaops/identity.env): STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY=<>= 32 bytes>
openssl rand -base64 48
systemctl restart 'stellaops-*'
Kubernetes / Helm
# Store the key in a Secret consumed by the gateway and all services:
kubectl create secret generic stellaops-identity-envelope \
--from-literal=signingKey="$(openssl rand -base64 48)" -n stellaops
# Reference it via Router__IdentityEnvelopeSigningKey in the Helm values, then:
helm upgrade stellaops stellaops/stellaops -f values.yaml
Verification
stella doctor run --check check.platform.identity-envelope
Related Checks
check.security.authority-signing-key— the Authority token-signing key (JWKS); a different signing dependencycheck.security.jwt.config— JWT signing/validation configurationcheck.servicegraph.gateway-router— the gateway that signs the envelopes must itself be reachable
