Envoy Gateway TLS and Security: Set Up HTTPS and Stop Your API from Running Naked

5 min read

Getting HTTP working is just the warmup. Unless your service lives in the Stone Age, production almost certainly needs HTTPS. This post covers the TLS fundamentals for Envoy Gateway so your APIs stop running in the clear.

How TLS Works in Gateway

The most common approach: terminate TLS at the Gateway listener.

Meaning:

  • The client connects via HTTPS
  • The Gateway handles the handshake and certificate
  • Traffic is then forwarded to your Service unencrypted internally

A basic HTTPS listener looks like this:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            group: ""
            name: example-cert

Two things matter here:

  • protocol: HTTPS
  • tls.mode: Terminate

This means: TLS ends here, and the Gateway handles the certificate.

HTTPS + HTTPRoute vs TLSRoute: What's the Difference

This is important, because many people see TLSRoute and immediately ask:

"Isn't an HTTPS listener already enough? Why do we need a separate TLSRoute?"

The difference is whether the Gateway terminates TLS:

Scenario Common Combination Description
Gateway terminates TLS, then applies HTTP rules HTTPS listener + HTTPRoute Most common website/API scenario
Gateway doesn't decrypt — routes encrypted traffic based on SNI only TLS listener + TLSRoute TLS passthrough scenario

In other words:

  • If you want to inspect path, header, method at the Gateway, you need to terminate TLS first, then use HTTPRoute
  • If you want to preserve end-to-end encryption and not decrypt at the Gateway, TLSRoute is the right path

Neither approach is "more advanced" — the point is knowing what you want: "terminate TLS and do L7 routing," or "keep encryption intact and do SNI-based routing."

Prepare the Certificate Secret

For test environments, a self-signed certificate works. The official examples use exactly this:

openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
  -subj '/O=example Inc./CN=example.com' \
  -keyout example.com.key \
  -out example.com.crt
 
openssl req -out www.example.com.csr -newkey rsa:2048 -nodes \
  -keyout www.example.com.key \
  -subj "/CN=www.example.com/O=example organization"
 
openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key \
  -set_serial 0 -in www.example.com.csr -out www.example.com.crt

Store the cert/key as a Kubernetes Secret:

kubectl create secret tls example-cert \
  --key=www.example.com.key \
  --cert=www.example.com.crt

The Gateway can then reference this Secret via certificateRefs.

⚠️ Self-signed certs are for testing only. In production, use cert-manager or your existing certificate management workflow. Don't carry the demo setup directly into production — that's like surviving on instant noodles: fine occasionally, not sustainable long-term.

Add an HTTPS Listener to an Existing Gateway

If you already have a eg Gateway from the previous post, you can patch it directly:

kubectl patch gateway eg --type=json --patch '
  - op: add
    path: /spec/listeners/-
    value:
      name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
        - kind: Secret
          group: ""
          name: example-cert
'

Verify the status:

kubectl get gateway/eg -o yaml

If the listener looks healthy, test HTTPS:

curl -v -HHost:www.example.com \
  --resolve "www.example.com:443:${GATEWAY_HOST}" \
  --cacert example.com.crt \
  https://www.example.com/get

This command is great for local validation, especially when DNS isn't wired up yet.

One Gateway, Multiple HTTPS Domains

If you have multiple domains, add multiple HTTPS listeners to the same Gateway. For example, adding foo.example.com:

listeners:
  - name: https-main
    protocol: HTTPS
    port: 443
    hostname: www.example.com
    tls:
      mode: Terminate
      certificateRefs:
        - name: example-cert
  - name: https-foo
    protocol: HTTPS
    port: 443
    hostname: foo.example.com
    tls:
      mode: Terminate
      certificateRefs:
        - name: foo-cert

Don't forget to update the corresponding HTTPRoute with the right hostname — otherwise the listener might be listening, but the route doesn't know where to send the traffic.

Mental Model for TLSRoute

For TLS passthrough, think of TLSRoute like this:

💡 The apiVersion for TLSRoute may differ depending on which version of Gateway API CRDs you've installed. The example below is for conceptual illustration — check your cluster's installed version before applying.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg-tls
spec:
  gatewayClassName: eg
  listeners:
    - name: tls
      protocol: TLS
      port: 443
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
  name: passthrough-route
spec:
  parentRefs:
    - name: eg-tls
  hostnames:
    - "db.example.com"
  rules:
    - backendRefs:
        - name: db-service
          port: 5432

Common use cases for this pattern:

  • Routing based only on SNI hostname
  • Preserving encryption all the way to the backend
  • No HTTP path/header parsing at the Gateway

So the biggest difference between TLSRoute and HTTPRoute isn't just the name — they're operating at entirely different traffic layers.

Cross-Namespace Certificate References

The official docs highlight a useful capability: A Gateway can reference certificate Secrets from other namespaces — but it requires a ReferenceGrant.

For example, if the platform team stores certificates centrally in envoy-gateway-system, but the application Gateway is in default, you can't just reference across namespace boundaries without explicit authorization.

Example:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-default-to-read-cert
  namespace: envoy-gateway-system
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: Gateway
      namespace: default
  to:
    - group: ""
      kind: Secret

This means:

  • A Gateway in the default namespace
  • Is authorized to reference Secret resources in envoy-gateway-system

Without a ReferenceGrant, this cross-namespace reference is treated as invalid. It's a protection mechanism — not Gateway API being unnecessarily strict.

One-Line Summary

Envoy Gateway TLS really isn't as mysterious as it sounds: prepare the certificate Secret, configure the HTTPS listener, confirm your route and hostname align — and that's basically it.

💡 The most common beginner mistake isn't that TLS is hard — it's that three things aren't aligned: hostname, certificateRefs, and HTTPRoute hostnames. If any one of them is off, traffic starts misbehaving.

Next Step

Now that you've covered installation, concepts, routing, and TLS, the next post consolidates this into a set of practices that are less likely to blow up in production: 👉 Best Practices