← News
/PLAYBOOK · 2026-09-11

Nginx HTTP/2 hardening in practice: defend Rapid Reset

HTTP/2 with no limits gets resource-exhausted by Rapid Reset. This article covers Nginx HTTP/2 hardening: http2_max_concurrent_streams, keepalive_requests, limit_conn and limit_req as backstop, h2c off, plus h2load verification and CDN real-IP false-ban triage.

Applicable scenarios

HTTP/2 is on for most mainstream sites, but most configs care about "enable" and ignore "limit". Rapid Reset (CVE-2023-44487), disclosed in 2023, showed an attacker can open and immediately reset streams at high rate on a single TCP connection, bypass classic per-IP connection caps, and fill server memory and CPU at very low bandwidth. This is for Nginx sites that already have HTTP/2 on. Full hardening: concurrent stream limits, connection reuse caps, and timeout backstops. Fits ops still on older Nginx minor versions, or with no HTTP/2 rate limits at all.

Prerequisites

How it works

HTTP/2 multiplexing handles many request streams in parallel on one TCP connection. Default Nginx caps max concurrent streams per connection via http2_max_concurrent_streams, but counting of "established then immediately RST_STREAM" streams is not strict. Rapid Reset exploits that gap: the client sends HEADERS then immediately RST_STREAM. The server already allocated stream resources; the request is cancelled. One connection can create thousands of create–destroy cycles per second.

Effective hardening is three layers:

Procedure

1. Confirm version and current HTTP/2 state

nginx -v
# Example output: nginx version: nginx/1.25.4
nginx -T 2>/dev/null | grep -i http2

If below 1.25.3 and you cannot upgrade, enable HTTP/2 on the fronting CDN or WAF and disable HTTP/2 on origin.

2. Configure HTTP/2 concurrent streams and connection-reuse limits

Declare globals in the http block, or override in a specific server / location:

# /etc/nginx/nginx.conf — http block
http {
    # Max concurrent streams per connection: default 128; lower this against Rapid Reset
    http2_max_concurrent_streams 64;

    # Max requests a connection may handle: after this the server sends GOAWAY and forces reconnect
    keepalive_requests 200;

    # Connection idle timeout: shortening speeds up resource reclaim
    keepalive_timeout 15s;

    # Request header read timeout: blocks slow-header attacks
    client_header_timeout 10s;
    client_body_timeout 15s;

    # Per-IP concurrent connection and request rate limits (connection-layer backstop)
    limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
    limit_req_zone  $binary_remote_addr zone=perip_req:10m rate=30r/s;

    include /etc/nginx/conf.d/*.conf;
}

3. Enable and apply limits in the 443 server block

server {
    listen 443 ssl;
    server_name helixcyber.ai;

    # Nginx 1.25.1+ uses a standalone directive to enable HTTP/2
    http2 on;

    ssl_certificate     /etc/nginx/ssl/example.crt;
    ssl_certificate_key /etc/nginx/ssl/example.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 10m;

    # Apply connection and request rate limits
    limit_conn perip_conn 50;
    limit_req  zone=perip_req burst=60 nodelay;

    # Cap concurrent HTTP/2 connections per IP (connection layer)
    limit_conn_status 429;

    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Parameter points: http2_max_concurrent_streams 64 means at most 64 parallel streams per connection; keepalive_requests 200 closes the connection after 200 requests — an attacker who wants to hold a high rate must keep opening new connections and then hits limit_conn; client_header_timeout 10s blocks slow-header variants.

4. Explicitly disable cleartext h2c

h2c (HTTP/2 over cleartext) has no TLS and is easy to abuse for request smuggling. Disable everywhere except internal trusted links. Check that port 80 is 301-only, no http2:

server {
    listen 80;
    server_name helixcyber.ai;
    return 301 https://$host$request_uri;
}

5. Apply config

sudo nginx -t && sudo systemctl reload nginx

Configuration verification

Confirm HTTP/2 is on and rate limits fire:

# Check the protocol negotiated by the server
curl -sI --http2 https://helixcyber.ai | head -1
# Expected: HTTP/2 200

# Confirm via ALPN that the TLS layer negotiated h2
openssl s_client -connect helixcyber.ai:443 -alpn h2 -servername helixcyber.ai </dev/null 2>/dev/null | grep -i "ALPN protocol"
# Expected: ALPN protocol: h2

Verify concurrent-stream and request-rate limits trigger:

# Fire many requests quickly from one IP and watch for 429
ab -n 500 -c 80 -k https://helixcyber.ai/ 2>&1 | grep -E "Non-2xx|Complete requests"
# Expected: Non-2xx responses (429); not all 200 means rate limiting is in effect

Watch error log for trigger records:

tail -f /var/log/nginx/error.log | grep -iE "limiting connections|limiting requests"

FAQ

FAQ 1: Open-source Nginx does not show http2_max_concurrent_streams. How do you confirm it took effect?

After Nginx 1.25.1 this directive is managed by the http2 directive and the new HTTP/2 module. Some versions of `nginx -T` do not echo it. Confirm at runtime: use `h2load` (nghttp2 client) with a specified concurrent stream count and watch whether the connection refuses new streams after the threshold:

h2load -n 1000 -c 10 -m 128 https://helixcyber.ai/
# If the concurrent-stream cap is 64, max concurrent streams in the output should show 64

If the version truly lacks the directive, upgrade Nginx first, or cap concurrent streams uniformly at the CDN layer.

FAQ 2: After adding limit_conn, real users occasionally get 429. How do you balance?

429 false positives usually come from a too-tight threshold or CDN-to-origin. Handling principles:

After tuning, regression-test login, checkout, and other critical paths. Confirm no 429 before full rollout.

Summary

HTTP/2 security is not the protocol; it is "enabled with no limits". Rapid Reset-class attacks bypass per-IP connection caps, so hardening focus is concurrent stream ceiling (http2_max_concurrent_streams), per-connection request ceiling (keepalive_requests), and dual connection-count / rate limits. Land in order: confirm Nginx already contains Rapid Reset mitigation, set concurrent-stream and request thresholds to traffic, then verify with h2load and ab that limits actually fire, and check that real IP is restored in CDN scenarios.