HTTP/2 support is often treated as a magic switch, but the reality is that Kubernetes Ingress controllers don’t automatically enable it; you have to explicitly configure your Ingress resources to signal their readiness.

Let’s see what this looks like in practice. Imagine we have a simple Nginx Ingress controller and a backend service my-app running on port 80.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: "HTTP" # This is the default, but good to be explicit
spec:
  ingressClassName: nginx
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app
            port:
              number: 80

This Ingress resource, as is, will only serve HTTP/1.1. To enable HTTP/2, we need to signal to the Ingress controller that the backend service itself is capable of speaking HTTP/2.

The core problem is that Kubernetes Ingress, by design, is an abstraction over a reverse proxy. It doesn’t dictate the protocol between the Ingress controller and the backend pods. The Ingress controller needs a hint.

Here are the common ways to achieve HTTP/2, from most common to less so:

  1. Backend Protocol Annotation (Nginx Ingress Controller): This is the most direct and common method for Nginx Ingress. You tell the controller what protocol to use when talking to your backend.

    • Diagnosis: Check the nginx.ingress.kubernetes.io/backend-protocol annotation on your Ingress resource. If it’s HTTP or absent, it defaults to HTTP/1.1.

    • Fix: Change the annotation to HTTPS or GRPC (if your backend supports gRPC, which implies HTTP/2). If you use HTTPS here, you must also configure TLS on your backend service. The Ingress controller will then speak TLS to your backend, and most TLS implementations will negotiate HTTP/2 if both sides support it.

      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: my-app-ingress
        annotations:
          nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" # Crucial change
      spec:
        ingressClassName: nginx
        tls: # TLS is now required for the backend connection
        - hosts:
          - myapp.example.com
          secretName: my-app-tls-secret # Secret containing your backend TLS cert/key
        rules:
        - host: myapp.example.com
          http:
            paths:
            - path: /
              pathType: Prefix
              backend:
                service:
                  name: my-app
                  port:
                    number: 443 # Your backend must be listening on 443 for TLS
      
    • Why it works: By setting backend-protocol to HTTPS, you instruct the Nginx Ingress controller to establish a TLS connection to the backend service on port 443. During the TLS handshake, HTTP/2 is negotiated if both the Ingress controller and the backend pods support it. If you’re not actually terminating TLS at the backend, this annotation is a misnomer, but it’s the mechanism Nginx uses to trigger the HTTP/2 negotiation.

  2. Backend Service TLS Configuration: This is intrinsically linked to the above. If your backend service is configured to accept TLS connections (and it should be if you’re using HTTPS for backend-protocol), then the negotiation for HTTP/2 happens as part of the TLS handshake.

    • Diagnosis: Ensure your backend Service in Kubernetes is pointing to pods that are listening on a TLS port (typically 443) and are configured to serve TLS. Check your Deployment or StatefulSet for the container’s ports and ensure containerPort is set to 443 and that the application within the pod is actually configured to use TLS.

    • Fix: Modify your Service to expose port 443 and ensure your Deployment/StatefulSet has pods listening on 443 with TLS enabled.

      # Example Service for backend TLS
      apiVersion: v1
      kind: Service
      metadata:
        name: my-app
      spec:
        selector:
          app: my-app
        ports:
        - protocol: TCP
          port: 443 # Service port
          targetPort: 443 # Container port
      
    • Why it works: Modern TLS libraries (like OpenSSL, which Nginx uses) support Application-Layer Protocol Negotiation (ALPN) during the handshake. ALPN allows the client (Ingress controller) and server (backend pod) to negotiate which application-layer protocols they will use. If both support HTTP/2 and TLS, it will be preferred over HTTP/1.1.

  3. Ingress Controller Specific Configuration (Less Common for HTTP/2 Backend): Some Ingress controllers might have global or specific configurations that force HTTP/2 for all backends or specific backends, independent of the backend-protocol annotation. This is less common for the backend connection but more common for the client-to-ingress connection.

    • Diagnosis: Consult the specific documentation for your Ingress controller (e.g., Traefik, HAProxy Ingress, Istio Gateway). Look for global configuration options or custom resource definitions (CRDs) that might enable HTTP/2 for egress traffic from the controller.
    • Fix: Apply the controller-specific configuration. For example, with Traefik, you might configure serversTransport in the static configuration.
    • Why it works: The Ingress controller itself is configured to offer or prefer HTTP/2 when initiating connections to its upstream services.
  4. HTTP/2 Proxying in the Application: If you absolutely cannot configure your backend to speak TLS or HTTP/2 directly, you could potentially run an HTTP/2-aware proxy within your Kubernetes pods, in front of your application.

    • Diagnosis: Your application pods are only listening on HTTP/1.1, and the Ingress controller is configured to talk HTTP/1.1 to them.
    • Fix: Deploy a sidecar proxy (like Envoy or a minimalist Go HTTP/2 proxy) in your pods. Configure the Ingress controller to talk to the sidecar (e.g., via backend-protocol: HTTP on a local port), and configure the sidecar to talk HTTP/2 to your actual application. This is complex and generally discouraged.
    • Why it works: The sidecar acts as a protocol translator, accepting HTTP/1.1 from the Ingress controller and then establishing an HTTP/2 connection to the application, or vice-versa.
  5. Client-to-Ingress HTTP/2: It’s important to distinguish between client-to-Ingress HTTP/2 and Ingress-to-backend HTTP/2. The former is often enabled by default on the Ingress controller’s public-facing listener if the controller supports it and the client offers it. The latter, which we’ve been discussing, is about the connection from the Ingress controller to your backend services.

    • Diagnosis: Use curl --http2 (or curl -v --http2) against your Ingress’s public endpoint. If it successfully negotiates HTTP/2 and shows performance benefits, your client-to-Ingress is working. The problem is usually the backend connection.
    • Fix: Ensure your Ingress controller is configured to listen on its public interface with HTTP/2 support enabled (often default for modern controllers like Nginx Ingress). For Nginx Ingress, this is usually controlled by the http2 directive in its configuration, which is often enabled by default.
    • Why it works: The Ingress controller’s frontend listener advertises HTTP/2 support via ALPN, and clients (browsers, curl) can then establish an HTTP/2 connection.

The most common pitfall is forgetting that nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" requires your backend service and pods to actually be configured for TLS on the port the Ingress controller is trying to reach. If your backend is only listening on HTTP/1.1, setting backend-protocol to HTTPS will cause connection failures, not HTTP/2.

After successfully configuring HTTP/2 to your backend, the next thing you’ll likely encounter is managing the lifecycle of TLS certificates for your backend services, especially if you’re using HTTPS as the backend-protocol.

Want structured learning?

Take the full Http2 course →