Hardening Nginx on HestiaCP: Mitigating Layer 7 DDoS Attacks and Speeding Up Dynamic Content with Caching
How to customize HestiaCP's Nginx templates to throttle requests, block slow connections and enable FastCGI Cache, cutting the impact of application-layer DDoS attacks while speeding up dynamic pages.
Anyone running a server with HestiaCP eventually runs into a familiar problem: the site slows to a crawl, CPU usage spikes, and the Nginx logs fill up with thousands of requests that look harmless on their own. Often this isn't a "good" traffic surge, it's an application-layer DDoS attack: one of those attacks that doesn't try to saturate network bandwidth, but instead exhausts server resources (CPU, memory, database connections) by sending HTTP requests that look perfectly legitimate. In this guide we'll work directly on HestiaCP's Nginx templates to add protection against this kind of attack, and at the same time use Nginx's caching features to take load off dynamic pages (for example pages generated by PHP), making the site more resilient even when traffic is legitimate but very high.
What a "Layer 7" DDoS attack is, and why it's Nginx's problem
DDoS (Distributed Denial of Service) attacks fall into different categories depending on which layer of the OSI network model they target. Attacks on Layers 3 and 4 (network and transport) try to saturate bandwidth or connection tables, and are typically filtered upstream of the server, for example by the hosting provider or a protection service such as a CDN. Layer 7 attacks (the "application" layer, where the HTTP protocol lives) are trickier because individual requests are almost indistinguishable from a real user's: a request to a login page, a site search, or an endpoint that queries the database, repeated thousands of times per second from many different IP addresses, can bring a server to its knees even when the network connection itself is nowhere near saturated.
Since Nginx is the web server that receives every HTTP request first in a HestiaCP installation (whether it runs standalone or as a reverse proxy in front of Apache), it's the ideal place to apply the first line of defense: it can limit how many requests it accepts from a single IP address, how many simultaneous connections it keeps open, and it can "absorb" part of the traffic by serving cached responses instead of regenerating them every time.
Why you shouldn't edit HestiaCP's original templates
In HestiaCP, the Nginx configuration for each domain isn't written by hand: it's generated automatically from model files called templates, which contain placeholders such as %domain%, %user%, %docroot% or %backend_lsnr% that get replaced with the site's real values whenever the configuration is created or rebuilt. Templates live in one of two locations on the server, depending on the setup:
- Nginx as the only web server (with PHP-FPM):
/usr/local/hestia/data/templates/web/nginx/php-fpm/ - Nginx as a reverse proxy in front of Apache:
/usr/local/hestia/data/templates/web/nginx/
HestiaCP's official documentation explicitly advises against editing the default template files (for example default.tpl): every time the panel is updated, these files get overwritten and any changes would be lost. The correct approach is to copy the template you want to customize under a new name, and edit only the copies. Each template typically needs a few files sharing the same base name but different extensions, for example hardened.tpl, hardened.stpl (the SSL-enabled variant) and, if present, the supporting hardened.sh script.
Creating the custom template step by step
After connecting to the server over SSH as root (or as a user with the right permissions), creating a template based on the default one looks like this:
cd /usr/local/hestia/data/templates/web/nginx/php-fpm/
cp default.tpl hardened.tpl
cp default.stpl hardened.stpl
At this point hardened.tpl and hardened.stpl are two independent copies that can be edited safely, without risking the changes being lost on the next HestiaCP update. The sections below show what to add inside these files to get the two main protections: request limiting and caching.
Limiting requests per IP address: limit_req
The first and most effective countermeasure against an application-layer attack is preventing a single IP address from sending an excessive number of requests in a short time. Nginx offers the limit_req module for exactly this purpose, which works in two steps: first you define a "shared memory zone" that tracks requests per IP, then you apply that zone to one or more location blocks in the configuration.
The zone definition goes in Nginx's global http block (the /etc/nginx/nginx.conf file, or a file inside /etc/nginx/conf.d/ that gets included automatically), not inside the per-domain template:
# /etc/nginx/conf.d/ratelimit.conf
limit_req_zone $binary_remote_addr zone=generic:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
Here generic is a zone that allows an average of 30 requests per minute per IP across the whole site, while login is a stricter zone (10 requests per minute) aimed specifically at authentication pages, which are typically this type of attack's favorite target. The 10m value specifies how much memory is reserved to track IP addresses: 10 MB is enough for roughly 160,000 distinct addresses, more than enough for most sites.
With that in place, inside the hardened.tpl template you apply the generic zone to the whole site and the stricter one to the login page:
server {
# ... directives generated automatically by HestiaCP ...
limit_req zone=generic burst=15 nodelay;
location = /wp-login.php {
limit_req zone=login burst=5 nodelay;
include /etc/nginx/fastcgi_params;
fastcgi_pass %backend_lsnr%;
}
}
The burst parameter lets short bursts of legitimate requests through (for example when a browser loads several resources in parallel) without blocking the visitor, while nodelay stops excess requests from being queued, returning a 503 Service Temporarily Unavailable error immediately to anyone over the limit. That way, a bot hammering the login page quickly starts getting errors and stops consuming server resources, while a normal visitor never notices anything.
Limiting simultaneous connections: limit_conn
A complementary technique is limiting how many connections a single IP can keep open at the same time, useful against attacks that open many connections and hold them busy for as long as possible. The logic again works in two steps:
# /etc/nginx/conf.d/ratelimit.conf
limit_conn_zone $binary_remote_addr zone=perip:10m;
server {
limit_conn perip 20;
}
With this configuration, a single IP address can't have more than 20 open connections to that site at once: beyond that threshold, Nginx responds with an error without even passing the request on to the PHP backend or Apache, saving valuable resources.
Protecting against slow connections (Slowloris)
A family of application-layer attacks known as Slowloris doesn't send many requests at all; instead it opens connections and keeps them busy by trickling data in extremely slowly, exhausting the maximum number of connections the server can handle. The countermeasure is to set tight timeouts for receiving request headers and bodies, so Nginx closes connections that are too slow:
server {
client_header_timeout 10s;
client_body_timeout 10s;
send_timeout 10s;
}
Nginx's default values for these timeouts are 60 seconds, which is plenty of time for a Slowloris attack to saturate the available connections; lowering them to 10 seconds (a value you can tune based on how slow your real visitors' average connections are) drastically shrinks the attack window with no noticeable impact on users with a normal connection.
Blocking suspicious IPs and User-Agents
When log analysis during an attack reveals recurring IP addresses or an unusual User-Agent string used by the malicious traffic (many simple bots use an empty or obviously fake User-Agent), you can add a direct block inside the template:
server {
# Block an IP range
deny 203.0.113.0/24;
# Block requests with an empty or suspicious User-Agent
if ($http_user_agent ~* "^$|curl|wget|python-requests") {
return 403;
}
}
This technique should be used with care: blocking by User-Agent only helps against the crudest bots, since more sophisticated ones perfectly mimic a real browser's User-Agent, but it's still a near-zero-cost filter that can strip out some unwanted traffic before it reaches the rest of the configuration.
Speeding up dynamic content with FastCGI Cache
The protections above cut down malicious traffic, but the second pillar of resilience is making sure legitimate traffic (even a lot of it, say during an organic traffic spike) weighs as little as possible on the server. For PHP-based sites (WordPress included) running on HestiaCP with Nginx and PHP-FPM, the answer is Nginx's built-in FastCGI Cache: the first time a page is requested, Nginx generates it normally by passing the request to PHP-FPM, but it saves a copy; subsequent requests for the same page are served straight from the cache, without touching PHP or the database at all, for a huge speed gain and almost no load on the server.
Here too, the cache zone needs to be defined globally:
# /etc/nginx/conf.d/fastcgi-cache.conf
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=WPCACHE:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
Then, inside the hardened.tpl template, HestiaCP provides a specific mechanism: right after the include fastcgi_params line in the block that handles PHP files, add a reference to the per-domain configuration file that the panel generates automatically once caching is enabled from the interface:
location ~ [^/]\.php(/|$) {
fastcgi_pass %backend_lsnr%;
fastcgi_index index.php;
include /etc/nginx/fastcgi_params;
include %home%/%user%/conf/web/%domain%/nginx.fastcgi_cache.conf*;
fastcgi_cache WPCACHE;
fastcgi_cache_valid 200 60m;
fastcgi_cache_use_stale updating error timeout invalid_header http_500;
add_header X-FastCGI-Cache $upstream_cache_status;
}
The X-FastCGI-Cache header is very handy for checking things work: it shows up in the HTTP response with the value HIT when a page is served from cache, MISS when it's generated by PHP and cached for the first time, and you can easily check it in your browser's developer tools (Network tab) or from a terminal with curl -I https://yoursite.com/. The fastcgi_cache_use_stale option with the updating parameter matters a lot during a traffic spike: if a page's cache entry expires at the exact moment hundreds of simultaneous requests arrive, Nginx sends only one of them to PHP to regenerate it, while serving everyone else the slightly stale previous version until the new one is ready, preventing all of them from hitting the backend at once.
Not every page should be cached: a CMS's admin area, or pages showing content personalized for the logged-in user, need to be excluded, otherwise one user could end up seeing another user's data. HestiaCP already handles part of this through the nginx.fastcgi_cache.conf file the panel generates, but for specific CMSs such as WordPress it's worth double-checking that cookies like session cookies or an e-commerce cart cookie correctly disable caching for that request via a cookie-based fastcgi_cache_bypass directive.
Applying the new template and checking the result
Once hardened.tpl and hardened.stpl are ready, turning them on for a domain is entirely handled through the HestiaCP interface:
- Log into the panel, open Web > [domain name] > Edit.
- In the Nginx template drop-down, select the new
hardenedtemplate (it shows up automatically in the list once the files are copied). - Save: HestiaCP regenerates the domain's configuration from the new template.
Alternatively, from the command line, the same result is achieved with:
v-change-web-domain-tpl user domain.com hardened
v-rebuild-web-domain user domain.com yes
where the final yes also forces a restart of the web services involved. Before applying the change in production, it's always a good idea to check the configuration syntax with nginx -t, which flags any errors without interrupting the running service, and to test the caching and rate-limiting behavior on a test domain or during a low-traffic window.
To clear the FastCGI cache after a change to the site (for example a theme update or new content), HestiaCP provides a "Purge Nginx Cache" button on the domain's edit page, or the v-purge-nginx-cache user domain.com command from the command line.
Conclusions
No single measure described in this guide is, by itself, a complete defense against a serious, sustained application-layer DDoS attack: that requires dedicated solutions upstream of the server, such as a CDN with DDoS protection or a Web Application Firewall. Still, combining request limiting, connection limiting, tight timeouts for slow clients, and a well-configured FastCGI cache on HestiaCP's Nginx templates significantly raises the amount of malicious traffic the server can absorb on its own, while also improving site performance during periods of high legitimate traffic. It's the kind of low-cost, high-benefit work that's worth doing on any HestiaCP server exposed publicly on the internet.
Official sources
Share
What's Your Reaction?
Like
0
Dislike
0
Love
0
Funny
0
Angry
0
Sad
0
Wow
0