ProofedMesh/deploy/approval-service/README.md

207 lines
9.2 KiB
Markdown
Raw Permalink Normal View History

# 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`](../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
```bash
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):
```bash
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:
```bash
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:
```bash
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):
```bash
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:
```bash
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
```bash
# 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.