--- title: "How to Harden Apache and Nginx on Ubuntu 24.04" description: "Harden Apache and Nginx on Ubuntu 24.04: hide versions, disable directory listing, limit requests, enforce modern TLS, add security headers and ban attackers." category: "Web Servers" language: en source: https://cubepath.com/docs/web-servers/apache-and-nginx-hardening updated: 2026-09-25 --- # How to Harden Apache and Nginx on Ubuntu 24.04 The default Apache and Nginx configurations on Ubuntu are built to work out of the box, not to expose as little as possible. A few changes reduce what an attacker can learn about your server and limit the damage of common attacks: hiding version numbers, turning off directory listings, rejecting unknown host names, enforcing modern TLS, sending security headers and banning clients that brute force your login pages. This tutorial applies these changes on Ubuntu 24.04, with the Apache and Nginx variant of each step side by side, so you can follow only the one you use. ## Prerequisites To follow this tutorial you need: - A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has `sudo` privileges. - Apache (`apache2`) or Nginx installed from the Ubuntu repositories and serving at least one site. - A domain name (`your_domain` in the examples) pointing to the server, and a TLS certificate from Let's Encrypt. If you don't have one yet, install Certbot with `sudo apt install certbot python3-certbot-nginx` (or `python3-certbot-apache`) and run `sudo certbot --nginx` (or `--apache`). - UFW allowing only the ports you need, for example `sudo ufw allow OpenSSH` and `sudo ufw allow "Nginx Full"` or `sudo ufw allow "Apache Full"`. Before each reload, always run the configuration test for your server (`sudo nginx -t` or `sudo apache2ctl configtest`). A reload with a broken configuration is refused, but a restart takes the site down. ## Step 1 - Keeping the web server updated Most real-world compromises exploit known, already fixed vulnerabilities. Ubuntu 24.04 installs `unattended-upgrades` by default, which applies security updates daily. Confirm it is active: ```bash systemctl status unattended-upgrades --no-pager cat /etc/apt/apt.conf.d/20auto-upgrades ``` ```output APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; ``` If the file is missing, enable automatic security updates with `sudo dpkg-reconfigure -plow unattended-upgrades`. Security fixes for `nginx` and `apache2` are then installed and the services restarted automatically. ## Step 2 - Hiding version information By default both servers advertise their version in the `Server` header and on error pages, which tells attackers exactly which vulnerabilities to try. ### Nginx Open the main configuration: ```bash sudo nano /etc/nginx/nginx.conf ``` In the `http` block, find the commented line `# server_tokens off;` and uncomment it: ```nginx http { ... server_tokens off; ... } ``` ### Apache Ubuntu keeps these settings in a separate file: ```bash sudo nano /etc/apache2/conf-available/security.conf ``` Set the following values (the file already contains them with less strict defaults): ```apache ServerTokens Prod ServerSignature Off TraceEnable Off ``` `ServerTokens Prod` reduces the header to `Server: Apache`, `ServerSignature Off` removes the version footer from error pages and `TraceEnable Off` disables the HTTP `TRACE` method. Test and reload the server you use: ```bash sudo nginx -t && sudo systemctl reload nginx sudo apache2ctl configtest && sudo systemctl reload apache2 ``` Verify the header: ```bash curl -sI https://your_domain/ | grep -i '^server' ``` ```output Server: nginx ``` ## Step 3 - Disabling directory listing and unused modules ### Apache The default `` block enables `Indexes`, so any directory without an `index.html` shows its contents. Edit `/etc/apache2/apache2.conf`: ```bash sudo nano /etc/apache2/apache2.conf ``` Remove `Indexes` from the `Options` line: ```apache Options FollowSymLinks AllowOverride None Require all granted ``` Then disable modules you don't use. `autoindex` generates directory listings and `status` exposes server statistics; `a2dismod` needs `-f` for `autoindex` because Debian marks it as essential: ```bash sudo a2dismod -f autoindex status sudo apache2ctl configtest && sudo systemctl restart apache2 ``` List what remains loaded with `sudo apache2ctl -M` and disable anything else your sites don't need (for example `cgi` or `userdir` if you enabled them earlier). ### Nginx Nginx does not list directories unless `autoindex on;` is set. Check that no site enables it: ```bash grep -rn autoindex /etc/nginx/ ``` Nginx modules on Ubuntu are either compiled in or loaded from `/etc/nginx/modules-enabled/`. Remove the symlinks there for dynamic modules you don't use, such as `50-mod-http-image-filter.conf` or `50-mod-mail.conf` if they are present. Verify from the outside that a directory without an index file is not listed: ```bash sudo mkdir /var/www/html/listing-test curl -I https://your_domain/listing-test/ sudo rmdir /var/www/html/listing-test ``` ```output HTTP/1.1 403 Forbidden ``` ## Step 4 - Rejecting requests for unknown host names Bots scan IP addresses rather than domains, so requests with an unknown or missing `Host` header should never reach your application. ### Nginx Create a catch-all server that closes the connection without a response (status `444` is Nginx specific and means exactly that): ```bash sudo nano /etc/nginx/sites-available/000-default-deny ``` ```nginx server { listen 80 default_server; listen [::]:80 default_server; server_name _; return 444; } ``` Enable it and remove the stock default site, which also uses `default_server`: ```bash sudo rm /etc/nginx/sites-enabled/default sudo ln -s /etc/nginx/sites-available/000-default-deny /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` For HTTPS you can add `listen 443 ssl default_server;` with `ssl_reject_handshake on;` (Nginx 1.19.4 and later) to the same block, which rejects TLS handshakes for unknown names without needing a certificate. ### Apache Apache uses the first virtual host loaded as the default. Ubuntu's `000-default.conf` serves `/var/www/html` to anyone who connects by IP. Edit it so it answers with `403` instead: ```bash sudo nano /etc/apache2/sites-available/000-default.conf ``` ```apache ServerName default.invalid Require all denied ``` ```bash sudo apache2ctl configtest && sudo systemctl reload apache2 ``` Test by requesting the server by IP address. Replace `your_server_ip` with the public IP: ```bash curl -I http://your_server_ip/ ``` With Nginx, `curl` reports `Empty reply from server`; with Apache you get `403 Forbidden`. Requests using `your_domain` still work. ## Step 5 - Limiting request size and timeouts Small limits make it harder to exhaust memory or keep connections open with slow requests. ### Nginx Add the limits to the `http` block of `/etc/nginx/nginx.conf`, or to a single `server` block if only one site needs different values: ```nginx client_max_body_size 10m; client_body_timeout 15s; client_header_timeout 15s; send_timeout 15s; keepalive_timeout 30s; ``` `client_max_body_size` caps uploads (the default is 1 MB, so raise it only as far as your application needs). Requests over the limit get `413 Request Entity Too Large`. ### Apache Create a small configuration file: ```bash sudo nano /etc/apache2/conf-available/limits.conf ``` ```apache Timeout 30 LimitRequestBody 10485760 ``` `Timeout` defaults to 300 seconds on Ubuntu, which is generous for slow clients. `LimitRequestBody` is in bytes (10 MB here). Enable the file: ```bash sudo a2enconf limits sudo apache2ctl configtest && sudo systemctl reload apache2 ``` Apache also ships `mod_reqtimeout`, enabled by default, which already protects against slow header attacks such as Slowloris. ## Step 6 - Enforcing modern TLS Allow only TLS 1.2 and 1.3 and redirect all HTTP traffic to HTTPS. If you used Certbot, the redirect and a good cipher list are already in place (`/etc/letsencrypt/options-ssl-nginx.conf` or `options-ssl-apache.conf`); you only need to check the protocol list. ### Nginx The `ssl_protocols` directive in the `http` block of `/etc/nginx/nginx.conf` applies to every site: ```nginx ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ``` Then add HTTP Strict Transport Security to the HTTPS `server` block of your site, so browsers refuse to connect over plain HTTP for a year: ```nginx add_header Strict-Transport-Security "max-age=31536000" always; ``` ### Apache Edit `/etc/apache2/mods-available/ssl.conf` and set: ```apache SSLProtocol -all +TLSv1.2 +TLSv1.3 ``` Add HSTS inside the `` block of your site (requires `sudo a2enmod headers`): ```apache Header always set Strict-Transport-Security "max-age=31536000" ``` > Important: Only enable HSTS when every subdomain you care about works over HTTPS. Browsers cache the policy for the whole `max-age` and you cannot undo it from the server side. Reload the server and verify that TLS 1.2 and TLS 1.3 handshakes succeed: ```bash openssl s_client -connect your_domain:443 -servername your_domain -tls1_2 /dev/null | grep -E '^New,' openssl s_client -connect your_domain:443 -servername your_domain -tls1_3 /dev/null | grep -E '^New,' ``` ```output New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384 New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 ``` The OpenSSL client on Ubuntu 24.04 refuses TLS 1.0 and 1.1 on its own, so it cannot prove that the server rejects them. For that, and for a full report on ciphers and certificate chain, run your domain through the Qualys SSL Labs server test. ## Step 7 - Adding security headers These headers tell browsers to block MIME sniffing, refuse to render your pages inside frames on other sites and limit the referrer information sent to third parties. ### Nginx Add them to the `server` block of your site: ```nginx add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; ``` > Note: `add_header` directives are inherited from the enclosing block only if the current block has none of its own. If a `location` block contains any `add_header`, repeat the security headers there too. ### Apache Enable `mod_headers` and add the headers to `/etc/apache2/conf-available/security.conf`, which is enabled by default and applies to every site: ```bash sudo a2enmod headers sudo nano /etc/apache2/conf-available/security.conf ``` ```apache Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "SAMEORIGIN" Header always set Referrer-Policy "strict-origin-when-cross-origin" ``` A `Content-Security-Policy` header gives the strongest protection against cross-site scripting, but it has to be written for each application. Start with `Content-Security-Policy-Report-Only` to see what would break before enforcing it. Reload and check: ```bash curl -sI https://your_domain/ | grep -iE 'strict-transport|x-content|x-frame|referrer' ``` ```output strict-transport-security: max-age=31536000 x-content-type-options: nosniff x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin ``` ## Step 8 - Checking the process user and file permissions On Ubuntu both servers start as root only to bind ports 80 and 443, then run their worker processes as the unprivileged `www-data` user. Confirm it: ```bash ps -eo user,comm | grep -E 'nginx|apache2' | sort | uniq -c ``` ```output 1 root nginx 2 www-data nginx ``` The web server only needs to read your site files, not write them. Keep the files owned by your deploy user and readable by everyone, and give `www-data` write access only to the directories that truly need it, such as an upload folder: ```bash sudo chown -R your_user:your_user /var/www/your_domain sudo find /var/www/your_domain -type d -exec chmod 755 {} + sudo find /var/www/your_domain -type f -exec chmod 644 {} + sudo chown -R www-data:www-data /var/www/your_domain/uploads ``` With this layout, a vulnerability in your application cannot modify its own code. Never use `chmod 777` to fix a permission problem. ## Step 9 - Banning abusive clients with Fail2Ban Fail2Ban reads log files and temporarily bans IP addresses that fail authentication repeatedly. It ships ready-made filters for both servers. ```bash sudo apt install fail2ban ``` Create a local configuration file so package updates don't overwrite your changes: ```bash sudo nano /etc/fail2ban/jail.d/web.local ``` For Nginx: ```ini [nginx-http-auth] enabled = true backend = auto maxretry = 5 findtime = 10m bantime = 1h ``` For Apache, use the `apache-auth` jail with the same options instead: ```ini [apache-auth] enabled = true backend = auto maxretry = 5 findtime = 10m bantime = 1h ``` `backend = auto` makes the jail read the log files in `/var/log/nginx/` or `/var/log/apache2/`, which is where these filters look for failed Basic Auth attempts. Restart Fail2Ban and check the jail: ```bash sudo systemctl restart fail2ban sudo fail2ban-client status nginx-http-auth ``` ```output Status for the jail: nginx-http-auth |- Filter | |- Currently failed: 0 | |- Total failed: 0 | `- File list: /var/log/nginx/error.log `- Actions |- Currently banned: 0 |- Total banned: 0 `- Banned IP list: ``` To also slow down floods of requests, configure rate limiting in Nginx with `limit_req` (see the CubePath guide on rate limiting in Nginx) and enable the `nginx-limit-req` jail in the same way. ## Troubleshooting **The site returns `403 Forbidden` after changing permissions.** `www-data` must be able to traverse every parent directory. Check the path with `namei -l /var/www/your_domain/index.html`; each directory needs the `x` bit for others (`755`). **`nginx -t` reports `duplicate default server`.** Another enabled site still uses `default_server` on the same port. Search with `grep -rn default_server /etc/nginx/sites-enabled/` and remove the extra flag. **Fail2Ban jail does not start.** Run `sudo fail2ban-client -d` to print the parsed configuration, and `sudo journalctl -u fail2ban -n 50` to see the error. A jail whose log file does not exist yet will fail; make sure the web server has written to it at least once. **Security headers missing on some pages.** In Nginx this is almost always the `add_header` inheritance rule described in Step 7. In Apache, check that the application does not send the headers itself with a different value. ## Conclusion Your Apache or Nginx server now hides its version, refuses directory listings and unknown host names, limits request sizes, accepts only TLS 1.2 and 1.3, sends security headers and bans clients that brute force protected areas. Next, consider adding a web application firewall such as ModSecurity with the OWASP Core Rule Set, writing a `Content-Security-Policy` for your application, and centralising your web server logs so suspicious activity is easy to spot.