|
All checks were successful
build-images / signing-service image (push) Successful in 26s
build-images / verification-service image (push) Successful in 27s
phase1-smoke / Phase 1 end-to-end smoke (push) Successful in 1m2s
build-images / approval-service image (push) Successful in 27s
build-images / dummy-sink image (push) Successful in 26s
validate / JSON Schema (ajv) (push) Successful in 21s
validate / Rego (opa fmt + opa test) (push) Successful in 15s
validate / DB (migrate up/down dry-run) (push) Successful in 27s
validate / Go (vet + test) (push) Successful in 1m22s
The Phase-1 NetworkPolicy from DEV-614 allowed ingress only from the `pangolin` namespace, under the mistaken assumption that Traefik runs there. In this cluster Traefik lives in `kube-system`; Pangolin only renders the Traefik dynamic config and does not sit in the request path. The policy therefore blocked external calls to `approval.proofedmesh.basicstack.de`, which 502'd at Traefik. Point the ingress rule at `kube-system/app.kubernetes.io/name=traefik` and update the inline comment to match reality. Verified: `curl -sS https://approval.proofedmesh.basicstack.de/healthz` → `{"status":"ok"}` (HTTP 200, LetsEncrypt cert via Traefik ACME). Co-Authored-By: Paperclip <noreply@paperclip.ing> |
||
|---|---|---|
| .. | ||
| 00-serviceaccount.yaml | ||
| 10-secret-dev.example.yaml | ||
| 10-secret-dev.sealed.yaml | ||
| 20-deployment.yaml | ||
| 30-service.yaml | ||
| 40-networkpolicy.yaml | ||
| README.md | ||
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.