diff --git a/.forgejo/workflows/build-images.yml b/.forgejo/workflows/build-images.yml index 38fa582..c7fcebb 100644 --- a/.forgejo/workflows/build-images.yml +++ b/.forgejo/workflows/build-images.yml @@ -101,3 +101,139 @@ jobs: set -eu docker push "${{ steps.tags.outputs.image_sha }}" docker push "${{ steps.tags.outputs.image_main }}" + + verification-service: + name: verification-service image + runs-on: ubuntu-latest + container: + image: debian:trixie-slim + options: --user 0:0 + env: + REGISTRY: harbor.basicstack.de + IMAGE_REPO: harbor.basicstack.de/basicstack/verification-service + DOCKER_BUILDKIT: "1" + steps: + - name: Install git + buildx client + ca-certs + run: | + set -euo pipefail + apt-get update + apt-get install -y --no-install-recommends \ + git curl ca-certificates docker.io docker-cli docker-buildx + git --version + docker --version + docker buildx version + + - name: Clone repository (sha256-aware) + run: | + set -euo pipefail + git clone --depth=1 --branch="${GITHUB_REF_NAME}" \ + "https://x-access-token:${GITHUB_TOKEN}@forgejo.basicstack.de/${GITHUB_REPOSITORY}.git" \ + "${GITHUB_WORKSPACE}" + cd "${GITHUB_WORKSPACE}" + git log -1 --oneline + + - name: Resolve tags + id: tags + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + SHA="$(git rev-parse --short HEAD)" + echo "sha=${SHA}" >> "${GITHUB_OUTPUT}" + echo "image_sha=${IMAGE_REPO}:${SHA}" >> "${GITHUB_OUTPUT}" + echo "image_main=${IMAGE_REPO}:main" >> "${GITHUB_OUTPUT}" + + - name: Build image + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + docker buildx build \ + --load \ + -f services/verification-service/Dockerfile \ + -t "${{ steps.tags.outputs.image_sha }}" \ + -t "${{ steps.tags.outputs.image_main }}" \ + . + + - name: Login to Harbor (push jobs only) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} + HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} + run: | + set -eu + echo "${HARBOR_PASSWORD}" | \ + docker login "${REGISTRY}" --username "${HARBOR_USERNAME}" --password-stdin + + - name: Push image (main only) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + set -eu + docker push "${{ steps.tags.outputs.image_sha }}" + docker push "${{ steps.tags.outputs.image_main }}" + + dummy-sink: + name: dummy-sink image + runs-on: ubuntu-latest + container: + image: debian:trixie-slim + options: --user 0:0 + env: + REGISTRY: harbor.basicstack.de + IMAGE_REPO: harbor.basicstack.de/basicstack/dummy-sink + DOCKER_BUILDKIT: "1" + steps: + - name: Install git + buildx client + ca-certs + run: | + set -euo pipefail + apt-get update + apt-get install -y --no-install-recommends \ + git curl ca-certificates docker.io docker-cli docker-buildx + git --version + docker --version + docker buildx version + + - name: Clone repository (sha256-aware) + run: | + set -euo pipefail + git clone --depth=1 --branch="${GITHUB_REF_NAME}" \ + "https://x-access-token:${GITHUB_TOKEN}@forgejo.basicstack.de/${GITHUB_REPOSITORY}.git" \ + "${GITHUB_WORKSPACE}" + cd "${GITHUB_WORKSPACE}" + git log -1 --oneline + + - name: Resolve tags + id: tags + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + SHA="$(git rev-parse --short HEAD)" + echo "sha=${SHA}" >> "${GITHUB_OUTPUT}" + echo "image_sha=${IMAGE_REPO}:${SHA}" >> "${GITHUB_OUTPUT}" + echo "image_main=${IMAGE_REPO}:main" >> "${GITHUB_OUTPUT}" + + - name: Build image + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + docker buildx build \ + --load \ + -f services/dummy-sink/Dockerfile \ + -t "${{ steps.tags.outputs.image_sha }}" \ + -t "${{ steps.tags.outputs.image_main }}" \ + . + + - name: Login to Harbor (push jobs only) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} + HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} + run: | + set -eu + echo "${HARBOR_PASSWORD}" | \ + docker login "${REGISTRY}" --username "${HARBOR_USERNAME}" --password-stdin + + - name: Push image (main only) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + set -eu + docker push "${{ steps.tags.outputs.image_sha }}" + docker push "${{ steps.tags.outputs.image_main }}" diff --git a/deploy/dummy-sink/00-serviceaccount.yaml b/deploy/dummy-sink/00-serviceaccount.yaml new file mode 100644 index 0000000..9222115 --- /dev/null +++ b/deploy/dummy-sink/00-serviceaccount.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dummy-sink + namespace: agent-trust + labels: + app.kubernetes.io/name: dummy-sink + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: sink +# dummy-sink uses an EXPLICIT projected SA token mounted into +# /var/run/secrets/tokens/trust-api as the second half of the +# signing-service double-identity contract (DEV-611). The default +# auto-mount is disabled so the pod only sees that scoped token. +automountServiceAccountToken: false diff --git a/deploy/dummy-sink/10-pvc.yaml b/deploy/dummy-sink/10-pvc.yaml new file mode 100644 index 0000000..ab74f62 --- /dev/null +++ b/deploy/dummy-sink/10-pvc.yaml @@ -0,0 +1,22 @@ +# PersistentVolumeClaim for the dummy-sink payload store. +# +# Payloads land at /data/dummy-sink/.json via atomic +# .tmp+rename (see services/dummy-sink/README.md). 1Gi is intentionally +# small — this is a mock sink for Phase-1 smoke, not a production store. +# Longhorn is BasicStack's cluster default dynamic-provisioning class. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: dummy-sink-data + namespace: agent-trust + labels: + app.kubernetes.io/name: dummy-sink + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: sink +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 1Gi diff --git a/deploy/dummy-sink/11-secret-dev.example.yaml b/deploy/dummy-sink/11-secret-dev.example.yaml new file mode 100644 index 0000000..48f04fa --- /dev/null +++ b/deploy/dummy-sink/11-secret-dev.example.yaml @@ -0,0 +1,30 @@ +# dummy-sink dev secret — EXAMPLE ONLY. +# +# This file is a template. The real cluster Secret MUST be created either +# as a SealedSecret (preferred, see deploy/dummy-sink/README.md) or +# applied imperatively with values that never land in the git repo. +# +# Secret shape (Phase 1): +# POCKET_ID_ISSUER — Pocket-ID OIDC issuer URL. +# POCKET_ID_CLIENT_ID — client id for this agent. +# POCKET_ID_CLIENT_SECRET — client secret for this agent. +# POCKET_ID_AUDIENCE — expected `aud` claim (defaults to trust-api). +# +# The projected SA token for the X-Workload-Token side is mounted from +# the pod's own ServiceAccount via a projected volume — no Secret +# required for that half of the double-identity contract. +apiVersion: v1 +kind: Secret +metadata: + name: dummy-sink-dev + namespace: agent-trust + labels: + app.kubernetes.io/name: dummy-sink + app.kubernetes.io/part-of: proofedmesh + proofedmesh.basicstack.de/mode: dev +type: Opaque +stringData: + POCKET_ID_ISSUER: "https://auth.basicstack.de" + POCKET_ID_CLIENT_ID: "REPLACE_WITH_POCKET_ID_CLIENT_ID" + POCKET_ID_CLIENT_SECRET: "REPLACE_WITH_POCKET_ID_CLIENT_SECRET" + POCKET_ID_AUDIENCE: "trust-api" diff --git a/deploy/dummy-sink/20-deployment.yaml b/deploy/dummy-sink/20-deployment.yaml new file mode 100644 index 0000000..10fc1f1 --- /dev/null +++ b/deploy/dummy-sink/20-deployment.yaml @@ -0,0 +1,125 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dummy-sink + namespace: agent-trust + labels: + app.kubernetes.io/name: dummy-sink + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: sink +spec: + # ReadWriteOnce PVC → single-writer. Scaling past replicas=1 requires + # an RWX storage class or per-pod PVCs (not Phase-1 scope). + replicas: 1 + revisionHistoryLimit: 5 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: dummy-sink + template: + metadata: + labels: + app.kubernetes.io/name: dummy-sink + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: sink + spec: + serviceAccountName: dummy-sink + # Default auto-mount is off; the projected `trust-api`-audience + # token is provided via an explicit projected volume below. + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: dummy-sink + image: harbor.basicstack.de/basicstack/dummy-sink:dev + imagePullPolicy: IfNotPresent + args: + - "-listen=:8083" + - "-data-dir=/data/dummy-sink" + - "-verify-url=http://verification-service.agent-trust.svc.cluster.local:8084" + - "-signing-url=http://signing-service.agent-trust.svc.cluster.local:8081" + - "-tlog-url=http://transparency-log.agent-trust.svc.cluster.local" + - "-sink-id=sink:dummy-sink" + - "-actor=agent-dummy-sink" + - "-signing-key-ref=trust-api/agent-dummy-sink-2026-q4" + env: + # Pocket-ID client credentials for the Authorization: Bearer + # half of the signing-service double-identity check. + - name: POCKET_ID_ISSUER + valueFrom: + secretKeyRef: + name: dummy-sink-dev + key: POCKET_ID_ISSUER + - name: POCKET_ID_CLIENT_ID + valueFrom: + secretKeyRef: + name: dummy-sink-dev + key: POCKET_ID_CLIENT_ID + - name: POCKET_ID_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: dummy-sink-dev + key: POCKET_ID_CLIENT_SECRET + - name: POCKET_ID_AUDIENCE + valueFrom: + secretKeyRef: + name: dummy-sink-dev + key: POCKET_ID_AUDIENCE + ports: + - name: http + containerPort: 8083 + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 2 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + failureThreshold: 3 + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "256Mi" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data/dummy-sink + - name: workload-token + mountPath: /var/run/secrets/tokens + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: dummy-sink-data + # Projected ServiceAccount token addressed to the trust-api + # audience. signing-service accepts this via its K8s + # TokenReview path (DEV-609) as the workload-identity side of + # the double-identity contract. + - name: workload-token + projected: + sources: + - serviceAccountToken: + path: trust-api + audience: trust-api + expirationSeconds: 3600 diff --git a/deploy/dummy-sink/30-service.yaml b/deploy/dummy-sink/30-service.yaml new file mode 100644 index 0000000..2c2042d --- /dev/null +++ b/deploy/dummy-sink/30-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: dummy-sink + namespace: agent-trust + labels: + app.kubernetes.io/name: dummy-sink + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: sink +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: dummy-sink + ports: + - name: http + port: 8083 + targetPort: http + protocol: TCP diff --git a/deploy/dummy-sink/40-networkpolicy.yaml b/deploy/dummy-sink/40-networkpolicy.yaml new file mode 100644 index 0000000..a01d098 --- /dev/null +++ b/deploy/dummy-sink/40-networkpolicy.yaml @@ -0,0 +1,86 @@ +# NetworkPolicy for dummy-sink. +# +# Cluster-wide NetworkPolicy enforcement is active on BasicStack (see +# cluster memory on NetworkPolicy activation). Pods in agent-trust +# without an explicit allow-list are isolated; this file is the +# explicit allow-list for dummy-sink per DEV-615. +# +# Ingress callers (per Phase-1 architecture V1.1 §7): +# - testagent-transform — the sole producer of handover deliveries. +# +# Egress reachable targets (Phase-1 §7 Senken-Vertrag): +# - verification-service — chain verification + replay guard. +# - signing-service — handover-attestation signing. +# - transparency-log — logging the signed handover envelope. +# - DNS — kube-dns for service name resolution. +# +# dummy-sink intentionally does NOT egress Pocket-ID directly; it uses +# the Pocket-ID OIDC endpoint via HTTPS/443 as the client_credentials +# source for its Bearer token to signing-service. That cluster-external +# target is included below. +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: dummy-sink + namespace: agent-trust + labels: + app.kubernetes.io/name: dummy-sink + app.kubernetes.io/part-of: proofedmesh +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: dummy-sink + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: testagent-transform + ports: + - port: 8083 + protocol: TCP + egress: + # verification-service (same namespace). + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: verification-service + ports: + - port: 8084 + protocol: TCP + # signing-service (same namespace). + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: signing-service + ports: + - port: 8081 + protocol: TCP + # transparency-log (same namespace, port 80 → container 8080). + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: transparency-log + ports: + - port: 8080 + protocol: TCP + # Pocket-ID OIDC token exchange + JWKS — cluster-external HTTPS/443. + - ports: + - port: 443 + protocol: TCP + # DNS egress to kube-dns. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP diff --git a/deploy/dummy-sink/README.md b/deploy/dummy-sink/README.md new file mode 100644 index 0000000..a1dc9d5 --- /dev/null +++ b/deploy/dummy-sink/README.md @@ -0,0 +1,115 @@ +# dummy-sink — deployment + +Kubernetes manifests for the ADR-0007 dummy-sink 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/dummy-sink/ +├── 00-serviceaccount.yaml — ServiceAccount (auto-mount off, projected token used) +├── 10-pvc.yaml — PVC dummy-sink-data (1Gi, longhorn, RWO) +├── 11-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 :8083 +├── 40-networkpolicy.yaml — ingress from testagent-transform, +│ egress to verification-service / signing-service / transparency-log +└── README.md — this file +``` + +## Apply order + +```bash +kubectl apply -f deploy/trillian/00-namespace.yaml # if not yet applied +kubectl apply -f deploy/dummy-sink/00-serviceaccount.yaml +kubectl apply -f deploy/dummy-sink/10-pvc.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/dummy-sink/20-deployment.yaml +kubectl apply -f deploy/dummy-sink/30-service.yaml +kubectl apply -f deploy/dummy-sink/40-networkpolicy.yaml +``` + +Reach the sink in-cluster at: + +``` +http://dummy-sink.agent-trust.svc.cluster.local:8083 +``` + +Health check (also used by Deployment probes): + +``` +GET /healthz → 200 {"status":"ok"} +``` + +Deliver endpoint (per ADR-0007 / DEV-611): + +``` +POST /api/v1/sink/dummy/deliver +Content-Type: application/json +``` + +## PVC + +`dummy-sink-data` is a 1 Gi Longhorn RWO claim mounted at +`/data/dummy-sink`. The Deployment uses `strategy.type: Recreate` +because the PVC is `ReadWriteOnce` — a rolling update would try to +schedule a second pod against the same volume. Scaling past a single +replica requires an RWX storage class or per-pod PVCs; neither is in +Phase-1 scope. + +## K8s secret convention + +The dummy-sink reads one Secret, `dummy-sink-dev` (or +`dummy-sink-prod` for production), with the Pocket-ID client +credentials. Shape: + +| Env key | Purpose | +| ------------------------- | ---------------------------------------------- | +| `POCKET_ID_ISSUER` | Pocket-ID OIDC issuer URL | +| `POCKET_ID_CLIENT_ID` | Pocket-ID client id for this agent | +| `POCKET_ID_CLIENT_SECRET` | Pocket-ID client secret for this agent | +| `POCKET_ID_AUDIENCE` | Expected `aud` claim (default `trust-api`) | + +The second half of the double-identity contract (the projected +ServiceAccount token at `/var/run/secrets/tokens/trust-api`) is wired +by the Deployment's `projected` volume and does not require a Secret. + +For the GitOps path, convert the created Secret to a SealedSecret and +commit the sealed form under the same filename as the example: + +```bash +kubectl create secret generic dummy-sink-dev -n agent-trust \ + --from-literal="POCKET_ID_ISSUER=https://auth.basicstack.de" \ + --from-literal="POCKET_ID_CLIENT_ID=" \ + --from-literal="POCKET_ID_CLIENT_SECRET=" \ + --from-literal="POCKET_ID_AUDIENCE=trust-api" \ + --dry-run=client -o yaml | \ + kubeseal --controller-namespace=sealed-secrets --format=yaml \ + > deploy/dummy-sink/11-secret-dev.sealed.yaml +``` + +## NetworkPolicy + +See `40-networkpolicy.yaml`. In short: + +- **Ingress:** `testagent-transform` pods only (matched by + `app.kubernetes.io/name`). +- **Egress:** `verification-service` (:8084), `signing-service` + (:8081), `transparency-log` (:8080), external HTTPS/443 for the + Pocket-ID OIDC endpoint, DNS to `kube-dns`. + +## Image + +Images are built by `.forgejo/workflows/build-images.yml` on every push +to `main` and pushed to: + +``` +harbor.basicstack.de/basicstack/dummy-sink: +harbor.basicstack.de/basicstack/dummy-sink:main (floating) +``` + +The Deployment currently references `:dev`; a GitOps apply will pin it +to a `:` once the image build pipeline has produced one. diff --git a/deploy/verification-service/00-serviceaccount.yaml b/deploy/verification-service/00-serviceaccount.yaml new file mode 100644 index 0000000..9676bf9 --- /dev/null +++ b/deploy/verification-service/00-serviceaccount.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: verification-service + namespace: agent-trust + labels: + app.kubernetes.io/name: verification-service + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: trust-core +# verification-service does not call the Kubernetes API server (DEV-612 +# scope: HTTP verification only), so the default auto-mounted SA token +# serves no purpose and is disabled to keep the pod boundary tight. +automountServiceAccountToken: false diff --git a/deploy/verification-service/10-secret-dev.example.yaml b/deploy/verification-service/10-secret-dev.example.yaml new file mode 100644 index 0000000..98d2e6d --- /dev/null +++ b/deploy/verification-service/10-secret-dev.example.yaml @@ -0,0 +1,28 @@ +# verification-service dev secret — EXAMPLE ONLY. +# +# This file is a template. The real cluster Secret MUST be created either +# as a SealedSecret (preferred, see deploy/verification-service/README.md) +# or applied imperatively with values that never land in the git repo. +# +# Secret shape (Phase 1): +# VERIFICATION_SIGNER_KEYS — comma- / newline-separated keyid=: +# loaded into the in-memory keyring. +# Encodings: pem | hex | b64 | b64u. +# +# The verifier itself does not reach OpenBao Transit in Phase 1; it only +# needs the public halves of the approval and transform signers to +# validate DSSE envelopes it pulls from the transparency-log. +apiVersion: v1 +kind: Secret +metadata: + name: verification-service-dev + namespace: agent-trust + labels: + app.kubernetes.io/name: verification-service + app.kubernetes.io/part-of: proofedmesh + proofedmesh.basicstack.de/mode: dev +type: Opaque +stringData: + VERIFICATION_SIGNER_KEYS: | + trust-api/approval-signer-2026-q4=pem:REPLACE_WITH_PEM_PUBLIC_KEY + trust-api/agent-testagent-transform-2026-q4=hex:REPLACE_WITH_64_HEX_PUBLIC_KEY diff --git a/deploy/verification-service/20-deployment.yaml b/deploy/verification-service/20-deployment.yaml new file mode 100644 index 0000000..8cc4f00 --- /dev/null +++ b/deploy/verification-service/20-deployment.yaml @@ -0,0 +1,98 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: verification-service + namespace: agent-trust + labels: + app.kubernetes.io/name: verification-service + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: trust-core +spec: + replicas: 2 + revisionHistoryLimit: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/name: verification-service + template: + metadata: + labels: + app.kubernetes.io/name: verification-service + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: trust-core + spec: + serviceAccountName: verification-service + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: verification-service + image: harbor.basicstack.de/basicstack/verification-service:dev + imagePullPolicy: IfNotPresent + args: + - "-listen=:8084" + - "-backend=memory" + env: + - name: TLOG_URL + value: "http://transparency-log.agent-trust.svc.cluster.local" + # Keyids are the Phase-1 defaults; override via the Secret + # below if a different key rotation lands. + - name: APPROVAL_SIGNER_KEY_ID + value: "trust-api/approval-signer-2026-q4" + - name: TRANSFORM_SIGNER_KEY_ID + value: "trust-api/agent-testagent-transform-2026-q4" + # Public-key material for the DSSE validators. The Secret + # name resolves per environment (see 10-secret-dev.example.yaml + # for the shape). + - name: VERIFICATION_SIGNER_KEYS + valueFrom: + secretKeyRef: + name: verification-service-dev + key: VERIFICATION_SIGNER_KEYS + ports: + - name: http + containerPort: 8084 + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 2 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + failureThreshold: 3 + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "1000m" + memory: "512Mi" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: verification-service diff --git a/deploy/verification-service/30-service.yaml b/deploy/verification-service/30-service.yaml new file mode 100644 index 0000000..cd483fd --- /dev/null +++ b/deploy/verification-service/30-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: verification-service + namespace: agent-trust + labels: + app.kubernetes.io/name: verification-service + app.kubernetes.io/part-of: proofedmesh + app.kubernetes.io/component: trust-core +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: verification-service + ports: + - name: http + port: 8084 + targetPort: http + protocol: TCP diff --git a/deploy/verification-service/40-networkpolicy.yaml b/deploy/verification-service/40-networkpolicy.yaml new file mode 100644 index 0000000..d06d9ba --- /dev/null +++ b/deploy/verification-service/40-networkpolicy.yaml @@ -0,0 +1,93 @@ +# NetworkPolicy for verification-service. +# +# Cluster-wide NetworkPolicy enforcement is active on BasicStack (see +# cluster memory on NetworkPolicy activation). Pods in agent-trust +# without an explicit allow-list are isolated; this file is the +# explicit allow-list for verification-service per DEV-615. +# +# Ingress callers (per Phase-1 architecture V1.1 §7): +# - approval-service — supplies the approval predicate when a +# fast chain pre-check is needed. +# - testagent-transform — the transformation agent that submits +# a chain to vet before shipping to the sink. +# - dummy-sink — the Phase-1 sink that calls back into +# verify/chain before persisting a payload. +# +# Egress reachable targets: +# - transparency-log — pulls DSSE envelopes + inclusion proofs +# (agent-trust/transparency-log, port 80). +# - trust-pg — Postgres backing the replay-store +# `verification_decisions` table (store=postgres). +# - Pocket-ID JWKS — out-of-cluster `auth.basicstack.de` JWKS +# fetch for OIDC validators (HTTPS/443). +# - DNS — kube-dns in kube-system for service name +# resolution. +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: verification-service + namespace: agent-trust + labels: + app.kubernetes.io/name: verification-service + app.kubernetes.io/part-of: proofedmesh +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: verification-service + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: approval-service + - podSelector: + matchLabels: + app.kubernetes.io/name: testagent-transform + - podSelector: + matchLabels: + app.kubernetes.io/name: dummy-sink + ports: + - port: 8084 + protocol: TCP + egress: + # transparency-log (in-cluster, agent-trust namespace, port 80). + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: transparency-log + ports: + - port: 8080 + protocol: TCP + # trust-api Postgres (replay-store backend). Reached via the + # `trust-pg` Service in the same namespace. + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: trust-pg + ports: + - port: 5432 + protocol: TCP + # Pocket-ID JWKS discovery is served from the public ingress + # (`auth.basicstack.de`). Egress is cluster-external HTTPS; no + # cluster selector will match, so this is an unrestricted + # TCP/443 egress rule. + - ports: + - port: 443 + protocol: TCP + # DNS egress to kube-dns (cluster hostname resolution + out-of- + # cluster CNAME lookups for Pocket-ID JWKS). + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP diff --git a/deploy/verification-service/README.md b/deploy/verification-service/README.md new file mode 100644 index 0000000..4f69ada --- /dev/null +++ b/deploy/verification-service/README.md @@ -0,0 +1,92 @@ +# verification-service — deployment + +Kubernetes manifests for the ADR-0006 verification-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/verification-service/ +├── 00-serviceaccount.yaml — ServiceAccount (auto-mount off) +├── 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 :8084 +├── 40-networkpolicy.yaml — ingress from approval-service / testagent-transform / dummy-sink, +│ egress to TLog + trust-pg + Pocket-ID JWKS +└── README.md — this file +``` + +## Apply order + +```bash +kubectl apply -f deploy/trillian/00-namespace.yaml # if not yet applied +kubectl apply -f deploy/verification-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/verification-service/20-deployment.yaml +kubectl apply -f deploy/verification-service/30-service.yaml +kubectl apply -f deploy/verification-service/40-networkpolicy.yaml +``` + +Reach the service in-cluster at: + +``` +http://verification-service.agent-trust.svc.cluster.local:8084 +``` + +Health check (also used by Deployment probes): + +``` +GET /healthz → 200 {"status":"ok"} +``` + +Verify endpoint (per ADR-0006 / DEV-612): + +``` +POST /api/v1/verify/chain +Content-Type: application/json +``` + +## K8s secret convention + +The verification-service only reads one Secret — `verification-service-dev` +(or `verification-service-prod` when the key rotation uses production +material). The Secret shape is a single key, `VERIFICATION_SIGNER_KEYS`, +whose value is the comma- or newline-separated keyid=`:` spec +documented in `services/verification-service/README.md`. + +For the GitOps path, convert the created Secret to a SealedSecret with +`kubeseal` and commit the sealed form under the same filename as the +example: + +```bash +kubectl create secret generic verification-service-dev -n agent-trust \ + --from-file=VERIFICATION_SIGNER_KEYS=./signer-keys.spec \ + --dry-run=client -o yaml | \ + kubeseal --controller-namespace=sealed-secrets --format=yaml \ + > deploy/verification-service/10-secret-dev.sealed.yaml +``` + +## NetworkPolicy + +See `40-networkpolicy.yaml`. In short: + +- **Ingress:** `approval-service`, `testagent-transform`, `dummy-sink` + pods (matched by `app.kubernetes.io/name`). +- **Egress:** `transparency-log` (:8080), `trust-pg` (:5432), external + HTTPS/443 for the Pocket-ID JWKS, DNS to `kube-dns`. + +## Image + +Images are built by `.forgejo/workflows/build-images.yml` on every push +to `main` and pushed to: + +``` +harbor.basicstack.de/basicstack/verification-service: +harbor.basicstack.de/basicstack/verification-service:main (floating) +``` + +The Deployment currently references `:dev`; a GitOps apply will pin it +to a `:` once the image build pipeline has produced one.