ProofedMesh/deploy/approval-service
CTO (Paperclip) 20ba29d3ac
All checks were successful
build-images / signing-service image (push) Successful in 26s
build-images / verification-service image (push) Successful in 27s
build-images / approval-service image (push) Successful in 31s
build-images / dummy-sink image (push) Successful in 29s
validate / JSON Schema (ajv) (push) Successful in 23s
validate / Rego (opa fmt + opa test) (push) Successful in 16s
phase1-smoke / Phase 1 end-to-end smoke (push) Successful in 1m0s
validate / DB (migrate up/down dry-run) (push) Successful in 26s
validate / Go (vet + test) (push) Successful in 1m20s
approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614)
Phase-1 Subtask 8/10 (parent DEV-606):
- deploy/approval-service/: SA (no auto-mount), Deployment (replicas=2,
  distroless-nonroot, read-only rootfs), ClusterIP :8080, NetworkPolicy
  (ingress from pangolin namespace only; egress to signing-service,
  transparency-log, trust-pg in agent-trust + pocket-id + DNS), README.
- 10-secret-dev.example.yaml template + the real 10-secret-dev.sealed.yaml
  carrying the Pocket-ID client id/secret, WebAuthn RP config, and the
  dev signing-service bearer; unsealed in-cluster to Secret
  approval-service-dev.
- services/approval-service/Dockerfile: BuildKit multi-stage, context-at-
  repo-root, matches the signing/verification/dummy-sink pattern.
- .forgejo/workflows/build-images.yml: new approval-service job pushing
  harbor.basicstack.de/basicstack/approval-service:{sha,main} on main.
- Pocket-ID: created proofedmesh-admin group + OIDC client
  "ProofedMesh Approval" with PKCE enforced, callback
  https://approval.proofedmesh.basicstack.de/oidc/callback, group
  restriction to proofedmesh-admin. Client secret stored as Paperclip
  secret proposal and in the SealedSecret above — never pasted inline.

Deployment applied cluster-side pending the first CI image build:
`kubectl apply -f deploy/approval-service/` is green; pods ErrImagePull
until the Forgejo workflow completes and pushes the :main tag.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 21:12:01 +00:00
..
00-serviceaccount.yaml approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614) 2026-10-25 21:12:01 +00:00
10-secret-dev.example.yaml approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614) 2026-10-25 21:12:01 +00:00
10-secret-dev.sealed.yaml approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614) 2026-10-25 21:12:01 +00:00
20-deployment.yaml approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614) 2026-10-25 21:12:01 +00:00
30-service.yaml approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614) 2026-10-25 21:12:01 +00:00
40-networkpolicy.yaml approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614) 2026-10-25 21:12:01 +00:00
README.md approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614) 2026-10-25 21:12:01 +00:00

approval-service — deployment

Kubernetes manifests for the ADR-0005 approval-service in the agent-trust namespace (DEV-608 scope: content-binding + WebAuthn-gated approvals; DEV-614 scope: Kubernetes deployment + Pocket-ID OIDC client registration).

The namespace itself is created by deploy/trillian/00-namespace.yaml; this directory assumes that file (or an equivalent) is already applied.

Layout

deploy/approval-service/
├── 00-serviceaccount.yaml       — ServiceAccount (no auto-mount, no kube API calls)
├── 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 :8080
├── 40-networkpolicy.yaml        — ingress from Pangolin, egress to signing/transparency/pocket-id/dns
└── README.md                    — this file

Prod wiring (DEV-614)

The Deployment reads the following prod flags via env:

Flag Env Purpose
-signing-url APPROVAL_SIGNING_URL signing-service base URL (ADR-0005 DSSE signer).
-signing-key-ref APPROVAL_SIGNING_KEY_REF Key reference used when signing approval attestations.
-tlog-url APPROVAL_TLOG_URL transparency-log base URL (Trillian frontend).
-session-ttl APPROVAL_SESSION_TTL Approval-session lifetime.
-challenge-ttl APPROVAL_CHALLENGE_TTL Max. time between preview and confirm.
(env only) APPROVAL_POCKET_ID_ISSUER Pocket-ID OIDC issuer, embedded as the attestation issuer field (Phase 2 auth discovery).
(env only) APPROVAL_POCKET_ID_CLIENT Pocket-ID OIDC client ID (DEV-614 registration output).
(env only) APPROVAL_POCKET_ID_SECRET Pocket-ID OIDC client secret (Phase 2 middleware consumes it).
(env only) APPROVAL_WEBAUTHN_RP_ID WebAuthn relying party ID (public hostname, no scheme).
(env only) APPROVAL_WEBAUTHN_RP_ORIGIN WebAuthn allowed origin (scheme + host, no path).
(env only) APPROVAL_SIGNING_TOKEN Bearer token presented to signing-service (dev mode).

The Pocket-ID client ID/secret, the WebAuthn RP config, and the signing-service bearer live in one Secret approval-service-dev so a SealedSecret rotation flow stays uniform.

Apply order

