Policy Gateway

Delivery scope: StellaOps.Policy.Gateway minimal 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-Tenant header.
    • Gateway client credentials (DPoP) for service automation or Offline Kit flows when no caller token is present.

2 · Endpoints

RouteMethodDescriptionRequired scope(s)
/api/policy/packsGETList policy packs and revisions for the active tenant.policy:read
/api/policy/packsPOSTCreate a policy pack shell or upsert display metadata.policy:author
/api/policy/packs/{packId}/revisionsPOSTCreate or update a policy revision (draft/approved).policy:author
/api/policy/packs/{packId}/revisions/{version}:activatePOSTActivate a revision, enforcing single/two-person approvals.policy:operate, policy:activate

Response shapes

  • Successful responses return camel-case DTOs matching PolicyPackDto, PolicyRevisionDto, or PolicyRevisionActivationDto as described in the Policy Engine API doc (/docs/api/policy.md).
  • Errors always return RFC 7807 ProblemDetails with deterministic fields (title, detail, status). Missing caller credentials now surface 401 with "Upstream authorization missing" detail.

Dual-control activation

  • Config-driven. Set PolicyEngine.activation.forceTwoPersonApproval=true when every activation must collect two distinct policy:activate approvals. When false, operators can opt into dual-control per revision (requiresTwoPersonApproval: true).
  • Defaults. PolicyEngine.activation.defaultRequiresTwoPersonApproval feeds the default when callers omit the checkbox/flag.
  • Statuses. First approval on a dual-control revision returns 202 pending_second_approval; duplicate actors get 400 duplicate_approval; the second distinct approver receives the usual 200 activated.
  • Audit trail. With PolicyEngine.activation.emitAuditLogs on, Policy Engine emits structured policy.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*.yaml now include a policy-engine-activation ConfigMap. The chart automatically injects it via envFrom into 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 keeps helm template deterministic.
  • File-based overrides (optional). The Policy Engine host already probes /config/policy-engine/activation.yaml, ../etc/policy-engine.activation.yaml, and ambient policy-engine.activation.yaml files beside the binary. Mounting the ConfigMap as a file at /config/policy-engine/activation.yaml works 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

HeaderSourceNotes
AuthorizationForwarded caller token or gateway client credentials.Caller tokens must include tenant scope; gateway tokens default to DPoP scheme.
DPoPCaller 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-TenantCallerTenant 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 with outcome (activated, pending_second_approval, already_active, bad_request, not_found, unauthorized, forbidden, error) and source (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 proof to mint sender-constrained proofs locally. The command accepts --htu, --htm, and --token arguments 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.sample and the resolved runtime config in the Offline Kit’s config/ tree.
  • Place the DPoP private key under secrets/policy-gateway-dpop.pem with 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_total metrics in the Prometheus snapshot.

7 · Change log

  • 2025-10-27 – Sprint 18.5: Initial gateway bootstrap + activation metrics + DPoP client credentials.