Configure API key authentication

This page describes how to set up API key authentication in F5 NGINX Gateway Fabric using the AuthenticationFilter custom resource definition (CRD).

API key authentication looks for a key in each request. Clients send the key in a request header or a URL query parameter. NGINX Gateway Fabric loads those keys from a Kubernetes Secret into the NGINX data plane, which checks each request and rejects unknown keys before they reach your apps.

Use API key authentication for machine-to-machine traffic, such as a backend service or partner integration calling your API. For user sign-in, use OpenID Connect authentication or basic authentication instead.

By following these instructions, you create two sample app endpoints: one that requires an API key and one that doesn’t. You then send requests with a valid key, an invalid key, and no key, and compare the results.

Caution
Keys in a URL are less private than keys in a header. They can appear in browser history, in Referer headers sent to other sites, and in logs at every hop between the client and your app. Prefer a header unless your clients can’t set one. If you do use a query parameter, serve traffic over HTTPS.

Before you begin

Set up the example resources

Create the sample apps, the Gateway, and the API keys. Each step gives you YAML to apply with kubectl.

Deploy the sample applications

Create the coffee and tea applications:

yaml
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:plain-text
        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:plain-text
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: tea
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
    name: http
  selector:
    app: tea
EOF

Check that the pods are running:

kubectl get pods
text
NAME                      READY   STATUS    RESTARTS   AGE
coffee-654ddf664b-4fmcq   1/1     Running   0          13s
tea-75bc9f4b6d-8gtjl      1/1     Running   0          13s

Create a Gateway

Create the Gateway resource, which makes NGINX Gateway Fabric provision an NGINX pod and a Service in front of it:

yaml
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: cafe-gateway
spec:
  gatewayClassName: nginx
  listeners:
  - name: http
    port: 80
    protocol: HTTP
    hostname: "*.example.com"
EOF

The *.example.com hostname matches any subdomain of example.com, such as the cafe.example.com host this guide uses.

Confirm the Gateway was assigned an IP address and reports a Programmed=True status with kubectl describe:

kubectl describe gateways.gateway.networking.k8s.io cafe-gateway
text
Addresses:
  Type:   IPAddress
  Value:  192.0.2.1

Save the public IP address and port of the Gateway into shell variables:

shell
GW_IP=XXX.YYY.ZZZ.III
GW_PORT=<PORT_NUMBER>

Replace <PORT_NUMBER> with the port number you use to access your Gateway.

Create the Secret and the filter

Apply the Secret and the filter together:

yaml
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: api-keys
type: Opaque
data:
  # The "auth" key contains one client per line in "client-name:api-key" format.
  #
  # Decoded value:
  #   client-mobile:key-abc123
  #   client-web:key-def456
  auth: Y2xpZW50LW1vYmlsZTprZXktYWJjMTIzCmNsaWVudC13ZWI6a2V5LWRlZjQ1Ngo=
---
apiVersion: gateway.nginx.org/v1alpha1
kind: AuthenticationFilter
metadata:
  name: api-key-auth
spec:
  type: APIKey
  apiKey:
    secretRef:
      name: api-keys
    sources:
      - type: Header
        name: X-API-Key
      - type: Query
        name: api_key
EOF

The Secret, named api-keys, holds the keys in a data entry named auth. Each line names one client and its key, in client-name:api-key format.

The filter, named api-key-auth, points at the Secret and says where clients send the key. The example accepts the key in the X-API-Key header or in an api_key query parameter.

The example keys are key-abc123 for client-mobile and key-def456 for client-web. Swap them for your own before you send real traffic. See Use your own keys.

Check that the filter is accepted:

kubectl describe authenticationfilters.gateway.nginx.org api-key-auth | grep "Status:" -A10
text
Status:
  Controllers:
    Conditions:
      Last Transition Time:  2026-10-19T10:26:58Z
      Message:               The AuthenticationFilter is accepted
      Observed Generation:   1
      Reason:                Accepted
      Status:                True
      Type:                  Accepted
    Controller Name:         gateway.nginx.org/nginx-gateway-controller
Events:                      <none>

Protect a route

Apply the HTTPRoute that attaches the filter to /coffee. The /tea route has no filter, so you can compare the two:

yaml
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: cafe-routes
spec:
  parentRefs:
  - name: cafe-gateway
    sectionName: http
  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: api-key-auth
  - matches:
    - path:
        type: PathPrefix
        value: /tea
    backendRefs:
    - name: tea
      port: 80
EOF

Check that the route is accepted:

kubectl describe httproute cafe-routes | grep "Status:" -A10
text
Status:
  Parents:
    Conditions:
      Last Transition Time:  2026-10-19T10:28:15Z
      Message:               The Route is accepted
      Observed Generation:   1
      Reason:                Accepted
      Status:                True
      Type:                  Accepted
      Last Transition Time:  2026-10-19T10:28:15Z
      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:          cafe-gateway
      Namespace:     default
      Section Name:  http
Events:              <none>

Test your configuration

Your clients need to resolve cafe.example.com to the IP address of the NGINX Service. This guide does that with the curl --resolve option.

Store the keys in shell variables. GOOD_KEY holds a key from the Secret and BAD_KEY holds a value that isn’t in it:

shell
GOOD_KEY=key-abc123
BAD_KEY=not-a-real-key

Send the key in a header:

curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "X-API-Key: $GOOD_KEY"
text
Server address: 192.0.2.7:8080
Server name: coffee-654ddf664b-nhhvr
Date: 19/Oct/2026:15:20:15 +0000
URI: /coffee
Request ID: 13a925b2514b62c45ea4a79800248d5c

