# signing-service — deployment Kubernetes manifests for the ADR-0005 signing-service in the `agent-trust` namespace. The namespace itself is created by [`deploy/trillian/00-namespace.yaml`](../trillian/00-namespace.yaml); this directory assumes that file (or an equivalent) is already applied. ## Layout ``` deploy/signing-service/ ├── 00-serviceaccount.yaml — ServiceAccount with projected SA token ├── 10-secret-dev.example.yaml — TEMPLATE for the dev-mode Secret ├── 20-deployment.yaml — Deployment (non-root, read-only rootfs, distroless) ├── 30-service.yaml — ClusterIP Service on :8081 ├── 40-networkpolicy.yaml — ingress from agent-trust, DNS egress └── README.md — this file ``` ## Apply order ```bash kubectl apply -f deploy/trillian/00-namespace.yaml # if not yet applied kubectl apply -f deploy/signing-service/00-serviceaccount.yaml # Create the dev Secret separately (see "Secret convention" below) — DO NOT # commit the real values. The .example file is a template, not a secret. kubectl apply -f deploy/signing-service/20-deployment.yaml kubectl apply -f deploy/signing-service/30-service.yaml kubectl apply -f deploy/signing-service/40-networkpolicy.yaml ``` Reach the service in-cluster at: ``` http://signing-service.agent-trust.svc.cluster.local:8081 ``` Health check (also used by Deployment probes): ``` GET /healthz → 200 {"status":"ok"} ``` Sign endpoint (per ADR-0005): ``` POST /api/v1/sign/dsse Authorization: Bearer ``` ## K8s secret convention The signing-service never pulls secrets from the pod filesystem or a config map. Every secret value is an **environment variable sourced from one Kubernetes Secret** named `signing-service-`. This keeps the SealedSecret rotation path uniform and lets the Deployment stay identical across modes. ### Dev mode (`-backend=memory`) Secret: `signing-service-dev` | Env key | Purpose | | ------------------------------------------- | ----------------------------------------------------------------------- | | `SIGNING_BEARERS` | `-bearers` value, comma-separated `bearer=subject` pairs | | `KEY_` | one entry per `key_ref`; value is a hex-encoded 64-byte Ed25519 private key | Slug rule: upper-case the `key_ref`, then replace `/`, `-`, `.` with `_`. For `trust-api/rate-change-signer-2026-q4` the env name is `KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4`. Mint a dev key: ```bash cd services/signing-service go run ./cmd/signing-service -gen-dev-key # → prints 128 hex chars (64 bytes raw Ed25519 private key) ``` Create the Secret for the cluster (never commit the raw value): ```bash BEARER_TOKEN="$(openssl rand -hex 32)" HEX_KEY="$(cd services/signing-service && go run ./cmd/signing-service -gen-dev-key)" kubectl create secret generic signing-service-dev \ -n agent-trust \ --from-literal="SIGNING_BEARERS=${BEARER_TOKEN}=agent-admin-api" \ --from-literal="KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4=${HEX_KEY}" ``` For the GitOps path, convert the above to a SealedSecret with `kubeseal` and commit the sealed form under the same filename as the example: ```bash kubectl create secret generic signing-service-dev -n agent-trust \ --from-literal="SIGNING_BEARERS=${BEARER_TOKEN}=agent-admin-api" \ --from-literal="KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4=${HEX_KEY}" \ --dry-run=client -o yaml | \ kubeseal --controller-namespace=sealed-secrets --format=yaml \ > deploy/signing-service/10-secret-dev.sealed.yaml ``` The `10-secret-dev.example.yaml` file is kept as the shape reference; the real sealed form is `10-secret-dev.sealed.yaml` (not yet committed — ships when the first real deployment lands). ### Prod mode (future, out of scope for DEV-605) Secret: `signing-service-prod` Planned env keys — kept here so future subtasks don't invent a new shape: | Env key | Purpose | Subtask | | ---------------------------- | ----------------------------------------------------------------- | ------------------------ | | `OPENBAO_ADDR` | OpenBao server URL (`https://openbao.agent-trust.svc:8200`) | OpenBao Transit signer | | `OPENBAO_ROLE` | AppRole / Kubernetes auth role name for the signing-service pod | OpenBao Transit signer | | `OPENBAO_TOKEN` (bootstrap) | optional bootstrap token; prefer SA-token-based login once wired | OpenBao Transit signer | | `POCKET_ID_JWKS_URL` | `https://auth.basicstack.de/.well-known/jwks.json` | Pocket-ID JWKS validator | | `POCKET_ID_ISSUER` | `https://auth.basicstack.de` | Pocket-ID JWKS validator | | `POCKET_ID_AUDIENCE` | expected `aud` claim (e.g. `trust-api`) | Pocket-ID JWKS validator | | `OPA_URL` | OPA sidecar / service URL (`http://opa.agent-trust.svc:8181`) | OPA policy evaluator | The matching command-line flags (`-backend=openbao`, `-policy=opa`, `-validator=pocket-id`) land with the respective subtasks. Until then the Deployment stays on `-backend=memory` with the dev Secret above; switching is a Deployment patch and a Secret swap — no manifest reshape. ## Verifying the deployment ```bash # Pod comes up, probe says OK: kubectl -n agent-trust rollout status deploy/signing-service # In-cluster reachability from a throwaway pod: kubectl -n agent-trust run curl --image=curlimages/curl:8 -it --rm --restart=Never -- \ curl -sS http://signing-service:8081/healthz ``` End-to-end sign check (adjust `BEARER_TOKEN` to the one you stored in the Secret): ```bash kubectl -n agent-trust run curl --image=curlimages/curl:8 -it --rm --restart=Never -- \ curl -sS -X POST http://signing-service:8081/api/v1/sign/dsse \ -H "Authorization: Bearer ${BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"key_ref":"trust-api/rate-change-signer-2026-q4", "statement":{"_type":"https://in-toto.io/Statement/v1", "subject":[{"name":"per_diem_rate/PDR-1","digest":{"sha256":"aaaa"}}], "predicateType":"https://basicstack.de/attestations/rate_change/v1", "predicate":{"change_kind":"create","rate_id":"PDR-1"}}}' ``` ## Image Images are built by `.forgejo/workflows/build-images.yml` on every push to `main` and pushed to: ``` harbor.basicstack.de/basicstack/signing-service: harbor.basicstack.de/basicstack/signing-service:main (floating) ``` The Deployment currently references `:dev`; a GitOps apply will pin it to a `:` once the image build pipeline has produced one.