Scenario 02 · Zero-trust API gateway
Partner and internal APIs sit on a network you can't trust — shared VPCs, third-party integrators, a compromised pod one hop away. A firewall rule or a bearer token in a header isn't identity. Every call has to prove who it is (cryptographic mTLS / SPIFFE) and what it's allowed to do (policy), at the edge, before it ever reaches a backend — and the default answer must be no.
Architecture
The SPIFFE ID extracted from the client certificate becomes the Cedar principal. The first explicit
permit wins; everything else is denied. No OPA sidecar, no external PDP round-trip.
The setup
First, require a client certificate on the listener (GEP-91 frontend validation) and map the SPIFFE SVID to the principal:
apiVersion: zenoingress.io/v1alpha1
kind: ClientAuthConfig
metadata:
name: partner-mtls
namespace: default
spec:
mode: Required # GEP-91 frontend client-cert validation
caCertificateRefs:
- name: partner-ca
group: ""
kind: ConfigMap
spiffe:
trustDomain: bank.example # SVID URI SAN → principal
Then authorize. The AuthzPolicy carries inline Cedar — the same engine
behind AWS Verified Permissions — evaluated in-process. onError: deny makes the policy
fail closed:
apiVersion: zenoingress.io/v1alpha1
kind: AuthzPolicy
metadata:
name: payments-authz
namespace: default
spec:
principalHeader: x-spiffe-id
onError: deny # no match / eval error ⇒ 403
policies: |
permit(
principal in Group::"payments",
action == Action::"POST",
resource
) when { context.path like "/api/payments*" };
Attach the policy to a route with an ExtensionRef filter, and pin the backend's identity
on the way out with a SAN-checked BackendTLSPolicy — mutual TLS in both directions:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: payments
namespace: default
spec:
parentRefs:
- name: zeno-gateway
rules:
- matches:
- path: { type: PathPrefix, value: /api/payments }
filters:
- type: ExtensionRef
extensionRef:
group: zenoingress.io
kind: AuthzPolicy
name: payments-authz
backendRefs:
- name: payments
port: 8443
---
apiVersion: zenoingress.io/v1alpha1
kind: BackendTLSPolicy
metadata: { name: payments-backend-mtls, namespace: default }
spec:
targetRefs:
- group: ""
kind: Service
name: payments
validation:
caCertificateRefs:
- { name: payments-ca, group: "", kind: ConfigMap }
subjectAltNames:
- type: Hostname
hostname: payments.internal.svc # SAN pin
clientCertificateRef:
name: zeno-client-cert # ZenoIngress' own SVID
What happens
partner-ca and extracts the
SPIFFE ID from the certificate's URI SAN, surfacing it as x-spiffe-id.
POST /api/payments/... from a principal
in Group::"payments" matches the permit. 200 · forwarded
onError: deny returns 403. deny-by-default
Outcome
SPIFFE mTLS and a formally-verified Cedar engine live inside the proxy — identity and authorization decided in microseconds at the door, with no OPA sidecar to deploy, scale, or round-trip to.
Run this yourself
Apply the manifests, then call the route with three different identities and watch the decision change:
$ # 1) a client whose SVID is in Group::payments
$ curl --cert pay.crt --key pay.key https://api.bank.example/api/payments -d '{}'
HTTP/2 200
$ # 2) a valid SVID, but NOT in the payments group
$ curl --cert audit.crt --key audit.key https://api.bank.example/api/payments -d '{}'
HTTP/2 403 (no matching permit)
$ # 3) no client certificate at all
$ curl https://api.bank.example/api/payments -d '{}'
curl: (56) OpenSSL: alert handshake failure — certificate required
SPIFFE mTLS + Cedar in one binary. Free for one production cluster.
Deploy Free