approval-service: K8s deployment manifests + Pocket-ID OIDC client (DEV-614)
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
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
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>
This commit is contained in:
parent
5d2bf73d84
commit
20ba29d3ac
9 changed files with 612 additions and 0 deletions
|
|
@ -170,6 +170,74 @@ jobs:
|
|||
docker push "${{ steps.tags.outputs.image_sha }}"
|
||||
docker push "${{ steps.tags.outputs.image_main }}"
|
||||
|
||||
approval-service:
|
||||
name: approval-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/approval-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/approval-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
|
||||
|
|
|
|||
13
deploy/approval-service/00-serviceaccount.yaml
Normal file
13
deploy/approval-service/00-serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: approval-service
|
||||
namespace: agent-trust
|
||||
labels:
|
||||
app.kubernetes.io/name: approval-service
|
||||
app.kubernetes.io/part-of: proofedmesh
|
||||
app.kubernetes.io/component: trust-core
|
||||
# The approval-service does not call the Kubernetes API; it talks only to
|
||||
# signing-service, transparency-log, trust-pg, and Pocket-ID. Opt the SA
|
||||
# out of auto-mounting since no in-pod kube client is used.
|
||||
automountServiceAccountToken: false
|
||||
32
deploy/approval-service/10-secret-dev.example.yaml
Normal file
32
deploy/approval-service/10-secret-dev.example.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# approval-service dev secret — EXAMPLE ONLY.
|
||||
#
|
||||
# This file is a template. The real cluster secret MUST be created either
|
||||
# as a SealedSecret (preferred, see deploy/approval-service/README.md) or
|
||||
# applied imperatively with values that never land in the git repo.
|
||||
#
|
||||
# Secret shape:
|
||||
# APPROVAL_SIGNING_TOKEN — Bearer token the approval-service presents
|
||||
# to the signing-service (dev: match the
|
||||
# `SIGNING_BEARERS` entry on signing-service)
|
||||
# APPROVAL_POCKET_ID_CLIENT — Pocket-ID OIDC client ID (returned by the
|
||||
# client registration in README)
|
||||
# APPROVAL_POCKET_ID_SECRET — Pocket-ID OIDC client secret
|
||||
# APPROVAL_WEBAUTHN_RP_ID — relying party ID shown to the browser
|
||||
# APPROVAL_WEBAUTHN_RP_ORIGIN — allowed origin for client_data_json
|
||||
#
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: approval-service-dev
|
||||
namespace: agent-trust
|
||||
labels:
|
||||
app.kubernetes.io/name: approval-service
|
||||
app.kubernetes.io/part-of: proofedmesh
|
||||
proofedmesh.basicstack.de/mode: dev
|
||||
type: Opaque
|
||||
stringData:
|
||||
APPROVAL_SIGNING_TOKEN: "REPLACE_WITH_SIGNING_SERVICE_BEARER"
|
||||
APPROVAL_POCKET_ID_CLIENT: "REPLACE_WITH_POCKET_ID_CLIENT_ID"
|
||||
APPROVAL_POCKET_ID_SECRET: "REPLACE_WITH_POCKET_ID_CLIENT_SECRET"
|
||||
APPROVAL_WEBAUTHN_RP_ID: "approval.proofedmesh.basicstack.de"
|
||||
APPROVAL_WEBAUTHN_RP_ORIGIN: "https://approval.proofedmesh.basicstack.de"
|
||||
19
deploy/approval-service/10-secret-dev.sealed.yaml
Normal file
19
deploy/approval-service/10-secret-dev.sealed.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: bitnami.com/v1alpha1
|
||||
kind: SealedSecret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: approval-service-dev
|
||||
namespace: agent-trust
|
||||
spec:
|
||||
encryptedData:
|
||||
APPROVAL_POCKET_ID_CLIENT: AgCOz00SToarJgnN2+ig0CB/Pk2nlPu9GJx7PtDXiWwPCeZ08O7jf+uMRV/HZxAJ8zS3UCKe7oW+x0/v0eY/oSbGk2FL0aYeNb0+Q6ZNBfO52DPzJzd0AqGenAqbR7eN/rkrZIGFQyI2gAtQPCwEYMW9tIdR99tH/OiN3gh6NMV1VHiSz2nrpBLySct5K13OHjIa559nxhgbplnolXjFMEfTKoJUWTG5e0M42uBvg3OGMMDR1rAzw30e7Oc/O5WvB3dZbYps/qQzMooQdlm0rlxCmZdgddIKmDz2qM55qPql48F4Ehwi+H4uIqnjlEMfX9zS8F5SnIqIwVMUOmlon2i7vuWPyK/ndXIVAg/zKUnbAy22lwP3/38gAfLuq8p7d7kUoPyVGTjQTGdHeRr0j3xaMULB5OGGRE4pSZcc9IWmz08yUIAQqamqRavvcGMMjS7alUMUA/710b5S2mFNdjHhr0rfX3JZdFZjTR988J7fqIuzeCF6OK8TbPKmcAH/f1Zup0uxek60enZ0qDA1A9LXej8fjXEZoKaVENr+S29vwhez0hwSvXWBWCd0ClH8/PFzhDye9/aP+myMUElJPqbSDyrnwJL3T+709jUsgyok6k0vYfi7xZbpyNw0ozff7sL43Loim7DTLrMeCFFwXd5z8lmC/VG7LTudSXXgJ//852I8vHGoJIcpU55qJNBhf+AWOVVy+Zl+q27pF7e3ZVyQNI7iQN6knWrqJ8E1TnaA80xvtKw=
|
||||
APPROVAL_POCKET_ID_SECRET: AgAnKttGUyDZ1BWt5VReGPFyzlwMYcQdwUyvjUP+cM9U3M9YpizOGcm+8UHFc3cizBwNoS6Sr1WgrNJebq0DuQNWqki/egNn3fhS/RDDIdCei+u9i/e5dns1u+LxBDvNbeBCFii8rQEBc7vyFtxFnKLrPLca7S+Ij4cqZilzgr0oo56yhsvAC1gDtXOgj4KqfEKmKP6Z4q+6YGCuRY69jCLm6kojhXsOBSuJTxYA8mi7qnDlH52FcMidMcyF0HxYiVqDl85shKb2fozGIxQLt/FFQZWBzP7EZPCEvK1+ZCN/k2ttVmfbByqJaUlmRpY8jS+btfTzavJYbApMLZNaw91MyUaeC1NfqkqUqv86NfaZFrOOwrZiUbF32LAMLcXUdH/KHokZdNgB0nan7971ZxM/gQ4m+tQpFovqmIDiA/acic8wVW64AtbQ4yC1q7lMzFis773V7ARn91dYg5/6vuBrzhITWYpHSyNbrIrW1q3ffB9tz/cFVBY0TLEmPlun3lmy7muzMTsR7aYp14hzRfeN0zSMEBzTqsbsL7ZlJk8poxIpHgRmbiJE0pAWK7+JIW2X+fylnfrIMEdHB/HtfPomAq8XpSGXfmFvyNcbQmPnKDnGDz7mF6n+K9g8on8TnPiHO/dig1TQ4eERbEOJRt5PEM+1/zJyhAoJcg3iJZqaW/wVXqisFrT11C3zwZLXZm+PbkJuJTnBbVWg8n5jI2k4Nv85PoD9QqhGpZsi6U4zBg==
|
||||
APPROVAL_SIGNING_TOKEN: AgCLoX/iq/XtDmCU0Fv3EIRKdVl3CELcNq4REz3hQJmMywFiAZpFnv1KGACjHvPOKoAwQdliwj0SrUXI1L6odVGCRVZtAratkujniYVSXQROTeYeUsqfC5MqBJivPT9D/7LCxfbscErilYQbedHaT+2OS0RL8gt96hVYbOfuRfpm5zGTZa02YeEpgHMnsd8huuSsyuhCnopnmQkR26qLNWfzMC4AQmu0sj+UrStoMga/zMEPem5HZVDfP3YdustuqCm49DzWGxJKizielN/OyLYdpeFWYZHeReFgniD+K4qL4aZTGGxwpxx+GUDLlVMq8C6uVJTahGx4tJal05kg7R/RL2kbN6ZWa+fAXG+a6cX6ne8NpVlsRDxKVge29zl4HreB9HmdPSe9WLws1eLa2UQPBLRDBQXXW+hDw94V0ohQr30HwODCQu832SwCNA/4BDOnhcyMPrhtU/YGS4gFZU6jd89Y70ovxoj9YiZUfcuTbYU8OApYPXOza8rAElesJVL44d0yevj0JM7Xh1/FuEATjbCRkUKT1LKRMnYr9pxMMbqZKa73av7k1i76mxUDOZPfeHPseWt6LsA3P76Kb53MQtaOaJR3Rp5Ex3qyuLU6frM0TGQIrRXqYkHxinzXxf48Sty6lKu4VuP8AbWkPHH8O5OMk2XQNNjuMF5zc1LeStY6gl8uPe2j3abfvIxSm21rfRJBIhypwoiYLlV9++RSrP1mQ10d3QWhB48uY0GfJWoFS6WVBTb85Apve+0cA/i1zCcCytifGdyskXM5BYa4
|
||||
APPROVAL_WEBAUTHN_RP_ID: AgAMCOBsvVGWBGDCf95hGeg/cLMiDVL9wnWipG1vjQHZxSiZJaHLV+0L9Tp70Ntha7/P552G9BwCz06XYibTILLHXmTO/RNN4am8tTE3dj3ktWotgNJBMBsrrOpE6ch6KHAZVZHwCwcUc4VwwjohutbtMN9iJuvUltaLQSwVA5CAkKHom56Rj2E5+ycSFgAdG0vjfhqpYWsdj+/XJum+w6p5Pe/QbfkKfr8f3nHWtoDbSLpeVp1G9NzFKF8ylTKi9yKYzPm27DGrJKzJ3yPuCI8arblF79v8cydKuLQ7+7WAtAPGz/VImCvpK+HqguwhtKUQdcQFmJrBvyN9msQHkDSAFKE3YyTxprO/2Crq9Az201S1jKoKCb9FlIhryRpdeUxlcCyf3jcBTayTIdiQXfaSily+NRFmgzb/W13qWPF+Zj/LW+ICY2v3S17J7DA1JfgYHqlzu4WjhDix8jAefFIm0+XKjSkTXYbcnCdpGyj+o9i7HXBB+rBkK5WW+Q9fBjKaPHKEXbeTc9/5D8RuNpvt1lDqfvEYsQEnkikWfRZABfIDKKKc+VfaLo9qmledaPLfMjSYG+B9b4oxfMgYwovC3Vj9elo4II5GPiWodHnVoiZvR442qEk8HFDMRRqOFINhJJUZmaAf4QgKdYCao6pGHCBMYCQb+8dSaVoBBzN8vjdAo2hXBKO6ZmbzXPJ39+XHX6Mynvbe1y38+r7cJofJiNPN8XmNi2WQdloI0t+zVD6b
|
||||
APPROVAL_WEBAUTHN_RP_ORIGIN: AgBwTEgNGOmO+/Dai5/Ul/zOPj3/JwLvQy3Uu7TmvvowvjG4d5fwXzX3ROOGwgM4CwcjYrjKCBv6QvEykAAjusqcqzmwuuF5dlQd6PE0PaXEi+qwxo15G0MPbwX4HyC8uLQW/X9wE3W2yUdYPicA6BUg27K3TUTaocTEFL3kiKbN0CYSYeZHcyZMrnytu+e1dfHF3Td3Cbzs6wO1c41KkXw9SKfiCQh+QdB94d19/pB148vcHFVeIKqm50FSUJEjwCvm09yki4QajSjleyz5KL+6xql/xFreN1660uryLjGuQaNVKcVx6Kmpq9xXeMbcx2peNex+JnMOGmVQyYKW9U7BdvZDsMhv0alJNm4q9sy+cn195YJ8rdIfXoQpPVwwXUFTGWIckpGtIdrSyDfj6rIVTt44GSI6Bnb99gKiYKLx6BUF9ElMFPdxeQEP6bxZ0m4aZJrD/LEbZ6T14d1oaWWhWNmsZyQ3BQ8IPXn/RfWmDMxQXshPdjhcx6p3Z9SLSCslRXl4AKfJ541u2vXHMaFGi4tx3t3qGXYssTQwaZqbNg8H33A+m8SpAgmarjZr39cZrxdYpuxFrURz2OiLX0V3POQvUTG5H3J8f9fcBwWCye6DCcpLxILRtnScZ9qtM2lLKe2QsoqVvlIDfOJzeieTztjEoegQen9xrNzitif75CQkyVSPNpmySVGVgPP0ePnAT/LL8XwlwaQr1UC7g5Mn7gffMphlJEfS0d5JJozs2ilDPslvVHwE3Xw=
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: approval-service-dev
|
||||
namespace: agent-trust
|
||||
---
|
||||
120
deploy/approval-service/20-deployment.yaml
Normal file
120
deploy/approval-service/20-deployment.yaml
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: approval-service
|
||||
namespace: agent-trust
|
||||
labels:
|
||||
app.kubernetes.io/name: approval-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: approval-service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: approval-service
|
||||
app.kubernetes.io/part-of: proofedmesh
|
||||
app.kubernetes.io/component: trust-core
|
||||
spec:
|
||||
serviceAccountName: approval-service
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: approval-service
|
||||
image: harbor.basicstack.de/basicstack/approval-service:main
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- "-listen=:8080"
|
||||
- "-signing-url=http://signing-service.agent-trust.svc.cluster.local:8081"
|
||||
- "-signing-key-ref=trust-api/approval-signer-2026-q4"
|
||||
- "-tlog-url=http://transparency-log.agent-trust.svc.cluster.local:8080"
|
||||
- "-session-ttl=15m"
|
||||
- "-challenge-ttl=2m"
|
||||
env:
|
||||
# ---- Pocket-ID OIDC issuer for attestation `issuer` field +
|
||||
# future auth middleware discovery (DEV-614). ----
|
||||
- name: APPROVAL_POCKET_ID_ISSUER
|
||||
value: "https://auth.basicstack.de"
|
||||
- name: APPROVAL_POCKET_ID_CLIENT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: approval-service-dev
|
||||
key: APPROVAL_POCKET_ID_CLIENT
|
||||
# ---- Pocket-ID client secret (OIDC authorization_code +
|
||||
# client_credentials; Phase 2 middleware consumes it). ----
|
||||
- name: APPROVAL_POCKET_ID_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: approval-service-dev
|
||||
key: APPROVAL_POCKET_ID_SECRET
|
||||
# ---- WebAuthn RP config (DEV-608): must match the browser
|
||||
# origin reached via Pangolin/Traefik. ----
|
||||
- name: APPROVAL_WEBAUTHN_RP_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: approval-service-dev
|
||||
key: APPROVAL_WEBAUTHN_RP_ID
|
||||
- name: APPROVAL_WEBAUTHN_RP_ORIGIN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: approval-service-dev
|
||||
key: APPROVAL_WEBAUTHN_RP_ORIGIN
|
||||
# ---- Bearer the approval-service presents to signing-service. ----
|
||||
- name: APPROVAL_SIGNING_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: approval-service-dev
|
||||
key: APPROVAL_SIGNING_TOKEN
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
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: "500m"
|
||||
memory: "256Mi"
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
topologyKey: kubernetes.io/hostname
|
||||
whenUnsatisfiable: ScheduleAnyway
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: approval-service
|
||||
18
deploy/approval-service/30-service.yaml
Normal file
18
deploy/approval-service/30-service.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: approval-service
|
||||
namespace: agent-trust
|
||||
labels:
|
||||
app.kubernetes.io/name: approval-service
|
||||
app.kubernetes.io/part-of: proofedmesh
|
||||
app.kubernetes.io/component: trust-core
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: approval-service
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
98
deploy/approval-service/40-networkpolicy.yaml
Normal file
98
deploy/approval-service/40-networkpolicy.yaml
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# NetworkPolicy for approval-service.
|
||||
#
|
||||
# Cluster-wide NetworkPolicy enforcement is already active on BasicStack
|
||||
# (see cluster memory on NetworkPolicy activation). Any pod in agent-trust
|
||||
# without a policy is isolated by default; this file is the explicit
|
||||
# allow-list for approval-service.
|
||||
#
|
||||
# Ingress: only from the Pangolin ingress edge (public traffic terminates
|
||||
# at Pangolin/Traefik; nothing in-cluster calls approval-service
|
||||
# directly in Phase 1).
|
||||
# Egress: signing-service (:8081) + transparency-log (:8080) in
|
||||
# agent-trust, Pocket-ID (:1411) for OIDC discovery/JWKS, DNS, and (once
|
||||
# trust-pg lands) a Postgres :5432 egress to trust-pg in agent-trust.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: approval-service
|
||||
namespace: agent-trust
|
||||
labels:
|
||||
app.kubernetes.io/name: approval-service
|
||||
app.kubernetes.io/part-of: proofedmesh
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: approval-service
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
# Public traffic reaches approval-service only via the Pangolin edge
|
||||
# (Traefik lives inside the Pangolin namespace). Keep this list
|
||||
# conservative — add additional callers on demand, not speculatively.
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: pangolin
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
egress:
|
||||
# signing-service (ADR-0005 signer) + transparency-log (Trillian frontend)
|
||||
# live in the same agent-trust namespace; future trust-pg will too.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: agent-trust
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: signing-service
|
||||
ports:
|
||||
- port: 8081
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: agent-trust
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: transparency-log
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
# Future trust-pg Postgres egress — pre-authorised here so the pod
|
||||
# does not need a NetworkPolicy edit when DEV-607's prod store lands.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: agent-trust
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: trust-pg
|
||||
ports:
|
||||
- port: 5432
|
||||
protocol: TCP
|
||||
# Pocket-ID for OIDC discovery + token endpoint.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: pocket-id
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: pocket-id
|
||||
ports:
|
||||
- port: 1411
|
||||
protocol: TCP
|
||||
# DNS egress to kube-system CoreDNS.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- port: 53
|
||||
protocol: UDP
|
||||
- port: 53
|
||||
protocol: TCP
|
||||
206
deploy/approval-service/README.md
Normal file
206
deploy/approval-service/README.md
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# 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.
|
||||
38
services/approval-service/Dockerfile
Normal file
38
services/approval-service/Dockerfile
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# ---------- build ----------
|
||||
FROM golang:1.23-alpine3.21 AS build
|
||||
|
||||
ENV CGO_ENABLED=0 \
|
||||
GOOS=linux \
|
||||
GOFLAGS="-trimpath"
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Pre-cache module metadata before copying sources. approval-service is
|
||||
# currently stdlib-only (no go.sum), but `go mod download` on a sum-less
|
||||
# module is a no-op and keeps the layer stable if a go.sum is added later.
|
||||
COPY services/approval-service/go.mod ./services/approval-service/
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
cd services/approval-service && go mod download
|
||||
|
||||
COPY services/approval-service/ ./services/approval-service/
|
||||
|
||||
WORKDIR /src/services/approval-service
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go vet ./... && \
|
||||
go test ./... && \
|
||||
go build -ldflags="-s -w" -o /out/approval-service ./cmd/approval-service
|
||||
|
||||
# ---------- runtime ----------
|
||||
# Distroless static+nonroot: no shell, no package manager, UID/GID 65532.
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
COPY --from=build /out/approval-service /approval-service
|
||||
|
||||
USER 65532:65532
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/approval-service"]
|
||||
CMD ["-listen=:8080"]
|
||||
Loading…
Add table
Reference in a new issue