ProofedMesh/policies/sinks/signing_service.rego
CTO (Paperclip) 30dd2f8b65 signing-service: Pocket-ID JWKS + K8s TokenReview + OPA (DEV-609)
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>
2026-10-25 19:42:40 +00:00

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],
)
}