The key matches an entry in the Secret, so the request reaches the app.

Send the key in a query parameter:

curl --resolve cafe.example.com:$GW_PORT:$GW_IP "http://cafe.example.com:$GW_PORT/coffee?api_key=$GOOD_KEY"
text
Server address: 192.0.2.7:8080
Server name: coffee-654ddf664b-nhhvr
Date: 19/Oct/2026:15:21:30 +0000
URI: /coffee?api_key=key-abc123
Request ID: 7b2e4a1c9f0d3e5a8c6b4d2f1a0e9c8b

Both sources are enabled, so the query parameter works the same way as the header.

Send the request without a key:

curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee
text
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx</center>
</body>
</html>

Neither source has a key, so NGINX Gateway Fabric responds with 401.

Send a key that isn’t in the Secret:

curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "X-API-Key: $BAD_KEY"
text
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

A 403 means NGINX Gateway Fabric checked the key and rejected it. A 401 means the request had no key to check.

Send a request to the route with no filter:

curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea
text
Server address: 10.244.0.10:8080
Server name: tea-75bc9f4b6d-ms2n8
Date: 19/Oct/2026:15:36:26 +0000
URI: /tea
Request ID: c7eb0509303de1c160cb7e7d2ac1d99f

The /tea route has no AuthenticationFilter, so NGINX Gateway Fabric forwards the request as it is.

The filter at a glance

The apiKey fields in the filter you created work as follows:

Field Description
type Set to APIKey to use API key authentication. A filter with type: APIKey can’t set spec.jwt, spec.oidc, or spec.basic.
apiKey.secretRef Name of the Secret that holds the keys. The Secret must be in the same namespace as the filter. Required.
apiKey.sources Where clients can send the key. Set one or two sources. Required.
sources[].type Header to read a request header, or Query to read a URL query parameter.
sources[].name Header name or query parameter name. Use letters, numbers, -, and _, up to 253 characters.

How the check works

Keys that get accepted

NGINX Gateway Fabric reads the keys from the auth entry of the Secret and loads them into the NGINX data plane. The data plane matches keys exactly, so an uppercase letter or a trailing space makes a key invalid. The client name isn’t used to authorize a request. The name tells you which key belongs to whom, so you can revoke one client without touching the others.

Sources, where the key comes from

A request passes when any source carries a valid key, so clients can use whichever one suits them. Header names are case-insensitive, so a source named x-api-key matches an X-API-Key header.

A query parameter name can’t contain a hyphen. NGINX can’t read query arguments whose names include -, so api-key is rejected while api_key is fine.

Additional configuration

Use your own keys

The example Secret holds base64-encoded keys, which is easy to copy but hard to edit. Create a file with your own keys, then build the Secret from it.

Create the auth file:

shell
cat <<EOF > auth
client-mobile:<API_KEY>
client-web:<API_KEY>
EOF

Replace each <API_KEY> with a key of your own. Generate keys with a random generator, for example openssl rand -hex 24.

Apply the Secret. The --from-file=auth flag names the data entry auth:

kubectl create secret generic api-keys --from-file=auth --dry-run=client -o yaml | kubectl apply -f -

Check what NGINX Gateway Fabric reads by decoding the Secret:

kubectl get secret api-keys -o jsonpath='{.data.auth}' | base64 --decode

Change or revoke a key

Keys come from the Secret, so you can change them without touching the filter or your routes. Change the auth file and run the same kubectl apply command. NGINX Gateway Fabric reads the changed Secret and updates the keys NGINX accepts.

To revoke a client, remove that client’s entry from the auth file and re-apply the Secret again. Requests using that key will then return 403.

Accept keys in a header only

Drop the query source so clients must use a header:

yaml
spec:
  type: APIKey
  apiKey:
    secretRef:
      name: api-keys
    sources:
      - type: Header
        name: X-API-Key

Keep keys out of access logs

Routes with API key authentication log the request path without the query string. Keys sent as query parameters don’t end up in the NGINX access log. Other systems can still record the full URL. Your app and any proxy between the client and the Gateway can log the key, so headers stay the safer option.

Allow browser requests

Browsers send an unauthenticated OPTIONS request before a cross-origin call. When you set CORS on the same route, NGINX Gateway Fabric answers that request before it looks for a key. Only the real call needs a key.

Give each client group its own keys

One Secret means one key list for every route that uses the filter. To give a group of clients its own keys, create a Secret and a filter for that group. Then reference the filter on the routes that group uses.

kubectl create secret generic partner-keys --from-literal=auth="partner-a:<API_KEY>" --dry-run=client -o yaml | kubectl apply -f -

Troubleshooting

  • Check that the filter is accepted with kubectl describe authenticationfilters.gateway.nginx.org api-key-auth. An Invalid condition tells you what to fix.
  • If the filter is rejected with a message about the Secret, check two things. The Secret must be in the same namespace as the filter, and its data entry must be named auth.
  • If the filter is rejected with a message about client-name:api-key entries, every line failed. Check that each line has a name, a colon, and a key, and that the keys use only supported characters.
  • If the filter is rejected with query parameter names must not contain hyphens, rename the source. Replace - with _ in the name.
  • If valid requests get 403, compare the key the client sends with the decoded Secret, character by character. The check is case-sensitive and rejects extra spaces.
  • If requests without a key get 200, check that the route rule references the filter. The extensionRef needs group: gateway.nginx.org, kind: AuthenticationFilter, and the filter name.
  • If requests stop working after you change a key, check that kubectl get secret api-keys -o jsonpath='{.data.auth}' | base64 --decode shows the keys you expect. NGINX Gateway Fabric ignores duplicate keys.

Further reading