signing-service: Dockerfile, K8s manifests (agent-trust), image CI, secret convention
- Dockerfile: multi-stage (golang:1.23-alpine3.21 → distroless/static:nonroot), CGO off, trimpath, stripped, UID/GID 65532, read-only rootfs friendly. - deploy/signing-service/: ServiceAccount, Deployment (2 replicas, probes, tight SecurityContext), ClusterIP Service on :8081, NetworkPolicy (ingress from agent-trust, DNS egress), example Secret, and README with the full dev + prod secret convention. - .forgejo/workflows/build-images.yml: builds image on push + PR, pushes :<git-sha> + :main to harbor.basicstack.de on main pushes only. Clone pattern matches validate.yml (sha256 repo, manual git clone). - README updates: service README gains Deployment section; repo README layout now mentions services/signing-service, services/transparency-log, deploy/, and the new workflow. Scope matches DEV-605. OpenBao, OPA and Pocket-ID JWKS stay stubs and move in their own follow-up subtasks; the prod secret shape is documented so those subtasks can plug in without reshaping the Deployment. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
5d338dd725
commit
b52936ce9d
11 changed files with 555 additions and 2 deletions
75
.forgejo/workflows/build-images.yml
Normal file
75
.forgejo/workflows/build-images.yml
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
name: build-images
|
||||||
|
# Build + push container images for ProofedMesh services on every push to
|
||||||
|
# main. PRs run the build to catch Dockerfile breakage but do not push.
|
||||||
|
#
|
||||||
|
# The repository is sha256 — same gotchas as validate.yml apply: no
|
||||||
|
# actions/checkout, clone manually from a trixie/alpine container with
|
||||||
|
# git ≥ 2.42.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
signing-service:
|
||||||
|
name: signing-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/signing-service
|
||||||
|
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
|
||||||
|
git --version
|
||||||
|
docker --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 build \
|
||||||
|
-f services/signing-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'
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
echo "${{ secrets.HARBOR_PASSWORD }}" | \
|
||||||
|
docker login "${REGISTRY}" --username "${{ secrets.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 }}"
|
||||||
|
|
@ -75,8 +75,14 @@ services/
|
||||||
per-diem-calculator/ # deterministic calculator (Go)
|
per-diem-calculator/ # deterministic calculator (Go)
|
||||||
admin-api/ # admin surface for per-diem rate management (Go)
|
admin-api/ # admin surface for per-diem rate management (Go)
|
||||||
attest-reconciler/ # periodic worker: re-signs per_diem_rates rows without rate_change_attestation_ref (Go)
|
attest-reconciler/ # periodic worker: re-signs per_diem_rates rows without rate_change_attestation_ref (Go)
|
||||||
|
signing-service/ # DSSE signing service (Go, ADR-0005) + Dockerfile
|
||||||
|
transparency-log/ # Trillian-backed transparency log (Go, ADR-0005)
|
||||||
|
deploy/
|
||||||
|
trillian/ # Trillian MySQL + log server/signer (namespace agent-trust)
|
||||||
|
signing-service/ # signing-service Deployment/Service/NetworkPolicy + Secret convention
|
||||||
.forgejo/workflows/
|
.forgejo/workflows/
|
||||||
validate.yml # JSON-Schema (ajv) + Rego (opa fmt + opa test) + Go (vet + test)
|
validate.yml # JSON-Schema (ajv) + Rego (opa fmt + opa test) + Go (vet + test)
|
||||||
|
build-images.yml # Container-Images für ProofedMesh-Services → harbor.basicstack.de
|
||||||
```
|
```
|
||||||
|
|
||||||
Weitere Verzeichnisse (weitere Services, Deployment-Manifeste, Senken-/Datenquellen-Adapter) folgen im Zuge der Architekturentwicklung.
|
Weitere Verzeichnisse (weitere Services, Deployment-Manifeste, Senken-/Datenquellen-Adapter) folgen im Zuge der Architekturentwicklung.
|
||||||
|
|
|
||||||
10
deploy/signing-service/00-serviceaccount.yaml
Normal file
10
deploy/signing-service/00-serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: signing-service
|
||||||
|
namespace: agent-trust
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signing-service
|
||||||
|
app.kubernetes.io/part-of: proofedmesh
|
||||||
|
app.kubernetes.io/component: trust-core
|
||||||
|
automountServiceAccountToken: true
|
||||||
30
deploy/signing-service/10-secret-dev.example.yaml
Normal file
30
deploy/signing-service/10-secret-dev.example.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# signing-service dev secret — EXAMPLE ONLY.
|
||||||
|
#
|
||||||
|
# This file is a template. The real cluster secret MUST be created either as
|
||||||
|
# a SealedSecret (preferred, see deploy/signing-service/README.md) or applied
|
||||||
|
# imperatively with values that never land in the git repo.
|
||||||
|
#
|
||||||
|
# Secret shape (dev mode):
|
||||||
|
# SIGNING_BEARERS — comma-separated bearer=subject pairs for the static
|
||||||
|
# token validator (-bearers flag)
|
||||||
|
# KEY_<SLUG> — hex-encoded 64-byte Ed25519 private key per key_ref
|
||||||
|
# ("/" "-" "." → "_", upper-case; see service README)
|
||||||
|
#
|
||||||
|
# Generate a dev key:
|
||||||
|
# go run ./cmd/signing-service -gen-dev-key
|
||||||
|
#
|
||||||
|
# For key_ref "trust-api/rate-change-signer-2026-q4" the env name is
|
||||||
|
# KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: signing-service-dev
|
||||||
|
namespace: agent-trust
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signing-service
|
||||||
|
app.kubernetes.io/part-of: proofedmesh
|
||||||
|
proofedmesh.basicstack.de/mode: dev
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
SIGNING_BEARERS: "dev-token=agent-admin-api"
|
||||||
|
KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4: "REPLACE_WITH_128_HEX_CHARS_FROM_gen-dev-key"
|
||||||
97
deploy/signing-service/20-deployment.yaml
Normal file
97
deploy/signing-service/20-deployment.yaml
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: signing-service
|
||||||
|
namespace: agent-trust
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signing-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: signing-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signing-service
|
||||||
|
app.kubernetes.io/part-of: proofedmesh
|
||||||
|
app.kubernetes.io/component: trust-core
|
||||||
|
spec:
|
||||||
|
serviceAccountName: signing-service
|
||||||
|
automountServiceAccountToken: true
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 65532
|
||||||
|
runAsGroup: 65532
|
||||||
|
fsGroup: 65532
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: signing-service
|
||||||
|
image: harbor.basicstack.de/basicstack/signing-service:dev
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args:
|
||||||
|
- "-listen=:8081"
|
||||||
|
- "-backend=memory"
|
||||||
|
- "-keys=trust-api/rate-change-signer-2026-q4"
|
||||||
|
- "-keys-from-env"
|
||||||
|
- "-bearers=$(SIGNING_BEARERS)"
|
||||||
|
env:
|
||||||
|
# Pulled from the dev Secret below; see deploy/signing-service/README.md
|
||||||
|
# for the full secret shape and the SealedSecret recipe.
|
||||||
|
- name: SIGNING_BEARERS
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: signing-service-dev
|
||||||
|
key: SIGNING_BEARERS
|
||||||
|
- name: KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: signing-service-dev
|
||||||
|
key: KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8081
|
||||||
|
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
|
||||||
|
topologySpreadConstraints:
|
||||||
|
- maxSkew: 1
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
whenUnsatisfiable: ScheduleAnyway
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: signing-service
|
||||||
18
deploy/signing-service/30-service.yaml
Normal file
18
deploy/signing-service/30-service.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: signing-service
|
||||||
|
namespace: agent-trust
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signing-service
|
||||||
|
app.kubernetes.io/part-of: proofedmesh
|
||||||
|
app.kubernetes.io/component: trust-core
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: signing-service
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8081
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
51
deploy/signing-service/40-networkpolicy.yaml
Normal file
51
deploy/signing-service/40-networkpolicy.yaml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# NetworkPolicy for signing-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 signing-service.
|
||||||
|
#
|
||||||
|
# Ingress: anyone in agent-trust (admin-api, attest-reconciler, transparency-log)
|
||||||
|
# plus future callers reaching through an in-cluster ingress controller.
|
||||||
|
# Egress: DNS (kube-system/coredns), and later the OpenBao Transit namespace
|
||||||
|
# and Pocket ID (JWKS) when those backends are wired. For now the
|
||||||
|
# service only needs DNS egress — the memory backend and static
|
||||||
|
# validator are self-contained.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: signing-service
|
||||||
|
namespace: agent-trust
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signing-service
|
||||||
|
app.kubernetes.io/part-of: proofedmesh
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: signing-service
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
name: agent-trust
|
||||||
|
ports:
|
||||||
|
- port: 8081
|
||||||
|
protocol: TCP
|
||||||
|
# Future: add admin-api / app namespaces here once they are deployed and
|
||||||
|
# labelled. Keep the list conservative — add on demand, not speculatively.
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: kube-dns
|
||||||
|
ports:
|
||||||
|
- port: 53
|
||||||
|
protocol: UDP
|
||||||
|
- port: 53
|
||||||
|
protocol: TCP
|
||||||
165
deploy/signing-service/README.md
Normal file
165
deploy/signing-service/README.md
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
# signing-service — deployment
|
||||||
|
|
||||||
|
Kubernetes manifests for the ADR-0005 signing-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/signing-service/
|
||||||
|
├── 00-serviceaccount.yaml — ServiceAccount with projected SA token
|
||||||
|
├── 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 :8081
|
||||||
|
├── 40-networkpolicy.yaml — ingress from agent-trust, DNS egress
|
||||||
|
└── README.md — this file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Apply order
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/trillian/00-namespace.yaml # if not yet applied
|
||||||
|
kubectl apply -f deploy/signing-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/signing-service/20-deployment.yaml
|
||||||
|
kubectl apply -f deploy/signing-service/30-service.yaml
|
||||||
|
kubectl apply -f deploy/signing-service/40-networkpolicy.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Reach the service in-cluster at:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://signing-service.agent-trust.svc.cluster.local:8081
|
||||||
|
```
|
||||||
|
|
||||||
|
Health check (also used by Deployment probes):
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /healthz → 200 {"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sign endpoint (per ADR-0005):
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/sign/dsse
|
||||||
|
Authorization: Bearer <token from SIGNING_BEARERS>
|
||||||
|
```
|
||||||
|
|
||||||
|
## K8s secret convention
|
||||||
|
|
||||||
|
The signing-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 `signing-service-<mode>`. This keeps the SealedSecret
|
||||||
|
rotation path uniform and lets the Deployment stay identical across modes.
|
||||||
|
|
||||||
|
### Dev mode (`-backend=memory`)
|
||||||
|
|
||||||
|
Secret: `signing-service-dev`
|
||||||
|
|
||||||
|
| Env key | Purpose |
|
||||||
|
| ------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| `SIGNING_BEARERS` | `-bearers` value, comma-separated `bearer=subject` pairs |
|
||||||
|
| `KEY_<SLUG>` | one entry per `key_ref`; value is a hex-encoded 64-byte Ed25519 private key |
|
||||||
|
|
||||||
|
Slug rule: upper-case the `key_ref`, then replace `/`, `-`, `.` with `_`. For
|
||||||
|
`trust-api/rate-change-signer-2026-q4` the env name is
|
||||||
|
`KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4`.
|
||||||
|
|
||||||
|
Mint a dev key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/signing-service
|
||||||
|
go run ./cmd/signing-service -gen-dev-key
|
||||||
|
# → prints 128 hex chars (64 bytes raw Ed25519 private key)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the Secret for the cluster (never commit the raw value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BEARER_TOKEN="$(openssl rand -hex 32)"
|
||||||
|
HEX_KEY="$(cd services/signing-service && go run ./cmd/signing-service -gen-dev-key)"
|
||||||
|
|
||||||
|
kubectl create secret generic signing-service-dev \
|
||||||
|
-n agent-trust \
|
||||||
|
--from-literal="SIGNING_BEARERS=${BEARER_TOKEN}=agent-admin-api" \
|
||||||
|
--from-literal="KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4=${HEX_KEY}"
|
||||||
|
```
|
||||||
|
|
||||||
|
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 signing-service-dev -n agent-trust \
|
||||||
|
--from-literal="SIGNING_BEARERS=${BEARER_TOKEN}=agent-admin-api" \
|
||||||
|
--from-literal="KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4=${HEX_KEY}" \
|
||||||
|
--dry-run=client -o yaml | \
|
||||||
|
kubeseal --controller-namespace=sealed-secrets --format=yaml \
|
||||||
|
> deploy/signing-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 yet committed — ships
|
||||||
|
when the first real deployment lands).
|
||||||
|
|
||||||
|
### Prod mode (future, out of scope for DEV-605)
|
||||||
|
|
||||||
|
Secret: `signing-service-prod`
|
||||||
|
|
||||||
|
Planned env keys — kept here so future subtasks don't invent a new shape:
|
||||||
|
|
||||||
|
| Env key | Purpose | Subtask |
|
||||||
|
| ---------------------------- | ----------------------------------------------------------------- | ------------------------ |
|
||||||
|
| `OPENBAO_ADDR` | OpenBao server URL (`https://openbao.agent-trust.svc:8200`) | OpenBao Transit signer |
|
||||||
|
| `OPENBAO_ROLE` | AppRole / Kubernetes auth role name for the signing-service pod | OpenBao Transit signer |
|
||||||
|
| `OPENBAO_TOKEN` (bootstrap) | optional bootstrap token; prefer SA-token-based login once wired | OpenBao Transit signer |
|
||||||
|
| `POCKET_ID_JWKS_URL` | `https://auth.basicstack.de/.well-known/jwks.json` | Pocket-ID JWKS validator |
|
||||||
|
| `POCKET_ID_ISSUER` | `https://auth.basicstack.de` | Pocket-ID JWKS validator |
|
||||||
|
| `POCKET_ID_AUDIENCE` | expected `aud` claim (e.g. `trust-api`) | Pocket-ID JWKS validator |
|
||||||
|
| `OPA_URL` | OPA sidecar / service URL (`http://opa.agent-trust.svc:8181`) | OPA policy evaluator |
|
||||||
|
|
||||||
|
The matching command-line flags (`-backend=openbao`, `-policy=opa`,
|
||||||
|
`-validator=pocket-id`) land with the respective subtasks. Until then the
|
||||||
|
Deployment stays on `-backend=memory` with the dev Secret above; switching
|
||||||
|
is a Deployment patch and a Secret swap — no manifest reshape.
|
||||||
|
|
||||||
|
## Verifying the deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod comes up, probe says OK:
|
||||||
|
kubectl -n agent-trust rollout status deploy/signing-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://signing-service:8081/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
End-to-end sign check (adjust `BEARER_TOKEN` to the one you stored in the
|
||||||
|
Secret):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n agent-trust run curl --image=curlimages/curl:8 -it --rm --restart=Never -- \
|
||||||
|
curl -sS -X POST http://signing-service:8081/api/v1/sign/dsse \
|
||||||
|
-H "Authorization: Bearer ${BEARER_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"key_ref":"trust-api/rate-change-signer-2026-q4",
|
||||||
|
"statement":{"_type":"https://in-toto.io/Statement/v1",
|
||||||
|
"subject":[{"name":"per_diem_rate/PDR-1","digest":{"sha256":"aaaa"}}],
|
||||||
|
"predicateType":"https://basicstack.de/attestations/rate_change/v1",
|
||||||
|
"predicate":{"change_kind":"create","rate_id":"PDR-1"}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image
|
||||||
|
|
||||||
|
Images are built by `.forgejo/workflows/build-images.yml` on every push to
|
||||||
|
`main` and pushed to:
|
||||||
|
|
||||||
|
```
|
||||||
|
harbor.basicstack.de/basicstack/signing-service:<git-sha>
|
||||||
|
harbor.basicstack.de/basicstack/signing-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.
|
||||||
4
services/signing-service/.dockerignore
Normal file
4
services/signing-service/.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
**/*_test.go.bak
|
||||||
|
**/.DS_Store
|
||||||
|
**/*.swp
|
||||||
|
**/*.swo
|
||||||
41
services/signing-service/Dockerfile
Normal file
41
services/signing-service/Dockerfile
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# ---------- build ----------
|
||||||
|
FROM golang:1.23-alpine3.21 AS build
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=0 \
|
||||||
|
GOOS=linux \
|
||||||
|
GOFLAGS="-trimpath"
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Only the signing-service module is needed. Copy the module files first so
|
||||||
|
# the dependency download layer is cacheable independently of source edits.
|
||||||
|
COPY services/signing-service/go.mod ./
|
||||||
|
# go.sum will be copied in when the module gains external deps; today the
|
||||||
|
# module is stdlib-only.
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
go mod download || true
|
||||||
|
|
||||||
|
COPY services/signing-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/signing-service ./cmd/signing-service
|
||||||
|
|
||||||
|
# ---------- runtime ----------
|
||||||
|
# Distroless static+nonroot: no shell, no package manager, UID/GID 65532.
|
||||||
|
# gcr.io/distroless/static-debian12:nonroot pins to the latest nonroot
|
||||||
|
# variant; CI tags images by git sha so this is reproducible at the point
|
||||||
|
# of build.
|
||||||
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
|
COPY --from=build /out/signing-service /signing-service
|
||||||
|
|
||||||
|
USER 65532:65532
|
||||||
|
EXPOSE 8081
|
||||||
|
ENTRYPOINT ["/signing-service"]
|
||||||
|
CMD ["-listen=:8081", "-backend=memory"]
|
||||||
|
|
@ -19,7 +19,9 @@ admin-api and other clients use as the single attestation reference.
|
||||||
- **Pocket ID JWKS verifier**: stub. Dev uses the static `-bearers` table.
|
- **Pocket ID JWKS verifier**: stub. Dev uses the static `-bearers` table.
|
||||||
Separate subtask.
|
Separate subtask.
|
||||||
- **OPA policy decider**: stub. Dev uses AllowAll. Separate subtask.
|
- **OPA policy decider**: stub. Dev uses AllowAll. Separate subtask.
|
||||||
- **Deployment manifest**: separate subtask.
|
- **Deployment manifest**: Kubernetes manifests under
|
||||||
|
[`deploy/signing-service/`](../../deploy/signing-service/) + Dockerfile
|
||||||
|
next to this README. See **Deployment** below.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
|
@ -111,3 +113,57 @@ Prod wiring in the OpenBao-Transit subtask: Signer backend becomes
|
||||||
backend becomes `policy.OPA` with the Rego bundle under `policies/signing/`
|
backend becomes `policy.OPA` with the Rego bundle under `policies/signing/`
|
||||||
(not yet created), token validator becomes `token.PocketID` with JWKS from
|
(not yet created), token validator becomes `token.PocketID` with JWKS from
|
||||||
`https://auth.basicstack.de/.well-known/jwks.json`.
|
`https://auth.basicstack.de/.well-known/jwks.json`.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Container image
|
||||||
|
|
||||||
|
Multi-stage build in `services/signing-service/Dockerfile`:
|
||||||
|
|
||||||
|
- build stage: `golang:1.23-alpine3.21`, runs `go vet` + `go test` before `go build`
|
||||||
|
- runtime stage: `gcr.io/distroless/static-debian12:nonroot` (UID/GID 65532, no shell)
|
||||||
|
- the final binary is static (`CGO_ENABLED=0`), stripped (`-ldflags="-s -w"`),
|
||||||
|
reproducible (`-trimpath`)
|
||||||
|
|
||||||
|
Build locally from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f services/signing-service/Dockerfile \
|
||||||
|
-t harbor.basicstack.de/basicstack/signing-service:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
CI (`.forgejo/workflows/build-images.yml`) builds on every push + PR to
|
||||||
|
`main` and pushes `:<git-sha>` + `:main` to Harbor on main pushes only.
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
Manifests live under
|
||||||
|
[`deploy/signing-service/`](../../deploy/signing-service/) and target the
|
||||||
|
`agent-trust` namespace. See
|
||||||
|
[`deploy/signing-service/README.md`](../../deploy/signing-service/README.md)
|
||||||
|
for the full secret convention (dev + prod layout), the SealedSecret
|
||||||
|
recipe, and the apply order.
|
||||||
|
|
||||||
|
Minimal example (dev mode, in-memory key, static bearer):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BEARER_TOKEN="$(openssl rand -hex 32)"
|
||||||
|
HEX_KEY="$(go run ./cmd/signing-service -gen-dev-key)"
|
||||||
|
|
||||||
|
kubectl apply -f ../../deploy/trillian/00-namespace.yaml
|
||||||
|
kubectl apply -f ../../deploy/signing-service/00-serviceaccount.yaml
|
||||||
|
|
||||||
|
kubectl create secret generic signing-service-dev -n agent-trust \
|
||||||
|
--from-literal="SIGNING_BEARERS=${BEARER_TOKEN}=agent-admin-api" \
|
||||||
|
--from-literal="KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4=${HEX_KEY}"
|
||||||
|
|
||||||
|
kubectl apply -f ../../deploy/signing-service/20-deployment.yaml
|
||||||
|
kubectl apply -f ../../deploy/signing-service/30-service.yaml
|
||||||
|
kubectl apply -f ../../deploy/signing-service/40-networkpolicy.yaml
|
||||||
|
|
||||||
|
kubectl -n agent-trust rollout status deploy/signing-service
|
||||||
|
```
|
||||||
|
|
||||||
|
In-cluster the service is reachable at
|
||||||
|
`http://signing-service.agent-trust.svc.cluster.local:8081` (health check
|
||||||
|
`/healthz`).
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue