Egress Gateway Credential Injection¶
This guide demonstrates how to use AuthPolicy with an Istio egress gateway to inject credentials into requests to external services using HashiCorp Vault as the credential source.
Overview¶
This guide builds on the workload identity pattern from the egress gateway setup guide. That pattern uses kubernetesTokenReview to validate workload SA tokens. Credential injection extends it by adding Vault integration - using the validated identity to fetch per-workload credentials and inject them into outbound requests.
Request Flow¶
- Internal workload sends an HTTP request to the egress gateway with
Authorization: Bearer <sa-token> - The gateway triggers an ext_authz check with Authorino
- Authorino validates the workload's Kubernetes service account token via TokenReview
- Authorino authenticates to Vault using the workload's SA token (Vault Kubernetes auth method)
- Authorino reads the external credential from Vault and overwrites the
Authorizationheader viaresponse.success.headers - The egress gateway originates TLS and forwards the request with the external credential to the external service
Topology¶
graph TB
WL([Internal<br/>Workload])
subgraph "gateway-system namespace"
GW[Gateway<br/>kuadrant-egressgateway]
HR[HTTPRoute<br/>httpbin-external]
SE[ServiceEntry<br/>httpbin-external]
DR[DestinationRule<br/>httpbin-external]
AP[AuthPolicy<br/>httpbin-external-auth]
end
subgraph "kuadrant-system namespace"
AUTH[Authorino]
end
subgraph "vault namespace"
VAULT[Vault<br/>K8s auth]
end
WL -->|"1. Authorization:<br/>Bearer sa-token"| GW
HR -.->|routes| GW
AP -.->|attached to| HR
GW -->|"2. ext_authz"| AUTH
AUTH -->|"3. Vault login<br/>(SA token)"| VAULT
AUTH -->|"4. read secret"| VAULT
GW -->|"5. HTTPS +<br/>Bearer external-key"| EXT([External API<br/>e.g. httpbin.org])
SE -.->|registers| EXT
DR -.->|TLS config| EXT
Prerequisites¶
- Egress gateway infrastructure deployed. See the Egress Gateway Setup guide.
- HashiCorp Vault with Kubernetes auth method configured. The setup script deploys a dev Vault instance automatically.
export EGRESS_IP=$(kubectl get gtw kuadrant-egressgateway -n gateway-system \
-o jsonpath='{.status.addresses[0].value}')
Credential Injection¶
The AuthPolicy below authenticates the workload via its Kubernetes service account token, fetches a per-identity credential from Vault, and injects it into the outbound request. The Vault path is constructed dynamically from the workload's namespace and service account name, so different workloads get different credentials. The workload sends Authorization: Bearer <sa-token>; the gateway replaces it with Authorization: Bearer <external-key>.
Step 1: Apply the AuthPolicy¶
kubectl apply -f - <<'EOF'
apiVersion: kuadrant.io/v1
kind: AuthPolicy
metadata:
name: httpbin-external-auth
namespace: gateway-system
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: httpbin-external
rules:
authentication:
"workload-identity":
kubernetesTokenReview:
audiences:
- "https://kubernetes.default.svc.cluster.local"
metadata:
vault_login:
http:
url: "http://vault.vault.svc.cluster.local:8200/v1/auth/kubernetes/login"
method: POST
contentType: application/json
body:
expression: '"{\"jwt\": \"" + request.headers.authorization.substring(7) + "\", \"role\": \"egress-workload\"}"'
priority: 0
vault_secret:
http:
urlExpression: '"http://vault.vault.svc.cluster.local:8200/v1/secret/data/egress/" + auth.metadata.vault_login.auth.metadata.service_account_namespace + "/" + auth.metadata.vault_login.auth.metadata.service_account_name'
method: GET
headers:
X-Vault-Token:
expression: 'auth.metadata.vault_login.auth.client_token'
priority: 1
authorization:
vault_credential_check:
patternMatching:
patterns:
- predicate: 'has(auth.metadata.vault_secret.data)'
response:
success:
headers:
authorization:
plain:
expression: '"Bearer " + auth.metadata.vault_secret.data.data.api_key'
EOF
How it works:
authentication.workload-identity- validates the workload's Kubernetes SA token via TokenReview. No API keys to distribute - every pod already has an SA token mounted.metadata.vault_login(priority: 0) — POSTs the SA token to Vault's Kubernetes auth endpoint. Vault validates the token and returns a scoped client token based on the workload's namespace and service account.metadata.vault_secret(priority: 1, runs aftervault_login) — reads the external credential from Vault at a path derived from the workload's namespace and service account (e.g.,secret/egress/egress-test/default). Uses the client token fromvault_login. Each workload identity gets its own credential.authorization.vault_credential_check- verifies the Vault credential fetch succeeded. If the workload's SA isn't authorized by Vault's policy, the request is denied (403).response.success.headers.authorization- overwrites theAuthorizationheader with the external credential. The workload's SA token never reaches the external service.
Step 2: Test¶
The setup script stored a test credential at secret/egress/egress-test/default for the test client. Test the three access scenarios:
# No token - denied (401)
kubectl exec test-client -n egress-test -- curl -s -o /dev/null -w "%{http_code}" \
-H "Host: httpbin.org" http://${EGRESS_IP}/get
# 401
# SA token from authorized namespace - credential injected
kubectl exec test-client -n egress-test -- sh -c '
curl -s -H "Host: httpbin.org" \
-H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
http://'"${EGRESS_IP}"'/get
'
# Response shows: Authorization: Bearer sk-test-openai-key-for-egress
# SA token from unauthorized namespace - denied (403)
kubectl run bad-client --image=curlimages/curl:latest -n default --restart=Never \
--command -- sleep infinity
kubectl wait --for=condition=Ready pod/bad-client -n default --timeout=30s
kubectl exec bad-client -n default -- sh -c '
curl -s -o /dev/null -w "%{http_code}" -H "Host: httpbin.org" \
-H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
http://'"${EGRESS_IP}"'/get
'
# 403
kubectl delete pod bad-client -n default
Per-Identity Credentials¶
Different workload identities automatically get different credentials from Vault. The urlExpression constructs the Vault path from the workload's namespace and service account name.
To verify, store a different credential for another namespace and test:
# Add workload-b namespace to the Vault role
kubectl exec vault-0 -n vault -- vault write auth/kubernetes/role/egress-workload \
bound_service_account_names=default \
bound_service_account_namespaces=egress-test,workload-b \
policies=egress-read \
ttl=1h
# Store a different credential for workload-b
kubectl exec vault-0 -n vault -- vault kv put secret/egress/workload-b/default \
api_key=sk-different-key-for-workload-b
# Deploy a workload in workload-b namespace
kubectl create namespace workload-b
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-client-b
namespace: workload-b
spec:
containers:
- name: curl
image: curlimages/curl:latest
command: ["sleep", "infinity"]
restartPolicy: Never
EOF
kubectl wait --for=condition=Ready pod/test-client-b -n workload-b --timeout=60s
# Each workload gets its own credential
kubectl exec test-client -n egress-test -- sh -c '
curl -s -H "Host: httpbin.org" \
-H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
http://'"${EGRESS_IP}"'/get
'
# Authorization: Bearer sk-test-openai-key-for-egress
kubectl exec test-client-b -n workload-b -- sh -c '
curl -s -H "Host: httpbin.org" \
-H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
http://'"${EGRESS_IP}"'/get
'
# Authorization: Bearer sk-different-key-for-workload-b
Security¶
Access control is enforced at two layers:
- Kubernetes TokenReview - Authorino validates the workload's SA token. Requests without a valid token are rejected (401).
- Vault Kubernetes auth - Vault validates the SA token independently and checks it against the role's
bound_service_account_namesandbound_service_account_namespaces. Only workloads in authorized namespaces get credentials. Unauthorized workloads are rejected (403).
No shared tokens or API keys are involved. Each workload authenticates with its own Kubernetes identity and receives its own credential from a per-identity Vault path.
Credential management: External API keys must be stored in Vault at the correct per-identity path (secret/egress/<namespace>/<sa-name>) before workloads can use them. This is typically a platform team responsibility. To allow workload teams to manage their own credentials, grant scoped Vault write access per namespace (e.g., secret/data/egress/<namespace>/*).
Cleanup¶
kubectl delete authpolicy httpbin-external-auth -n gateway-system --ignore-not-found
kubectl delete pod bad-client -n default --ignore-not-found
kubectl delete pod test-client-b -n workload-b --ignore-not-found
kubectl delete namespace workload-b --ignore-not-found
To tear down the egress gateway infrastructure and Vault:
curl -sL https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/refs/heads/main/hack/setup-egress.sh | bash -s cleanup