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.

Documentation menu

Deploy the enforcer

Pairing Secret and 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-grpc

Without Helm, apply the equivalent manifests:

services/enforcer/integrations/istio/enforcer.yaml
# 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

services/enforcer/integrations/istio/mesh-config.yaml
# 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

services/enforcer/integrations/istio/gateway.yaml
# 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

services/enforcer/integrations/istio/authorization-policy.yaml
# 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

Verify with the enrolled workload identity
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