Policy Gateway
Delivery scope:
StellaOps.Policy.Gatewayminimal API service fronting Policy Engine pack CRUD + activation endpoints for UI/CLI clients. Sender-constrained with DPoP and tenant headers, suitable for online and Offline Kit deployments.
1 · Responsibilities
- Proxy policy pack CRUD and activation requests to Policy Engine while enforcing scope policies (
policy:read,policy:author,policy:review,policy:operate,policy:activate). - Normalise responses (DTO +
ProblemDetails) so Console, CLI, and automation receive consistent payloads. - Guard activation actions with structured logging and metrics so approvals are auditable.
- Support dual auth modes:
- Forwarded caller tokens (Console/CLI) with DPoP proofs +
X-Stella-Tenantheader. - Gateway client credentials (DPoP) for service automation or Offline Kit flows when no caller token is present.
- Forwarded caller tokens (Console/CLI) with DPoP proofs +
2 · Endpoints
| Route | Method | Description | Required scope(s) |
|---|---|---|---|
/api/policy/packs | GET | List policy packs and revisions for the active tenant. | policy:read |
/api/policy/packs | POST | Create a policy pack shell or upsert display metadata. | policy:author |
/api/policy/packs/{packId}/revisions | POST | Create or update a policy revision (draft/approved). | policy:author |
/api/policy/packs/{packId}/revisions/{version}:activate | POST | Activate a revision, enforcing single/two-person approvals. | policy:operate, policy:activate |
Response shapes
- Successful responses return camel-case DTOs matching
PolicyPackDto,PolicyRevisionDto, orPolicyRevisionActivationDtoas described in the Policy Engine API doc (/docs/api/policy.md). - Errors always return RFC 7807
ProblemDetailswith deterministic fields (title,detail,status). Missing caller credentials now surface401with"Upstream authorization missing"detail.
Dual-control activation
- Config-driven. Set
PolicyEngine.activation.forceTwoPersonApproval=truewhen every activation must collect two distinctpolicy:activateapprovals. When false, operators can opt into dual-control per revision (requiresTwoPersonApproval: true). - Defaults.
PolicyEngine.activation.defaultRequiresTwoPersonApprovalfeeds the default when callers omit the checkbox/flag. - Statuses. First approval on a dual-control revision returns
202 pending_second_approval; duplicate actors get400 duplicate_approval; the second distinct approver receives the usual200 activated. - Audit trail. With
PolicyEngine.activation.emitAuditLogson, Policy Engine emits structuredpolicy.activation.*scopes (pack id, revision, tenant, approver IDs, comments) so the gateway metrics/ELK dashboards can show who approved what.
Activation configuration wiring
- Helm ConfigMap.
deploy/helm/stellaops/values*.yamlnow include apolicy-engine-activationConfigMap. The chart automatically injects it viaenvFrominto both the Policy Engine and Policy Gateway pods, so overriding the ConfigMap data updates the services with no manifest edits. - Type safety. Quote ConfigMap values (e.g.,
"true","false") because Kubernetes ConfigMaps carry string data. This mirrors the defaults checked into the repo and keepshelm templatedeterministic. - File-based overrides (optional). The Policy Engine host already probes
/config/policy-engine/activation.yaml,../etc/policy-engine.activation.yaml, and ambientpolicy-engine.activation.yamlfiles beside the binary. Mounting the ConfigMap as a file at/config/policy-engine/activation.yamlworks immediately if/when we add a volume. - Offline/Compose. Compose/offline bundles can continue exporting
STELLAOPS_POLICY_ENGINE__ACTIVATION__*variables directly; the ConfigMap wiring simply mirrors those keys for Kubernetes clusters.
3 · Authentication & headers
| Header | Source | Notes |
|---|---|---|
Authorization | Forwarded caller token or gateway client credentials. | Caller tokens must include tenant scope; gateway tokens default to DPoP scheme. |
DPoP | Caller or gateway. | Required when Authority mandates proof-of-possession (default). Generated per request; gateway keeps ES256/ES384 key material under etc/policy-gateway-dpop.pem. |
X-Stella-Tenant | Caller | Tenant isolation header. Forwarded unchanged; gateway automation omits it. |
Gateway client credentials are configured in policy-gateway.yaml:
policyEngine:
baseAddress: "https://policy-engine.internal"
audience: "api://policy-engine"
clientCredentials:
enabled: true
clientId: "policy-gateway"
clientSecret: "<secret>"
scopes:
- policy:read
- policy:author
- policy:review
- policy:operate
- policy:activate
dpop:
enabled: true
keyPath: "../etc/policy-gateway-dpop.pem"
algorithm: "ES256"
🔐 DPoP key – store the private key alongside Offline Kit secrets; rotate it whenever the gateway identity or Authority configuration changes.
4 · Metrics & logging
All activation calls emit:
policy_gateway_activation_requests_total{outcome,source}– counter labelled withoutcome(activated,pending_second_approval,already_active,bad_request,not_found,unauthorized,forbidden,error) andsource(caller,service).policy_gateway_activation_latency_ms{outcome,source}– histogram measuring proxy latency.
Structured logs (category StellaOps.Policy.Gateway.Activation) include PackId, Version, Outcome, Source, and upstream status code for audit trails.
5 · Sample curl workflows
Assuming you already obtained a DPoP-bound access token ($TOKEN) for tenant acme:
# Generate a DPoP proof for GET via the CLI helper
DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs \
--htm GET \
--token "$TOKEN")
curl -sS https://gateway.example.com/api/policy/packs \
-H "Authorization: DPoP $TOKEN" \
-H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme"
# Draft a new revision
DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs/policy.core/revisions \
--htm POST \
--token "$TOKEN")
curl -sS https://gateway.example.com/api/policy/packs/policy.core/revisions \
-H "Authorization: DPoP $TOKEN" \
-H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme" \
-H "Content-Type: application/json" \
-d '{"version":5,"requiresTwoPersonApproval":true,"initialStatus":"Draft"}'
# Activate revision 5 (returns 202 when awaiting the second approver)
DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs/policy.core/revisions/5:activate \
--htm POST \
--token "$TOKEN")
curl -sS https://gateway.example.com/api/policy/packs/policy.core/revisions/5:activate \
-H "Authorization: DPoP $TOKEN" \
-H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme" \
-H "Content-Type: application/json" \
-d '{"comment":"Rollout baseline"}'
For air-gapped environments, bundle policy-gateway.yaml and the DPoP key in the Offline Kit (see /docs/24_OFFLINE_KIT.md §5.7).
DPoP proof helper: Use
stella auth dpop proofto mint sender-constrained proofs locally. The command accepts--htu,--htm, and--tokenarguments and emits a ready-to-use header value. Teams maintaining alternate tooling (for example,scripts/make-dpop.sh) can substitute it as long as the inputs and output match the CLI behaviour.
6 · Offline Kit guidance
- Include
policy-gateway.yaml.sampleand the resolved runtime config in the Offline Kit’sconfig/tree. - Place the DPoP private key under
secrets/policy-gateway-dpop.pemwith restricted permissions; document rotation steps in the manifest. - When building verification scripts, use the gateway endpoints above instead of hitting Policy Engine directly. The Offline Kit validator now expects
policy_gateway_activation_requests_totalmetrics in the Prometheus snapshot.
7 · Change log
- 2025-10-27 – Sprint 18.5: Initial gateway bootstrap + activation metrics + DPoP client credentials.