deploy: verification-service + dummy-sink Phase-1 manifests (DEV-615)
Some checks failed
build-images / signing-service image (push) Successful in 23s
build-images / verification-service image (push) Successful in 48s
build-images / dummy-sink image (push) Successful in 49s
validate / JSON Schema (ajv) (push) Successful in 23s
validate / Rego (opa fmt + opa test) (push) Successful in 15s
phase1-smoke / Phase 1 end-to-end smoke (push) Successful in 1m0s
validate / DB (migrate up/down dry-run) (push) Successful in 24s
validate / Go (vet + test) (push) Has been cancelled

ServiceAccount, Deployment, Service, and NetworkPolicy per
sub-directory, matching the signing-service template (non-root,
read-only rootfs, distroless, agent-trust namespace).

verification-service — NetworkPolicy allows ingress only from
approval-service, testagent-transform, and dummy-sink, and egress
only to transparency-log, trust-pg, Pocket-ID JWKS (HTTPS/443),
and kube-dns.

dummy-sink — adds a 1Gi longhorn PVC `dummy-sink-data` mounted at
/data/dummy-sink, projected trust-api SA token at
/var/run/secrets/tokens/trust-api, Pocket-ID client creds from a
dev Secret, strategy Recreate (RWO PVC). NetworkPolicy allows
ingress from testagent-transform and egress to verification-service,
signing-service, transparency-log, HTTPS/443 (Pocket-ID OIDC), and
kube-dns.

build-images workflow — adds verification-service and dummy-sink
jobs mirroring signing-service's BuildKit + Harbor pipeline.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO 2026-10-25 21:09:14 +00:00
parent 5bbd3b2c68
commit 5d2bf73d84
14 changed files with 888 additions and 0 deletions

View file

@ -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 }}"

View file

@ -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

View file

@ -0,0 +1,22 @@
# PersistentVolumeClaim for the dummy-sink payload store.
#
# Payloads land at /data/dummy-sink/<decision_id>.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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

115
deploy/dummy-sink/README.md Normal file
View file

@ -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=<id>" \
--from-literal="POCKET_ID_CLIENT_SECRET=<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:<git-sha>
harbor.basicstack.de/basicstack/dummy-sink: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.

View file

@ -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

View file

@ -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=<enc>:<data>
# 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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=`<enc>:<data>` 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:<git-sha>
harbor.basicstack.de/basicstack/verification-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.