Skip to content
← All field notes

Note / 002 · Infrastructure

Cloudflare reverse proxy and DNS

How DNS, TLS and Nginx fit together—and the configuration details that prevent redirect loops and exposed origins.

Published
2026-04-02
Reading time
6 min read

When several applications share a server, Cloudflare and Nginx can form a clean edge-to-origin path: Cloudflare handles public DNS and edge traffic, while Nginx selects the application by hostname and proxies to a private local port.

The setup is simple when each layer has one responsibility. Most failures happen when DNS, TLS and proxy behavior are changed together without checking where the request actually stops.

Understand the request path

For api.example.com, the request typically moves through four stages:

  1. Public DNS resolves the hostname to Cloudflare.
  2. Cloudflare accepts the visitor's HTTPS connection.
  3. Cloudflare opens a second connection to the origin server.
  4. Nginx routes the request to the application listening on a private port.

That means there are two TLS connections to reason about—not one.

Configure DNS deliberately

Create an A record for IPv4 or an AAAA record for IPv6. A CNAME is useful when the hostname should follow another hostname.

Cloudflare's orange-cloud proxy hides the origin behind Cloudflare's network and enables its edge features. DNS-only mode exposes the configured origin address directly. Use DNS-only temporarily when isolating a problem, not as an unexplained permanent workaround.

DNS changes are cached. Before editing more settings, verify what different resolvers return:

dig api.example.com A
dig @1.1.1.1 api.example.com A
dig +trace api.example.com

Use Full (strict) TLS

Use Full (strict) so Cloudflare validates the certificate presented by the origin. The origin certificate can be a publicly trusted certificate or a Cloudflare Origin CA certificate when requests always pass through Cloudflare.

Avoid Flexible mode for an HTTPS application. Flexible uses HTTPS from the visitor to Cloudflare but HTTP from Cloudflare to the origin. If the origin redirects HTTP to HTTPS, the two layers can create an infinite redirect loop.

Keep the application private

Bind the application to a loopback or private container address rather than exposing its port publicly. Let Nginx be the controlled entry point.

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/certs/origin.pem;
    ssl_certificate_key /etc/nginx/certs/origin.key;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Test configuration before reloading:

sudo nginx -t
sudo systemctl reload nginx

Use a reload instead of a restart when possible so valid configuration can be applied without dropping active connections.

Restore the real visitor address safely

Without additional configuration, Nginx sees a Cloudflare proxy address as the remote client. Cloudflare sends the visitor address in CF-Connecting-IP, but that header is trustworthy only when the request came from Cloudflare.

Configure Nginx's real IP module with Cloudflare's published proxy ranges and reject or firewall direct public traffic to the origin. Blindly trusting a forwarded header from any client allows address spoofing and weakens rate limits and audit logs.

Protect the origin

The orange-cloud proxy does not protect an origin that remains directly reachable by its IP address.

  • Allow inbound web traffic only from Cloudflare's published IP ranges, or use an authenticated Cloudflare Tunnel.
  • Keep SSH restricted separately; do not route it through ordinary proxied DNS records.
  • Do not expose application ports such as 3000 or 8080 publicly.
  • Rotate an origin IP if it was previously published and hiding it is part of the threat model.

Debug one hop at a time

When the site is unavailable, test from the inside out:

  1. Is the application listening, and does curl http://127.0.0.1:3000/health work on the server?
  2. Does Nginx accept the hostname locally with the correct Host header?
  3. Does the origin certificate match the hostname and remain valid?
  4. Does public DNS return the intended record?
  5. What Cloudflare error code appears, and which connection does it describe?

Cloudflare 521, 522 and 525 errors point to different origin failures. Treating every one as “DNS is broken” creates unnecessary configuration changes and hides the first useful clue.

The useful mental model

DNS answers where to connect. TLS establishes identity and encryption. Nginx decides where the request goes next. Cloudflare sits in front of all three but does not remove the need to configure the origin correctly.

Debug the boundary between layers, not the entire stack at once.

Further reading