Wires the signing-service's production path for ProofedMesh Phase 1: the service now performs a double identity check (Pocket-ID OIDC JWT + K8s ServiceAccount TokenReview), reconciles both against the trust-api `agents` registry, and asks an OPA/Rego policy whether the caller may sign with the requested key_ref. Code - internal/token/pocket_id.go: OIDC-Discovery-backed JWKS validator with automatic kid rotation (coreos/go-oidc). - internal/token/k8s_tokenreview.go: hand-rolled TokenReview client; reads X-Workload-Token, posts to authentication.k8s.io/v1 via the pod's in-cluster SA token + CA. - internal/registry: Postgres resolver for the agents table + in-memory Static resolver for tests. - internal/policy/opa.go: OPA/rego StructuredDecider; input carries both lookups so the policy can enforce identity consistency AND the key_ref ↔ role mapping. - internal/httpapi/sign.go: new structured flow when Validator+TokenReviewer+Registry+StructuredPolicy are all wired; dev path (static bearers + AllowAll) remains intact. - cmd/signing-service/main.go: -pocket-id-issuer, -pocket-id-audience, -k8s-tokenreview, -registry-dsn, -policy-bundle flags. Policy - policies/sinks/signing_service.rego: identities_agree + key_ref_matches_role rules; agent → key_ref prefix map covers approval-service, testagent-transform, dummy-sink. Deploy - deploy/signing-service/01-rbac.yaml: ClusterRole with tokenreviews.create + binding to the signing-service SA. - deploy/signing-service/20-deployment.yaml: adds POCKET_ID_ISSUER, POCKET_ID_AUDIENCE, TRUST_PG_DSN env, projected SA token volume, OPA policy ConfigMap mount, structured flags. - deploy/signing-service/00-serviceaccount.yaml: disables default auto-mount; a projected volume is used. - deploy/signing-service/README.md: documents prod flags + ConfigMap generation. Tests - httptest-mock Pocket-ID (OIDC discovery + JWKS, kid rotation), StaticTokenReviewer fake, Static registry, real Rego policy. - Happy paths: Approval-Service → trust-api/approval-signer-2026-q4; testagent-transform → trust-api/agent-testagent-transform-2026-q4. - Denies: registry mismatch, wrong key_ref for role, missing / bad workload token, bad Pocket-ID token, kid rotation. Co-Authored-By: Paperclip <noreply@paperclip.ing>
187 lines
8.3 KiB
Markdown
187 lines
8.3 KiB
Markdown
# 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 (opts out of auto-mount; a projected volume is used)
|
|
├── 01-rbac.yaml — ClusterRole + Binding for TokenReview (DEV-609)
|
|
├── 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
|
|
```
|
|
|
|
## Prod wiring (DEV-609)
|
|
|
|
The Deployment reads the following prod flags via env:
|
|
|
|
| Flag | Env | Purpose |
|
|
| ---------------------- | ------------------------ | ----------------------------------------------------------------------------------------- |
|
|
| `-pocket-id-issuer` | `POCKET_ID_ISSUER` | Pocket-ID OIDC issuer URL — enables JWKS validator (replaces static bearer table). |
|
|
| `-pocket-id-audience` | `POCKET_ID_AUDIENCE` | Expected `aud` claim on Pocket-ID tokens. |
|
|
| `-k8s-tokenreview=on` | (flag) | Enables K8s TokenReview on `X-Workload-Token` using the pod's projected SA token. |
|
|
| `-registry-dsn` | `TRUST_PG_DSN` | Postgres DSN for the trust-api `agents` table (DEV-607). |
|
|
| `-policy-bundle` | (flag, file path) | Rego file mounted at `/etc/signing-service/policies/signing_service.rego`. |
|
|
|
|
The OPA policy source lives at `policies/sinks/signing_service.rego`. Create
|
|
the ConfigMap it is mounted from before applying the Deployment:
|
|
|
|
```bash
|
|
kubectl -n agent-trust create configmap signing-service-policy \
|
|
--from-file=signing_service.rego=policies/sinks/signing_service.rego \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
```
|
|
|
|
## 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 <token from SIGNING_BEARERS>
|
|
```
|
|
|
|
## 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-<mode>`. 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_<SLUG>` | 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:<git-sha>
|
|
harbor.basicstack.de/basicstack/signing-service:main (floating)
|
|
```
|
|
|
|
The Deployment currently references `:dev`; a GitOps apply will pin it to a
|
|
`:<git-sha>` once the image build pipeline has produced one.
|