kubectl apply -f deploy/trillian/00-namespace.yaml  # if not yet applied
kubectl apply -f deploy/approval-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/approval-service/20-deployment.yaml
kubectl apply -f deploy/approval-service/30-service.yaml
kubectl apply -f deploy/approval-service/40-networkpolicy.yaml

Reach the service in-cluster at:

http://approval-service.agent-trust.svc.cluster.local:8080

Health check (also used by Deployment probes):

GET /healthz → 200 {"status":"ok"}

Approval API (per ADR-0005 §4.3):

POST /api/v1/sessions
POST /api/v1/approvals/preview
POST /api/v1/approvals/confirm

Public ingress (https://approval.proofedmesh.basicstack.de) is terminated at the Pangolin/Traefik edge and routed to the cluster Service; the IngressRoute itself is managed from the Pangolin controller side and is not included here.

Pocket-ID OIDC client registration

The approval-service authenticates browser users via Pocket-ID (authorization_code with PKCE) and will exchange a client_credentials token with signing-service in Phase 2. One client covers both flows:

  • Name: ProofedMesh Approval
  • Callback URL: https://approval.proofedmesh.basicstack.de/oidc/callback
  • PKCE enabled: true (authorization_code only)
  • Group restriction: proofedmesh-admin (members may approve)

The client was registered against the production Pocket-ID (auth.basicstack.de) via its admin API using the POCKET_ID_API_KEY secret (DEV-614). The resulting client ID + secret are stored in the cluster Secret approval-service-dev and never committed to git.

To re-register from scratch (idempotent via name upsert):

curl -sS -X POST \
  -H "X-API-Key: $POCKET_ID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "ProofedMesh Approval",
        "callbackURLs": ["https://approval.proofedmesh.basicstack.de/oidc/callback"],
        "isPublic": false,
        "pkceEnabled": true,
        "isGroupRestricted": true
      }' \
  https://auth.basicstack.de/api/oidc/clients

Allow the proofedmesh-admin group on the client:

curl -sS -X PUT \
  -H "X-API-Key: $POCKET_ID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '["<proofedmesh-admin-group-id>"]' \
  https://auth.basicstack.de/api/oidc/clients/<client-id>/allowed-user-groups

Rotate (or mint the initial) client secret:

curl -sS -X POST \
  -H "X-API-Key: $POCKET_ID_API_KEY" \
  https://auth.basicstack.de/api/oidc/clients/<client-id>/secret

K8s secret convention

The approval-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 approval-service-<mode>. This keeps the SealedSecret rotation path uniform.

Dev mode

Secret: approval-service-dev

Env key Purpose
APPROVAL_SIGNING_TOKEN Bearer presented to signing-service (dev-static table).
APPROVAL_POCKET_ID_CLIENT Pocket-ID OIDC client ID (returned by the registration above).
APPROVAL_POCKET_ID_SECRET Pocket-ID OIDC client secret.
APPROVAL_WEBAUTHN_RP_ID WebAuthn relying-party ID (public hostname).
APPROVAL_WEBAUTHN_RP_ORIGIN WebAuthn origin shown to the browser.

Create the Secret for the cluster (never commit the raw value):

kubectl create secret generic approval-service-dev \
  -n agent-trust \
  --from-literal="APPROVAL_SIGNING_TOKEN=${SIGNING_BEARER}" \
  --from-literal="APPROVAL_POCKET_ID_CLIENT=${POCKET_ID_CLIENT}" \
  --from-literal="APPROVAL_POCKET_ID_SECRET=${POCKET_ID_SECRET}" \
  --from-literal="APPROVAL_WEBAUTHN_RP_ID=approval.proofedmesh.basicstack.de" \
  --from-literal="APPROVAL_WEBAUTHN_RP_ORIGIN=https://approval.proofedmesh.basicstack.de"

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 approval-service-dev -n agent-trust \
  --from-literal="APPROVAL_SIGNING_TOKEN=${SIGNING_BEARER}" \
  --from-literal="APPROVAL_POCKET_ID_CLIENT=${POCKET_ID_CLIENT}" \
  --from-literal="APPROVAL_POCKET_ID_SECRET=${POCKET_ID_SECRET}" \
  --from-literal="APPROVAL_WEBAUTHN_RP_ID=approval.proofedmesh.basicstack.de" \
  --from-literal="APPROVAL_WEBAUTHN_RP_ORIGIN=https://approval.proofedmesh.basicstack.de" \
  --dry-run=client -o yaml | \
  kubeseal --controller-namespace=sealed-secrets --format=yaml \
    > deploy/approval-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 committed until the first real deployment lands).

Verifying the deployment

# Pod comes up, probe says OK:
kubectl -n agent-trust rollout status deploy/approval-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://approval-service:8080/healthz

# External reachability through the Pangolin edge:
curl -sS https://approval.proofedmesh.basicstack.de/healthz

Image

Images are built by .forgejo/workflows/build-images.yml on every push to main and pushed to:

harbor.basicstack.de/basicstack/approval-service:<git-sha>
harbor.basicstack.de/basicstack/approval-service:main      (floating)

The Deployment currently references :main; a GitOps apply will pin it to a :<git-sha> once the first image build has run.