Configure OpenID Connect (OIDC) authentication
This guide describes how to configure OpenID Connect (OIDC) authentication in NGINX Gateway Fabric using the AuthenticationFilter custom resource definition (CRD).
OIDC authentication lets you delegate user login to a trusted Identity Provider (IdP) such as Keycloak, Okta, or Auth0. Once a user signs in through the IdP, that session is recognized across every route protected by the same IdP. They are not prompted to log in again when moving between applications. NGINX Gateway Fabric redirects unauthenticated users to the IdP, receives an authorization code after login, and exchanges that code for identity tokens on the user’s behalf. Your backend services receive only requests that have already passed authentication and never handle credentials directly.
When a user requests a protected resource, NGINX Gateway Fabric uses the Authorization Code Flow:
- NGINX redirects the browser to the IdP’s authorization endpoint.
- The user authenticates with the IdP.
- The IdP redirects the browser back to NGINX with a short-lived authorization code.
- NGINX exchanges that code for an ID token and access token via a direct, back-channel HTTPS call to the IdP’s token endpoint.
- NGINX validates the ID token, creates a session cookie, and forwards the original request to the backend.
- Subsequent requests carry the session cookie, so the IdP is not contacted again until the session expires.
TLS is required in two directions. For inbound connections, the callback redirect from the IdP back to NGINX must be served over HTTPS. The AuthenticationFilter must be attached to an HTTPRoute that uses an HTTPS listener, and the Gateway listener’s tls.certificateRefs provides the certificate NGINX presents to the browser. This is the same certificate that NGINX would provide to any client. For outbound connections, NGINX connects to the IdP over HTTPS to exchange the authorization code for tokens. By default, NGINX trusts the system CA bundle. To use a custom CA, specify it in oidc.caCertificateRefs. Attaching an OIDC AuthenticationFilter to a non-HTTPS route will cause the filter to be rejected.
OIDC configuration references Kubernetes Opaque Secrets for sensitive material. The clientSecretRef field expects a Secret with the key client-secret. Your IdP requires a client ID and secret to identify and authenticate the application contacting its realm. NGINX presents these credentials when exchanging the authorization code for tokens. The caCertificateRefs field expects a Secret with the key ca.crt, containing PEM-encoded CA certificates that NGINX uses to verify the IdP’s TLS certificate on outbound connections. If omitted, NGINX uses the system CA bundle. The crlSecretRef field expects a Secret with the key ca.crl, containing a PEM-encoded Certificate Revocation List. NGINX checks the IdP’s certificate serial number against this list before every outbound connection. This field can be omitted if CRL checking is not required.
You can consolidate multiple keys in a single Secret or use separate Secrets for each. Either approach works as long as each Secret contains the correct key name.
ImportantOIDC authentication requires NGINX Plus and is not supported with open-source NGINX.
To follow this guide, you need the following:
- Install NGINX Gateway Fabric with NGINX Plus.
- Install cert-manager in your cluster.
The following steps use cert-manager to issue a local Certificate Authority (CA) and sign certificates for both Keycloak and NGINX. cert-manager creates the required Kubernetes Secrets directly so no manual secret creation is needed for TLS.
Create a self-signed ClusterIssuer to bootstrap the CA, then issue the CA certificate and create a second ClusterIssuer backed by it:
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-cluster-issuer
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: local-ca
namespace: cert-manager
spec:
isCA: true
commonName: LocalCA
secretName: local-ca-secret
issuerRef:
name: selfsigned-cluster-issuer
kind: ClusterIssuer
group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: local-ca-issuer
spec:
ca:
secretName: local-ca-secret
EOFCreate certificates for Keycloak and NGINX. cert-manager will create keycloak-tls-cert and nginx-secret in the default namespace:
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: keycloak-cert
namespace: default
spec:
secretName: keycloak-tls-cert
issuerRef:
name: local-ca-issuer
kind: ClusterIssuer
commonName: keycloak.default.svc.cluster.local
dnsNames:
- keycloak.default.svc.cluster.local
- keycloak
- localhost
ipAddresses:
- 127.0.0.1
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: nginx-cert
namespace: default
spec:
secretName: nginx-secret
issuerRef:
name: local-ca-issuer
kind: ClusterIssuer
commonName: cafe.example.com
dnsNames:
- cafe.example.com
EOFIf you already have an IdP set up with a realm, a client, and a user, skip to Setup.
Deploy Keycloak to your cluster. Keycloak must serve HTTPS because NGINX connects to it over TLS for token exchange. The keycloak-tls-cert Secret was created by cert-manager in the previous step and is mounted into the Keycloak container below.
TheredirectUrisfield must include the exact hostname and port that the NGINX Gateway is exposed on. If you are accessing the Gateway via port-forward or on a non-standard port, include that port explicitly. For example,https://cafe.example.com:9443/*. If the URI does not match exactly what NGINX sends, Keycloak will reject the request with anInvalid parameter: redirect_urierror. Our default callback location is set to/oidc_callback_<namespace>_<filtername>
kubectl apply -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: keycloak-realm-config
data:
nginx-gateway-realm.json: |
{
"realm": "nginx-gateway",
"enabled": true,
"sslRequired": "external",
"roles": {
"realm": [
{
"name": "user",
"composite": false
}
]
},
"clients": [
{
"clientId": "nginx-gateway-coffee",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"secret": "oidc-coffee-client-secret",
"directAccessGrantsEnabled": true,
"standardFlowEnabled": true,
"redirectUris": ["https://cafe.example.com/*"],
"webOrigins": ["https://cafe.example.com"]
}
],
"users": [
{
"username": "testuser",
"enabled": true,
"emailVerified": true,
"email": "testuser@example.com",
"firstName": "Test",
"lastName": "User",
"credentials": [
{
"type": "password",
"value": "testpassword",
"temporary": false
}
],
"realmRoles": ["user"]
}
]
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
labels:
app: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.5
args:
- "start-dev"
- "--https-certificate-file=/etc/keycloak-certs/tls.crt"
- "--https-certificate-key-file=/etc/keycloak-certs/tls.key"
- "--import-realm"
env:
- name: KC_BOOTSTRAP_ADMIN_USERNAME
value: "admin"
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
value: "admin"
- name: KC_HTTP_ENABLED
value: "true"
- name: KC_HTTPS_ENABLED
value: "true"
- name: KC_PROXY_HEADERS
value: "xforwarded"
ports:
- name: http
containerPort: 8080
- name: https
containerPort: 8443
volumeMounts:
- name: keycloak-certs
mountPath: /etc/keycloak-certs
readOnly: true
- name: realm-config
mountPath: /opt/keycloak/data/import
readOnly: true
readinessProbe:
httpGet:
path: /realms/master
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
volumes:
- name: keycloak-certs
secret:
secretName: keycloak-tls-cert
- name: realm-config
configMap:
name: keycloak-realm-config
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
spec:
selector:
app: keycloak
ports:
- name: http
port: 8080
targetPort: 8080
- name: https
port: 8443
targetPort: 8443
EOFThis creates a Keycloak deployment and service.
Keycloak is configured with a realm named nginx-gateway, a client with ID nginx-gateway-coffee and secret oidc-coffee-client-secret, and a test user with username testuser and password testpassword. Update these values to match your environment before applying.
Store the client secret in a shell variable for use in later steps:
CLIENT_SECRET=oidc-coffee-client-secretOnce the pod is running, expose Keycloak with port-forward:
kubectl port-forward svc/keycloak 8443:8443The browser must be able to resolve the Keycloak hostname to reach the login page during the OIDC flow. Add the following entry to your /etc/hosts file:
127.0.0.1 keycloak.default.svc.cluster.localTo visit the Keycloak admin console, open https://keycloak.default.svc.cluster.local:8443 in your browser.
In this guide, you will deploy two sample applications behind a single HTTPS gateway. The /coffee path is protected by OIDC, so users must log in through the identity provider before accessing the coffee backend. The /tea path does not require authentication, and requests are forwarded directly to the tea backend.
Run the following kubectl apply command to create the coffee and tea deployments and services:
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: coffee
spec:
replicas: 1
selector:
matchLabels:
app: coffee
template:
metadata:
labels:
app: coffee
spec:
containers:
- name: coffee
image: nginxdemos/nginx-hello
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: coffee
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: coffee
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tea
spec:
replicas: 1
selector:
matchLabels:
app: tea
template:
metadata:
labels:
app: tea
spec:
containers:
- name: tea
image: nginxdemos/nginx-hello
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: tea
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: tea
EOFConfirm the pods are running with kubectl get pods:
kubectl get podsNAME READY STATUS RESTARTS AGE
coffee-654ddf664b-fllj7 1/1 Running 0 15s
tea-75bc9f4b6d-cx2jl 1/1 Running 0 15sOIDC requires an HTTPS listener. The tls.certificateRefs entry points to a Secret containing the TLS certificate and key that NGINX presents to clients. The nginx-secret Secret was created by cert-manager in the previous step.
If you are accessing the Gateway using port-forward, the local port must match the Gateway listener port. Keycloak redirects the browser back to NGINX using the listener port, so if there is a mismatch the redirect will fail. This may require updating the Gateway listener port to a non-standard value such as 9443.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway
spec:
gatewayClassName: nginx
listeners:
- name: https
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: nginx-secret
EOFConfirm the Gateway was assigned an IP address:
kubectl describe gateways.gateway.networking.k8s.io gatewayAddresses:
Type: IPAddress
Value: 10.96.20.187Save the IP and port into shell variables:
GW_IP=XXX.YYY.ZZZ.III
GW_PORT=443NGINX must resolve the IdP’s hostname at runtime to fetch the OIDC discovery document and exchange tokens. Without a DNS resolver configured, NGINX cannot start the OIDC flow and will log no resolver defined to resolve. Before proceeding, configure a DNS resolver in the NginxProxy resource.
Get the IP address of the kube-dns service in the kube-system namespace:
kubectl get svc -n kube-system kube-dnsNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 10dNGINX Gateway Fabric creates an NginxProxy resource during installation. Edit it to add the dnsResolver field:
kubectl edit nginxproxies.gateway.nginx.org -n nginx-gateway ngf-proxy-configspec:
dnsResolver:
addresses:
- type: IPAddress
value: 10.96.0.10This Secret holds the client secret and the CA certificate NGINX uses to verify Keycloak’s TLS certificate on outbound connections. The CA certificate is extracted from the keycloak-tls-cert Secret that cert-manager created.
kubectl create secret generic keycloak-secret \
--from-literal=client-secret=$CLIENT_SECRET \
--from-file=ca.crt=<(kubectl get secret keycloak-tls-cert -o jsonpath='{.data.ca\.crt}' | base64 -d)The AuthenticationFilter defines how NGINX communicates with the IdP. The only required fields are issuer, clientID, and clientSecretRef. Everything else is optional.
kubectl apply -f - <<EOF
apiVersion: gateway.nginx.org/v1alpha1
kind: AuthenticationFilter
metadata:
name: oidc-coffee
spec:
type: OIDC
oidc:
clientSecretRef:
name: keycloak-secret
clientID: nginx-gateway-coffee
issuer: https://keycloak.default.svc.cluster.local:8443/realms/nginx-gateway
caCertificateRefs:
- name: keycloak-secret
EOFVerify the filter is accepted:
kubectl describe authenticationfilters.gateway.nginx.org oidc-coffeeStatus:
Controllers:
Conditions:
Last Transition Time: 2026-03-17T10:00:00Z
Message: The AuthenticationFilter is accepted
Observed Generation: 1
Reason: Accepted
Status: True
Type: Accepted
Controller Name: gateway.nginx.org/nginx-gateway-controller
Events: <none>Create an HTTPRoute with two rules. The /coffee rule attaches the AuthenticationFilter, whereas /tea is publicly accessible with no authentication required.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: cafe-routes
spec:
parentRefs:
- name: gateway
sectionName: https
hostnames:
- "cafe.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /coffee
backendRefs:
- name: coffee
port: 80
filters:
- type: ExtensionRef
extensionRef:
group: gateway.nginx.org
kind: AuthenticationFilter
name: oidc-coffee
- matches:
- path:
type: Exact
value: /tea
backendRefs:
- name: tea
port: 80
EOFVerify the route is accepted:
kubectl describe httproute cafe-routesStatus:
Parents:
Conditions:
Last Transition Time: 2026-03-17T10:00:05Z
Message: The Route is accepted
Observed Generation: 1
Reason: Accepted
Status: True
Type: Accepted
Last Transition Time: 2026-03-17T10:00:05Z
Message: All references are resolved
Observed Generation: 1
Reason: ResolvedRefs
Status: True
Type: ResolvedRefs
Controller Name: gateway.nginx.org/nginx-gateway-controller
Parent Ref:
Group: gateway.networking.k8s.io
Kind: Gateway
Name: gateway
Section Name: https
Events: <none>For local testing, add the following entry to your /etc/hosts file so your browser can resolve cafe.example.com to the Gateway’s public IP:
<GW_IP> cafe.example.comThe steps below use a browser for OIDC since the flow involves redirects and cookies that curl cannot handle end-to-end.
Open https://cafe.example.com:$GW_PORT/coffee in a browser. Because the route has an AuthenticationFilter, NGINX will:
- Detect there is no valid session cookie.
- Redirect your browser to the IdP’s login page.
- Log in with username
testuserand passwordtestpassword. - After you log in, redirect you back to NGINX with an authorization code.
- Exchange the code for tokens in the background, set a session cookie, and forward you to the
coffeebackend.
You will see a response from the coffee application only after a successful login.
Since /tea requires no authentication, you can access it directly with curl:
curl -k --resolve cafe.example.com:$GW_PORT:$GW_IP https://cafe.example.com:$GW_PORT/teaServer address: 10.244.0.10:8080
Server name: tea-75bc9f4b6d-ms2n8
Date: 17/Mar/2026:10:01:00 +0000
URI: /tea
Request ID: c7eb0509303de1c160cb7e7d2ac1d99fThe tea backend responds immediately with no authentication challenge because no AuthenticationFilter is attached to that rule.
By default, NGINX issues a session cookie named NGX_OIDC_SESSION with an 8-hour timeout that resets on each request to a protected resource. Use session.cookieName and session.timeout to override these values.
spec:
type: OIDC
oidc:
session:
cookieName: my-app-session
timeout: 30mUse logout.uri to set the path a user visits to log out. When a request hits that path, NGINX clears the session and redirects to the IdP’s logout endpoint. If logout.postLogoutURI is not set, NGINX returns a 200 OK with the body "You have been logged out.". It can be set to a path to redirect the user there after logout. The path must be matched by an existing HTTPRoute rule. Set it to a full URL to redirect the user to an external page.
Use logout.frontChannelLogoutURI if your IdP uses front-channel logout, where the IdP sends a logout request to a browser-visible URL to clear the NGINX session. The IdP must send iss and sid as query parameters. Set logout.tokenHint to true if your IdP requires the original ID token to be passed in the logout request.
spec:
type: OIDC
oidc:
logout:
uri: /logout
postLogoutURI: /after_logout
frontChannelLogoutURI: /frontchannel_logout
tokenHint: truePKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. NGINX enables it automatically when the IdP requires the S256 code challenge method. Set pkce explicitly if you need to force it on or off.
spec:
type: OIDC
oidc:
pkce: trueUse extraAuthArgs to append additional query parameters to the authorization request sent to the IdP. For example, prompt: "login" forces the IdP to show the login page on every request, and max_age sets the maximum time in seconds since the user last authenticated before re-authentication is required.
spec:
type: OIDC
oidc:
extraAuthArgs:
prompt: "login"
max_age: "3600"By default, NGINX Gateway Fabric registers the OIDC callback at /oidc_callback_<namespace>_<filtername>. Use redirectURI to set a different path. If you provide a path-only value, NGINX creates a location block to handle the callback. If you provide a full URL, it is treated as an external handler and no location block is created. Register the same value in your IdP as an allowed redirect URI.
spec:
type: OIDC
oidc:
redirectURI: /my_callbackBy default, NGINX fetches IdP metadata from <issuer>/.well-known/openid-configuration. Use configURL if your IdP exposes metadata at a different path.
spec:
type: OIDC
oidc:
configURL: "https://keycloak.example.com/realms/my-realm/.well-known/openid-configuration"A CRL is a list of certificate serial numbers that a CA has revoked before expiry. When NGINX connects to the IdP over TLS, it checks the IdP’s certificate against the CRL and rejects the connection if the certificate has been revoked. You are responsible for keeping the CRL up to date. A stale CRL may not catch recently revoked certificates. The crlSecretRef and caCertificateRefs fields are separate so you can rotate the CRL independently, though both keys can live in the same Secret.
spec:
type: OIDC
oidc:
crlSecretRef:
name: oidc-crl- Confirm the filter’s
typeisOIDCand theoidcblock is present. - Check that the Secrets referenced by
clientSecretRef,caCertificateRefs, andcrlSecretRefexist in the same namespace as the filter and contain the expected keys (client-secret,ca.crt,ca.crl).
- Verify the
extensionRefin the HTTPRoute matches theAuthenticationFiltername and namespace exactly. - Confirm the route’s
parentRefspoints to an HTTPS listener. OIDC filters are rejected on non-HTTPS listeners.
- Confirm the
redirectURIregistered in the IdP exactly matches the path NGINX is using (default:/oidc_callback_<namespace>_<filtername>). - Ensure the Gateway’s TLS certificate is valid for the hostname the browser is using.