Configure HTTPS termination

Learn how to terminate HTTPS traffic using NGINX Gateway Fabric.

Overview

In this guide, we will show how to configure HTTPS termination for your application, using an HTTPRoute redirect filter, secret, and ReferenceGrant.

To validate client certificates using mutual TLS (mTLS), see Securing frontend client traffic using mutual TLS.

Before you begin

Set up

Create the coffee application in Kubernetes by copying and pasting the following block into your terminal:

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
EOF

This will create the coffee service and a deployment. Run the following command to verify the resources were created:

kubectl get pods,svc

Your output should include the coffee pod and the coffee service:

text
NAME                          READY   STATUS      RESTARTS   AGE
pod/coffee-6b8b6d6486-7fc78   1/1     Running   0          40s


NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/coffee       ClusterIP   10.96.189.37   <none>        80/TCP    40s

Configure HTTPS termination and routing

For HTTPS, the deployment requires a certificate and a private key stored in a Secret. The Secret lives in a separate namespace, so a ReferenceGrant is required to access it. cert-manager issues the certificate from a local self-signed CA and automatically creates the cafe-secret Secret.

To create the certificate namespace, copy and paste the following into your terminal:

yaml
kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
  name: certificate
EOF

Install cert-manager onto the cluster using Helm with Gateway API features enabled.

  • Add the Helm repository.

    shell
    helm repo add jetstack https://charts.jetstack.io
    helm repo update
  • Install cert-manager, and enable the GatewayAPI feature gate:

    shell
    helm install \
      cert-manager jetstack/cert-manager \
      --namespace cert-manager \
      --create-namespace \
      --set config.apiVersion="controller.config.cert-manager.io/v1alpha1" \
      --set config.kind="ControllerConfiguration" \
      --set config.enableGatewayAPI=true \
      --set crds.enabled=true

Create a self-signed ClusterIssuer, a CA Certificate, and a CA-backed ClusterIssuer. cert-manager uses the resulting local-ca-issuer to sign certificates in any namespace:

yaml
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
EOF

Create a Certificate for cafe.example.com in the certificate namespace. cert-manager creates the cafe-secret Secret, which contains tls.crt, tls.key, and ca.crt.

yaml
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: cafe-cert
  namespace: certificate
spec:
  secretName: cafe-secret
  issuerRef:
    name: local-ca-issuer
    kind: ClusterIssuer
  commonName: cafe.example.com
  dnsNames:
  - cafe.example.com
EOF

To create the access-to-cafe-secret referencegrant, copy and paste the following into your terminal:

yaml
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
  name: access-to-cafe-secret
  namespace: certificate
spec:
  to:
  - group: ""
    kind: Secret
    name: cafe-secret # if you omit this name, then Gateways in default namespace can access all Secrets in the certificate namespace
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: default
EOF

To create the cafe gateway, copy and paste the following into your terminal:

yaml
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: cafe
spec:
  gatewayClassName: nginx
  listeners:
  - name: http
    port: 80
    protocol: HTTP
  - name: https
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
      - kind: Secret
        name: cafe-secret
        namespace: certificate
      options:
        nginx.org/ssl-protocols: "TLSv1.2 TLSv1.3"
        nginx.org/ssl-ciphers: "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:HIGH:!aNULL:!MD5"
        nginx.org/ssl-prefer-server-ciphers: "on"
EOF

This gateway configures:

  • http listener for HTTP traffic
  • https listener for HTTPS traffic. It terminates TLS connections using the cafe-secret we created. The SSL protocol and ciphers are also configured using the TLS options.

After creating the Gateway resource, NGINX Gateway Fabric will provision an NGINX Pod and Service fronting it to route traffic. Verify the gateway is created:

kubectl describe gateways.gateway.networking.k8s.io cafe

Verify the status is Accepted:

text
Status:
  Addresses:
    Type:   IPAddress
    Value:  10.96.36.219
  Conditions:
    Last Transition Time:  2026-01-09T05:40:37Z
    Message:               The Gateway is accepted
    Observed Generation:   1
    Reason:                Accepted
    Status:                True
    Type:                  Accepted
    Last Transition Time:  2026-01-09T05:40:37Z
    Message:               The Gateway is programmed
    Observed Generation:   1
    Reason:                Programmed
    Status:                True
    Type:                  Programmed
  • Save the public IP address and port(s) of the Gateway into shell variables:

    text
     GW_IP=XXX.YYY.ZZZ.III
     GW_HTTP_PORT=<http port number>
     GW_HTTPS_PORT=<https port number>
In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.

To create the httproute resources, copy and paste the following into your terminal:

yaml
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: cafe-tls-redirect
spec:
  parentRefs:
  - name: cafe
    sectionName: http
  hostnames:
  - "cafe.example.com"
  rules:
  - filters:
    - type: RequestRedirect
      requestRedirect:
        scheme: https
        port: 443
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: coffee
spec:
  parentRefs:
  - name: cafe
    sectionName: https
  hostnames:
  - "cafe.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /coffee
    backendRefs:
    - name: coffee
      port: 80
EOF

The first route issues a requestRedirect from the http listener on port 80 to https on port 443. The second route binds the coffee route to the https listener.

Send traffic

Using the external IP address and ports for the NGINX Service, we can send traffic to our coffee application.

If you have a DNS record allocated for cafe.example.com, you can send the request directly to that hostname, without needing to resolve.

To test that NGINX sends an HTTPS redirect, we will send requests to the coffee service on the HTTP port. We will use curl’s --include option to print the response headers (we are interested in the Location header).

curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/coffee --include
text
HTTP/1.1 302 Moved Temporarily
...
Location: https://cafe.example.com/coffee
...

Now access the application over HTTPS. Because the certificate is signed by a local self-signed CA that curl does not trust, use curl’s --insecure option to turn off certificate verification.

curl --resolve cafe.example.com:$GW_HTTPS_PORT:$GW_IP https://cafe.example.com:$GW_HTTPS_PORT/coffee --insecure
text
Server address: 10.244.0.6:80
Server name: coffee-6b8b6d6486-7fc78

HTTPS Traffic without SNI (Server Name Indication)

Some frontend load balancers strip out SNI information before the traffic reaches the NGINX gateway. In order for NGINX to still process and forward this traffic properly, you must define your HTTPS Listener without a hostname. This instructs NGINX Gateway Fabric to configure a default HTTPS virtual server to handle non-SNI traffic. The TLS configuration on this Listener will be used to verify and terminate TLS for this traffic, before the Host header is then used to forward to the proper virtual server to handle the request. You can attach your HTTPRoutes to this empty Listener.

By default, NGINX Gateway Fabric verifies that the Listener hostname matches both the SNI and Host header on an incoming client request. This does not require the SNI and Host header to be the same. This is to avoid misdirected requests, and returns a 421 response code. If you run into issues and want to disable this SNI/Host verification, you can update the NginxProxy CRD with the following field in the spec:

yaml
spec:
  disableSNIHostValidation: true

See also