Skip to content
Registry StackDocsDevelopment (unreleased)

Configure Transit signing for Registry Evidence

For the operator

View as Markdown

You have a candidate that serves with a local JWK signer. Move the same Evidence project to a Transit signer before it serves real callers: Vault or OpenBao holds the private key and never exports it, and Registry Evidence signs through a workload-local proxy socket. The service process receives no Vault or OpenBao token.

  • A Vault or OpenBao Transit mount administered outside the application workload, and an authenticated operator session for it. The commands use a mount named transit.
  • A copy of the release’s production example. Download the source archive of your release, https://github.com/registrystack/registry-stack/archive/refs/tags/v<version>.tar.gz, where <version> is the version rstack evidence --version prints. It unpacks to registry-stack-<version>/; copy its products/evidence/reference/deployment-targets/environments/production/ directory to production/ beside your project. The copy holds the Transit policy, the proxy configuration, and the deployment target production/evidence/target.yaml; <target> in the commands is production/evidence.
  • The evidence component installed for rstack, and jq.

Create a non-derived, non-exportable P-256 key with plaintext backup disabled:

Terminal window
vault write transit/keys/evidence-signing \
type=ecdsa-p256 \
derived=false \
exportable=false \
allow_plaintext_backup=false

Read its metadata to a review file and record the key version you will pin:

Terminal window
vault read -format=json transit/keys/evidence-signing > "<operator-review-path>/evidence-key.json"

The proxy identity needs read on the key’s metadata and update on its sign endpoint, constrained to the pinned key version and the exact JWS request shape. This is production/transit/policies/evidence-signing.hcl, which pins key version 12:

path "transit/keys/evidence-signing" {
capabilities = ["read"]
}
path "transit/sign/evidence-signing/sha2-256" {
capabilities = ["update"]
required_parameters = ["input", "key_version", "marshaling_algorithm", "prehashed"]
allowed_parameters = {
"input" = []
"key_version" = ["12"]
"marshaling_algorithm" = ["jws"]
"prehashed" = [true]
}
}

Registry Evidence sends key_version as a string because OpenBao matches allowed parameter values by string. Keep the policy value quoted. The runtime also refuses a pinned key version below the key’s min_encryption_version; Rotate Registry Evidence signing keys uses that control. The Vault Transit API and the OpenBao Transit API document every other field; use the equivalent bao commands for OpenBao.

The proxy holds the provider credential and Registry Evidence holds only the socket. This is production/transit/proxy-configs/evidence.hcl, without its pid_file and auto_auth method, which you replace with your platform’s:

vault {
address = "https://vault.example.org:8200"
ca_cert = "/etc/registry-evidence/transit/ca.pem"
retry {
num_retries = -1
}
}
api_proxy {
use_auto_auth_token = "force"
}
listener "unix" {
address = "/run/registry-evidence/transit-proxy.sock"
tls_disable = true
socket_mode = "0660"
socket_user = "vault"
socket_group = "registry-evidence"
require_request_header = true
}

Keep use_auto_auth_token = "force": the proxy replaces any token on an incoming request with its own, so the proxy identity is the only one that reaches the provider through this socket. Under true, a token supplied by whatever reached the socket would be used instead. num_retries = -1 makes one signing attempt one provider request, and the socket group admits only the Evidence Gateway identity. The Vault API proxy documentation and the OpenBao Agent configuration describe the rest.

Convert the pinned key version’s public PEM from the review file into the target’s public-keys/ directory:

Terminal window
jq -r '.data.keys["<key-version>"].public_key' "<operator-review-path>/evidence-key.json" \
| rstack evidence jwk from-pem - --output-dir "<target>/public-keys"
wrote <target>/public-keys/<thumbprint>.jwk.json (kid <thumbprint>)

The kid is the RFC 7638 thumbprint, and it names the public JWK file. The command refuses a private key. Commit only the public JWK file, then delete the review file; do not commit provider responses. Run rstack evidence jwk from-pem --help for the command reference.

Replace the target’s signer block. This is the one in production/evidence/target.yaml:

signer:
type: transit
unixSocketPath: /run/registry-evidence/transit-proxy.sock
mount: transit
keyName: evidence-signing
keyVersion: 12
attemptTimeoutMilliseconds: 2000
algorithm: ES256
publicJwkFile: public-keys/PSmhXYjoKCBO6gCOMNHqMvnnsAC3m0aaLfXZDWJLmd4.jwk.json

keyVersion must be the version the policy allows, and publicJwkFile the file jwk from-pem wrote. Put no provider token or private key in the target.

Check the project against the target for production:

Terminal window
rstack evidence check "<project>" --target "<target>/target.yaml" --production
Evidence project check: complete
Project: <project>
Target: <target>/target.yaml
0 errors, 0 warnings in 1 file

Package the candidate. package runs the check and the fixtures again before it writes anything:

Terminal window
rstack evidence package "<project>" --target "<target>/target.yaml" --output "<candidate>"
Package digest: sha256:<package-digest>
Package: <candidate>/bundle
Runtime: <candidate>/runtime.yaml
Authority: <candidate>/compiled-authority.yaml

The public JWK file goes into the bundle, and the socket, mount, key name, and key version go into the candidate’s runtime.yaml. Install the bundle and activate the target authority as in Build and deploy a Registry Evidence project.

Start the proxy first. Then, on the target host, run doctor:

Terminal window
rstack evidence doctor --runtime-config "<candidate>/runtime.yaml"

Doctor reads the key’s metadata, checks the custody fields and the key version, compares the provider’s public key with the public JWK file, and signs and verifies a self-test message. When the proxy is not answering, it refuses:

error[evidence.deployment.signing-unavailable] <candidate>/runtime.yaml
runtime signing initialization failed: the Transit provider did not answer on the configured Unix socket (missing socket, refused connection, or timeout)

When doctor passes, start the service with rstack evidence runtime serve and route traffic only after /ready reports ready. If the provider becomes unreachable later, signing fails closed and readiness reports the signer unavailable until a self-test succeeds. See rstack evidence doctor --help.

Integrate a Registry Evidence candidate with Docker Compose