Wires the signing-service's production path for ProofedMesh Phase 1: the service now performs a double identity check (Pocket-ID OIDC JWT + K8s ServiceAccount TokenReview), reconciles both against the trust-api `agents` registry, and asks an OPA/Rego policy whether the caller may sign with the requested key_ref. Code - internal/token/pocket_id.go: OIDC-Discovery-backed JWKS validator with automatic kid rotation (coreos/go-oidc). - internal/token/k8s_tokenreview.go: hand-rolled TokenReview client; reads X-Workload-Token, posts to authentication.k8s.io/v1 via the pod's in-cluster SA token + CA. - internal/registry: Postgres resolver for the agents table + in-memory Static resolver for tests. - internal/policy/opa.go: OPA/rego StructuredDecider; input carries both lookups so the policy can enforce identity consistency AND the key_ref ↔ role mapping. - internal/httpapi/sign.go: new structured flow when Validator+TokenReviewer+Registry+StructuredPolicy are all wired; dev path (static bearers + AllowAll) remains intact. - cmd/signing-service/main.go: -pocket-id-issuer, -pocket-id-audience, -k8s-tokenreview, -registry-dsn, -policy-bundle flags. Policy - policies/sinks/signing_service.rego: identities_agree + key_ref_matches_role rules; agent → key_ref prefix map covers approval-service, testagent-transform, dummy-sink. Deploy - deploy/signing-service/01-rbac.yaml: ClusterRole with tokenreviews.create + binding to the signing-service SA. - deploy/signing-service/20-deployment.yaml: adds POCKET_ID_ISSUER, POCKET_ID_AUDIENCE, TRUST_PG_DSN env, projected SA token volume, OPA policy ConfigMap mount, structured flags. - deploy/signing-service/00-serviceaccount.yaml: disables default auto-mount; a projected volume is used. - deploy/signing-service/README.md: documents prod flags + ConfigMap generation. Tests - httptest-mock Pocket-ID (OIDC discovery + JWKS, kid rotation), StaticTokenReviewer fake, Static registry, real Rego policy. - Happy paths: Approval-Service → trust-api/approval-signer-2026-q4; testagent-transform → trust-api/agent-testagent-transform-2026-q4. - Denies: registry mismatch, wrong key_ref for role, missing / bad workload token, bad Pocket-ID token, kid rotation. Co-Authored-By: Paperclip <noreply@paperclip.ing>
74 lines
2.8 KiB
Rego
74 lines
2.8 KiB
Rego
# METADATA
|
|
# title: Signing-service policy (ProofedMesh Phase 1)
|
|
# description: |
|
|
# Decides whether an authenticated client may obtain a DSSE signature for a
|
|
# given key_ref. Enforces two independent conditions:
|
|
#
|
|
# 1. The Pocket-ID client identity (`sub` claim) and the Kubernetes
|
|
# ServiceAccount identity (`<ns>/<sa>` from TokenReview) resolve to the
|
|
# same agent row in the trust-api `agents` table.
|
|
# 2. The requested `key_ref` is in the set of key_refs this agent's role
|
|
# is permitted to sign with.
|
|
#
|
|
# Both lookups are passed in as `input.registry.by_pocket_id` and
|
|
# `input.registry.by_k8s`. The signing-service MUST perform both lookups
|
|
# from the DB and hand the raw rows (or `null` on no match) to OPA — the
|
|
# policy does not reach out to the DB on its own.
|
|
# authors:
|
|
# - CTO Agent <cto@basicstack.de>
|
|
# related_resources:
|
|
# - ref: docs/architecture/v1.1.md#user-content-44-identitaet-und-schluessel
|
|
# - ref: db/trust/migrations/0002_agents.up.sql
|
|
package proofedmesh.sinks.signing_service
|
|
|
|
import rego.v1
|
|
|
|
default allow := false
|
|
|
|
# Map agent_id → set of allowed key_ref prefixes. Prefixes end at the
|
|
# quarter boundary so we can rotate the suffix (`-2026-q4`, `-2027-q1`, …)
|
|
# without changing the policy. Keep this list in sync with the trust-api
|
|
# agent registry seeds (db/trust/seeds/0001_phase1_agents.sql).
|
|
agent_key_prefixes := {
|
|
"agent-approval-service": {"trust-api/approval-signer-"},
|
|
"agent-testagent-transform": {"trust-api/agent-testagent-transform-"},
|
|
"agent-dummy-sink": {"trust-api/dummy-sink-signer-"},
|
|
}
|
|
|
|
# Both registry lookups must have returned the SAME agent row and that row
|
|
# must be internally consistent with the identities actually presented by
|
|
# the caller (Pocket-ID `sub` and the K8s ServiceAccount reported by
|
|
# TokenReview). Any null, mismatch, or absent field denies.
|
|
identities_agree if {
|
|
input.registry.by_pocket_id.agent_id == input.registry.by_k8s.agent_id
|
|
input.registry.by_pocket_id.pocket_id_client == input.pocket_id_client
|
|
input.registry.by_pocket_id.k8s_namespace == input.k8s.namespace
|
|
input.registry.by_pocket_id.k8s_service_account == input.k8s.service_account
|
|
input.registry.by_k8s.pocket_id_client == input.pocket_id_client
|
|
input.registry.by_k8s.k8s_namespace == input.k8s.namespace
|
|
input.registry.by_k8s.k8s_service_account == input.k8s.service_account
|
|
}
|
|
|
|
key_ref_matches_role if {
|
|
some prefix in agent_key_prefixes[input.registry.by_pocket_id.agent_id]
|
|
startswith(input.key_ref, prefix)
|
|
}
|
|
|
|
allow if {
|
|
identities_agree
|
|
key_ref_matches_role
|
|
}
|
|
|
|
deny_reason contains msg if {
|
|
not identities_agree
|
|
msg := "pocket-id and k8s identities do not resolve to the same registry agent"
|
|
}
|
|
|
|
deny_reason contains msg if {
|
|
identities_agree
|
|
not key_ref_matches_role
|
|
msg := sprintf(
|
|
"agent %q is not permitted to sign with key_ref %q",
|
|
[input.registry.by_pocket_id.agent_id, input.key_ref],
|
|
)
|
|
}
|