OfflineKit Signer-custody staging

This runbook stages the OK-7 package-signing seam. Signer keeps the artifact signing key; the OfflineKit builder receives only a proof-of-entitlement value, a DPoP key, and a least-privilege Authority client credential. The builder calls Signer through the gateway and writes the two DSSE sidecars consumed by the canonical OfflineKit import API.

The repository work in this runbook is safe to perform without changing the running estate. Starting or recreating services, deploying images, publishing a kit, and running live forcing probes remain owner-attended window actions.

Contract and stop conditions

1. Prepare service-specific local configuration

From the repository root, copy these tracked templates into the ignored local secret directory:

devops/compose/env/offlinekit-builder-authority.env.example
  -> .local-secrets/offlinekit-builder-authority.env

devops/compose/env/offlinekit-signer.env.example
  -> .local-secrets/offlinekit-signer.env

The Authority file contains only OFFLINEKIT_BUILDER_AUTHORITY_CLIENT_SECRET. The Signer file contains only the reviewed producer digest and PoE entitlement metadata. Do not combine them or load either file into another service.

Create the client secret and PoE token in the operator’s local secret store. Use high-entropy hexadecimal values; a PoE value must not contain __ because Signer uses that sequence as its configuration-key delimiter. Put the same PoE value into every Signer__Entitlements__Tokens__<POE>__... key and make it available to the builder only through STELLAOPS_OFFLINEKIT_SIGNER_POE.

Compute the reviewed builder identity from the exact source file:

sha256sum devops/offline/kit/build_offline_kit.py

Set the Signer allowlist value to sha256: followed by the reported 64 lowercase hexadecimal characters. A bare digest is invalid.

2. Prepare the DPoP key

Generate or select an operator-held P-256 private key. The builder requires an OpenSSL 3.x command and verifies that major version before reading the key. One suitable command is:

openssl ecparam -name prime256v1 -genkey -noout -out <local-dpop-key.pem>

Restrict the file to the operator account. If it is encrypted, expose its password to the builder by setting the environment variable named by STELLAOPS_OFFLINEKIT_SIGNER_DPOP_KEY_PASSWORD; never put the password on the command line.

Before building, set these values from the local secret/configuration store without echoing them:

STELLAOPS_OFFLINEKIT_AUTHORITY_TOKEN_URL=https://<gateway>/connect/token
OFFLINEKIT_BUILDER_AUTHORITY_CLIENT_SECRET=<local secret>
STELLAOPS_OFFLINEKIT_AUTHORITY_TENANT=default
STELLAOPS_OFFLINEKIT_SIGNER_URL=https://<gateway>
STELLAOPS_OFFLINEKIT_SIGNER_POE=<local PoE token>
STELLAOPS_OFFLINEKIT_SIGNER_DPOP_KEY_FILE=<local-dpop-key.pem>

Leave STELLAOPS_OFFLINEKIT_SIGNER_BEARER_TOKEN unset to let the builder mint the DPoP-bound client-credentials token. A pre-minted token is supported only when its cnf.jkt was created from the same key.

Use --authority-ca-file and --signer-ca-file when the gateway certificate is not rooted in the host trust store.

3. Repository-side preflight

Render only; do not start services:

docker compose \
  -f devops/compose/docker-compose.stella-ops.yml \
  -f devops/compose/docker-compose.offlinekit-signer.yml \
  config --quiet

Run the bounded conformance tests:

pwsh ./tools/scripts/test-targeted-xunit.ps1 `
  -Project src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard.Tests/StellaOps.Authority.Plugin.Standard.Tests.csproj `
  -Method "*RepositoryStandardYaml_DefinesFirstPartyCliBootstrapClients*"

pwsh ./tools/scripts/test-targeted-xunit.ps1 `
  -Project src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj `
  -Method "*OfflineKitSignerCustodyRouteConfigTests*"

python -m unittest devops/offline/kit/test_build_offline_kit.py -v

The overlay deliberately remains outside the canonical Compose helper chains. At this boundary the work is staged; stop before up, restart, --force-recreate, image build/push, kit publication, or a live request.

4. Owner-attended window acceptance

Use the live-deploy playbook to install gate-green Authority, Router, and Signer artifacts and apply the opt-in overlay. Record the deployed image digests and route configuration before testing. No source checkout alone proves this step.

Build a kit through the gateway:

python devops/offline/kit/build_offline_kit.py \
  --version <version> \
  --channel <channel> \
  --release-dir <release-dir> \
  --staging-dir <scratch-staging-dir> \
  --output-dir <output-dir> \
  --signer-url "${STELLAOPS_OFFLINEKIT_SIGNER_URL}" \
  --authority-token-url "${STELLAOPS_OFFLINEKIT_AUTHORITY_TOKEN_URL}" \
  --authority-ca-file <ca-bundle.pem> \
  --signer-ca-file <ca-bundle.pem>

Acceptance requires all of the following, with HTTP status and correlation/audit identifier recorded without credentials or proof material:

  1. The dedicated client mints a token with both audiences, both scopes, tenant default, and cnf.jkt matching the operator DPoP key.
  2. Both sign calls and both verify calls succeed through the gateway.
  3. An unbound bearer token is rejected before Signer.
  4. A producer digest not in Signer’s exact allowlist is rejected.
  5. Reusing a DPoP proof/jti is rejected by the durable replay store.
  6. A proof for a different htu is rejected.
  7. The output directory contains the bundle, checksum, published manifest, carrier DSSE sidecar, manifest DSSE sidecar, and metadata file; the CLI can resolve the sibling sidecars from that metadata after the directory is moved.

Do not publish the kit or perform the OfflineKit import/data cutover as part of this signing acceptance unless those separate window actions are explicitly in scope.

Rollback

If the window fails, remove only the opt-in overlay from the Compose invocation and restore the previously recorded Authority, Router, and Signer image/config digests through the live-deploy procedure. Preserve failed request audit ids and redacted logs. Do not delete Signer keys, OfflineKit data, or shared databases.