Istio
The Istio ingress gateway terminates mutual TLS, and a CUSTOM AuthorizationPolicy sends every request to mt-enforcer through the envoyExtAuthzGrpc extension provider. Mesh mTLS and an ALLOW policy keep the backend reachable only through the gateway.
Deploy the enforcer
mt-enforcer pair --api https://api.mutualtrust.io --state ./mt-state
kubectl create namespace mt-system
kubectl -n mt-system create secret generic mt-enforcer-pairing --from-file=gateway-state.json=./mt-state/gateway-state.json
helm install mt-enforcer services/enforcer/integrations/kubernetes/charts/mt-enforcer -n mt-system --set mode=envoy-grpcWithout Helm, apply the equivalent manifests:
# mt-enforcer for Istio without Helm (equivalent to the chart with mode=envoy-grpc).
# Create the pairing Secret first:
# mt-enforcer pair --api https://api.mutualtrust.io --state ./mt-state
# kubectl -n mt-system create secret generic mt-enforcer-pairing --from-file=gateway-state.json=./mt-state/gateway-state.json
apiVersion: v1
kind: Namespace
metadata:
name: mt-system
labels: { istio-injection: disabled }
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mt-enforcer
namespace: mt-system
labels: { app.kubernetes.io/name: mt-enforcer }
spec:
replicas: 2
selector:
matchLabels: { app.kubernetes.io/name: mt-enforcer }
template:
metadata:
labels: { app.kubernetes.io/name: mt-enforcer }
annotations: { prometheus.io/scrape: "true", prometheus.io/port: "9464" }
spec:
automountServiceAccountToken: false
securityContext: { runAsNonRoot: true, runAsUser: 65532, fsGroup: 65532, seccompProfile: { type: RuntimeDefault } }
containers:
- name: enforcer
image: mt-enforcer:1.0.0
args: ["envoy-grpc", "--api=https://api.mutualtrust.io", "--state=/var/lib/mt-enforcer/pairing",
"--data-dir=/var/lib/mt-enforcer/data", "--listen=0.0.0.0:9191", "--admin=0.0.0.0:9464"]
ports:
- { name: grpc, containerPort: 9191 }
- { name: admin, containerPort: 9464 }
readinessProbe: { httpGet: { path: /readyz, port: admin }, periodSeconds: 5 }
livenessProbe: { httpGet: { path: /healthz, port: admin }, periodSeconds: 10 }
securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: { drop: [ALL] } }
resources: { requests: { cpu: 50m, memory: 64Mi }, limits: { memory: 256Mi } }
volumeMounts:
- { name: pairing, mountPath: /var/lib/mt-enforcer/pairing, readOnly: true }
- { name: data, mountPath: /var/lib/mt-enforcer/data }
volumes:
- { name: pairing, secret: { secretName: mt-enforcer-pairing, defaultMode: 0400 } }
- { name: data, emptyDir: {} }
---
apiVersion: v1
kind: Service
metadata:
name: mt-enforcer
namespace: mt-system
spec:
selector: { app.kubernetes.io/name: mt-enforcer }
ports:
- { name: grpc, port: 9191, targetPort: grpc, appProtocol: grpc }
- { name: admin, port: 9464, targetPort: admin }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: mt-enforcer, namespace: mt-system }
spec:
minAvailable: 1
selector: { matchLabels: { app.kubernetes.io/name: mt-enforcer } }Register the provider
# Istio: register mt-enforcer as an external authorization provider (ext_authz
# gRPC). Merge into your IstioOperator or the istio ConfigMap's mesh config.
# The enforcer itself is deployed with ../kubernetes/charts/mt-enforcer:
# helm install mt-enforcer ../kubernetes/charts/mt-enforcer -n mt-system --set mode=envoy-grpc
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
extensionProviders:
- name: mt-enforcer
envoyExtAuthzGrpc:
service: mt-enforcer.mt-system.svc.cluster.local
port: 9191
timeout: 0.5s
failOpen: false # enforcer unreachable: deny
statusOnError: "503"Gateway with mutual TLS
# Istio ingress gateway terminating mutual TLS for the protected API. The
# credential Secret holds your server certificate (tls.crt, tls.key); the client
# CA bundle lives in orders-api-credential-cacert (key ca.crt), kept current by
# ../kubernetes/bundle-sync.yaml.
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
name: orders-api
namespace: istio-system
spec:
selector:
istio: ingressgateway
servers:
- port: { number: 443, name: https, protocol: HTTPS }
hosts: [orders-api.example.com]
tls:
mode: MUTUAL
credentialName: orders-api-credential
minProtocolVersion: TLSV1_2
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: orders-api
namespace: orders
spec:
hosts: [orders-api.example.com]
gateways: [istio-system/orders-api]
http:
- route:
- destination: { host: orders-api.orders.svc.cluster.local, port: { number: 9001 } }The client CA bundle goes into orders-api-credential-cacert (key ca.crt). integrations/kubernetes/bundle-sync.yaml keeps that Secret in step with the signed manifest; set its target to istio-system / orders-api-credential-cacert / ca.crt.
Authorization policies
# Every request through the ingress gateway is authorized by mt-enforcer
# (CUSTOM action, provider from mesh-config.yaml). The enforcer sets
# x-mt-principal, x-mt-grant and x-mt-request-id on allowed requests and removes
# any other inbound x-mt-* header.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: mt-enforcer
namespace: istio-system
spec:
selector:
matchLabels: { istio: ingressgateway }
action: CUSTOM
provider:
name: mt-enforcer
rules:
- to:
- operation:
hosts: [orders-api.example.com]
---
# The backend accepts only mesh mTLS (no plaintext) ...
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: strict
namespace: orders
spec:
mtls:
mode: STRICT
---
# ... and only from the ingress gateway's workload identity, so the enforcer cannot be bypassed.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: orders-api-from-gateway-only
namespace: orders
spec:
selector:
matchLabels: { app: orders-api }
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"]The enforcer receives the external client's certificate in attributes.source.certificate, re-validates it against the signed bundle and authorizes the request. It sets x-mt-principal, x-mt-grant and x-mt-request-id on allowed requests and removes any other inbound x-mt-*.
Verify
mt enroll --token <token from portal> --api https://api.mutualtrust.io
mt call https://orders-api.example.com:443/orders/1
# 200, and your origin receives x-mt-principal: urn:mutualtrust:workload:...
mt call https://orders-api.example.com:443/admin/export
# 403 {"code":"route_not_permitted", ...}
# any other client: the files are in the workload directory that mt status prints
W="$(dirname "$(mt status --json | jq -r .files.chain)")"
curl --cert "$W/chain.pem" --key "$W/key.pem" https://orders-api.example.com:443/orders/1