- Dockerfile: multi-stage (golang:1.23-alpine3.21 → distroless/static:nonroot), CGO off, trimpath, stripped, UID/GID 65532, read-only rootfs friendly. - deploy/signing-service/: ServiceAccount, Deployment (2 replicas, probes, tight SecurityContext), ClusterIP Service on :8081, NetworkPolicy (ingress from agent-trust, DNS egress), example Secret, and README with the full dev + prod secret convention. - .forgejo/workflows/build-images.yml: builds image on push + PR, pushes :<git-sha> + :main to harbor.basicstack.de on main pushes only. Clone pattern matches validate.yml (sha256 repo, manual git clone). - README updates: service README gains Deployment section; repo README layout now mentions services/signing-service, services/transparency-log, deploy/, and the new workflow. Scope matches DEV-605. OpenBao, OPA and Pocket-ID JWKS stay stubs and move in their own follow-up subtasks; the prod secret shape is documented so those subtasks can plug in without reshaping the Deployment. Co-Authored-By: Paperclip <noreply@paperclip.ing> |
||
|---|---|---|
| .. | ||
| 00-serviceaccount.yaml | ||
| 10-secret-dev.example.yaml | ||
| 20-deployment.yaml | ||
| 30-service.yaml | ||
| 40-networkpolicy.yaml | ||
| README.md | ||
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; 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
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:
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):
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:
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
# 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):
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.