Nginx + Cloudflare Security Hardening Checklist for Multi-Domain SaaS

A practical security hardening checklist for multi-domain SaaS servers running Nginx behind Cloudflare — covering .env blocking, WAF rules, rate limiting, SSL configuration, and header security.

Table of Contents

Core principle

On a multi-domain server, a misconfiguration in one site affects all of them. The goal of this checklist is not exhaustive hardening — it is eliminating the most common, high-impact vulnerabilities at the Nginx and Cloudflare layer before they become incidents.

All examples assume Ubuntu 24 with Nginx, behind Cloudflare (proxied), serving a mix of Next.js and PHP applications. See Next.js 15 on a Hetzner VPS for the deployment configuration these rules build on.

Nginx configuration

Block sensitive files across all server blocks

The highest-priority rule. .env, .git, composer.json, and related files must never be served publicly under any circumstances.


# Add to every server block — or to a shared include (see below)

location ~* (\.env|\.git|composer\.(json|lock)|package(-lock)?\.json|webpack\.config\.js|Dockerfile) {

    deny all;

    return 404;

}



location ~ /\. {

    deny all;

    return 404;

}

Add this to every server block. On a multi-domain setup it is easy to add the rule to three blocks and miss the fourth.

An exposed .env file leaks database credentials, SMTP API keys, payment provider secrets, and third-party service tokens in a single HTTP request. This is one of the most common server-level incidents for self-hosted SaaS — and one of the most preventable.

Disable version exposure


# In nginx.conf, inside http {}

server_tokens off;

Stops Nginx from advertising its version number in headers and error pages. Attackers targeting known CVEs for a specific version need that version number first.

Security headers

Add to each server block or to a shared include file:


add_header X-Frame-Options "SAMEORIGIN" always;

add_header X-Content-Type-Options "nosniff" always;

add_header X-XSS-Protection "1; mode=block" always;

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

For HTTPS-only domains (all of them):


add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Do not add HSTS until every subdomain on the domain is confirmed HTTPS. Once a browser caches the HSTS header it will refuse HTTP connections to any subdomain for the full max-age duration.

Rate limiting


# In nginx.conf, inside http {}

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;


# In the relevant server block

location /api/auth/login {

    limit_req zone=login burst=3 nodelay;

}



location /api/ {

    limit_req zone=api burst=10 nodelay;

}

Disable unused HTTP methods


if ($request_method !~ ^(GET|POST|PUT|PATCH|DELETE|OPTIONS)$) {

    return 405;

}

Adjust to match what your application actually uses.

Upload size limits


client_max_body_size 10M;

Set per application. A dental clinic platform accepting X-ray uploads needs a higher limit than a QR menu system.

Cloudflare configuration

SSL/TLS: Full (Strict)

Under SSL/TLS → Overview, set the mode to Full (Strict). This validates the SSL certificate on your origin server. Flexible mode sends unencrypted traffic from Cloudflare to your server — do not use it.

Force HTTPS

Under SSL/TLS → Edge Certificates:

Lock origin access to Cloudflare IPs

Direct requests to your server IP bypass Cloudflare entirely. Block them:


# Cloudflare IPv4 — verify current list at cloudflare.com/ips

allow 173.245.48.0/20;

allow 103.21.244.0/22;

allow 103.22.200.0/22;

allow 103.31.4.0/22;

allow 141.101.64.0/18;

allow 108.162.192.0/18;

allow 190.93.240.0/20;

allow 188.114.96.0/20;

allow 197.234.240.0/22;

allow 198.41.128.0/17;

allow 162.158.0.0/15;

allow 104.16.0.0/13;

allow 104.24.0.0/14;

allow 172.64.0.0/13;

allow 131.0.72.0/22;



# Cloudflare IPv6

allow 2400:cb00::/32;

allow 2606:4700::/32;

allow 2803:f800::/32;

allow 2405:b500::/32;

allow 2405:8100::/32;

allow 2a06:98c0::/29;

allow 2c0f:f248::/32;



allow 127.0.0.1;

deny all;

Cloudflare's IP ranges change periodically. Review cloudflare.com/ips when change notifications arrive.

WAF rules

Under Security → WAF:

Bot Fight Mode

Under Security → Bots, enable Bot Fight Mode (available on the free plan). Blocks a large volume of automated scanning traffic upstream.

Multi-domain specific: the shared include pattern

On a multi-domain setup, security rules added manually to each server block drift over time. A new domain gets stood up quickly and the security block gets skipped. A shared include file prevents this:


# /etc/nginx/snippets/security.conf

server_tokens off;



add_header X-Frame-Options "SAMEORIGIN" always;

add_header X-Content-Type-Options "nosniff" always;

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;



location ~ /\. { deny all; return 404; }

location ~* (\.env|\.git|composer\.(json|lock)|package(-lock)?\.json) {

    deny all;

    return 404;

}

Include it in every server block:


server {

    server_name example.com;

    include snippets/security.conf;

    # ... rest of config

}

Every new domain added to the server inherits the full security baseline automatically.

Environment variable isolation

On a server running multiple applications, ensure each .env file is:

If there is any reason to believe a .env was publicly accessible at any point: revoke and regenerate every credential in it immediately, review access logs for the exposure window, and check for unauthorised outbound email activity — spam sent via a compromised SMTP key is the most common immediate exploit.

Quick reference checklist


Nginx

[ ] server_tokens off

[ ] Sensitive file blocking (.env, .git, composer.json)

[ ] Dotfile blocking

[ ] Security headers (X-Frame-Options, HSTS, nosniff)

[ ] Rate limiting on login and API endpoints

[ ] client_max_body_size set per application

[ ] Shared include file used across all server blocks



Cloudflare

[ ] SSL/TLS set to Full (Strict)

[ ] Always Use HTTPS enabled

[ ] Minimum TLS 1.2

[ ] Origin locked to Cloudflare IPs only

[ ] Managed Ruleset (WAF) enabled

[ ] Bot Fight Mode enabled

[ ] Rate limiting rule on authentication endpoints

Summary

The configurations above are not optional hardening for cautious operators — they are the baseline for any SaaS running on a public-facing VPS. An exposed .env file, missing security headers, or an unlocked origin are incidents waiting to happen.

The shared include file pattern is the most leveraged change for a multi-domain setup: every new domain inherits the full security baseline without a manual check.

AKORNET runs four SaaS products across six domains on a single Hetzner VPS. This checklist reflects our production configuration. See our products at akor.net →

FAQ

How do you prevent an .env file from being publicly accessible via Nginx?

Add a location block that matches .env and other sensitive files and returns a 404. This must be added to every server block — a rule in one block does not protect others on the same server.

Should Cloudflare SSL mode be set to Flexible or Full (Strict)?

Full (Strict). Flexible mode sends unencrypted traffic from Cloudflare to your origin server, which defeats the purpose of HTTPS. Full (Strict) validates the SSL certificate on the origin server.

How do you lock an Nginx origin server to Cloudflare traffic only?

Add allow rules for all current Cloudflare IP ranges and a deny all rule at the bottom of each server block. Cloudflare publishes its IP ranges at cloudflare.com/ips — these change occasionally and should be reviewed when Cloudflare sends change notifications.

What is the benefit of a shared Nginx include file for security headers?

On a multi-domain setup, security rules added manually to each server block tend to drift over time — a new domain gets set up quickly and the security block gets skipped. A shared include file means every server block inherits the same baseline automatically.

Need help implementing this?

Talk with the AKORNET team about your project or SaaS infrastructure.

Get in Touch →