Ravindra BagaleCourses & study guides मराठी Track your progress

8. Hosting a Static Website and Changing the Configuration

8.9 Basic Hardening of a Static Web Server

The site is live – now let's make it a little more secure. These changes are small, but scanners such as Nikto spot them immediately.

# Nginx – in the http { } block of nginx.conf
server_tokens off;                                  # hide version number
# in each server block
autoindex off;
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;
location ~ /\.(?!well-known) { deny all; }          # block .git, .env, .htaccess
# Apache – /etc/httpd/conf.d/security.conf (Ubuntu: conf-available/security.conf)
ServerTokens Prod
ServerSignature Off
TraceEnable Off
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
<DirectoryMatch "/\.(?!well-known)">
    Require all denied
</DirectoryMatch>
curl -I http://localhost          # check: no version in the Server header, new headers present

On Ubuntu Apache, enable the headers module first with sudo a2enmod headers.

Why this matters for security

Hiding versions does not fix vulnerabilities, but it removes free information. Blocking dot-files stops leaks of .git/ (entire source code) and .env (passwords) – both are among the first things web scanners request.

In simple words (sopya bhashet)

Friends, hardening is like putting locks on a house. A running site is like an open front door; now we close the windows – we hide the version number, switch off the folder file list (directory listing), and put a "No entry" board on hidden files such as .git and .env. This does not stop every attack, but it greatly reduces the free information an attacker gets. And after every change, run nginx -t or apachectl configtest – never skip this.

Step by step on Amazon Linux 2023 (Nginx)

We harden the mysite site from 8.2 (/etc/nginx/conf.d/mysite.conf, server_name = your public IP).

# 0. keep a copy of both files before you edit
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
sudo cp /etc/nginx/conf.d/mysite.conf /etc/nginx/conf.d/mysite.conf.bak

# 1. note the headers BEFORE (Host header = public IP, so our server block answers)
IP=$(curl -s https://checkip.amazonaws.com)
curl -I http://localhost -H "Host: $IP"

# 2. hide the version: add  server_tokens off;  inside the http { } block
sudo vi /etc/nginx/nginx.conf

# 3. in the server { } block of mysite.conf add: autoindex off; the three add_header lines;
#    and the location block that denies dot-files (see the Nginx block above)
sudo vi /etc/nginx/conf.d/mysite.conf

# 4. test the syntax, then reload only if the test passes
sudo nginx -t && sudo service nginx reload

# 5. check AFTER
curl -I http://localhost -H "Host: $IP"

For Apache on Amazon Linux the flow is the same: sudo yum install -y httpd (if not installed), put the Apache lines above in /etc/httpd/conf.d/security.conf, then sudo apachectl configtest && sudo service httpd reload.

Worked example 1 – Mauli Dairy, Pune (fictional): before and after headers

A fictional Pune dairy hosts its price-list page on an EC2 instance with Nginx. Step 1 and step 5 above give:

Before hardening After hardening
Status line HTTP/1.1 200 OK HTTP/1.1 200 OK
Server nginx/<version number> nginx (no version)
X-Content-Type-Options – nosniff
X-Frame-Options – SAMEORIGIN
Referrer-Policy – strict-​origin-​when-​cross-​origin

The page itself did not change for customers. What changed: a scanner no longer learns the exact Nginx version, the browser will not guess file types (nosniff), and other websites cannot load the dairy's page inside a hidden frame (SAMEORIGIN), which blocks click-jacking tricks.

Worked example 2 – Nashik Valley Grapes (fictional exporter): the forgotten .git and .env

The developer of a fictional Nashik grape exporter's website ran git clone straight into /var/www/mysite, and a .env file with a test password is also sitting there. Test from your laptop:

curl -I http://<PUBLIC_IP>/.git/config     # before: 200 OK  (anyone can download it)
curl -I http://<PUBLIC_IP>/.env            # before: 200 OK
# ... add the dot-file location block, nginx -t, reload ...
curl -I http://<PUBLIC_IP>/.git/config     # after: 403 Forbidden
curl -I http://<PUBLIC_IP>/.env            # after: 403 Forbidden
curl -I http://<PUBLIC_IP>/.well-known/acme-challenge/test.txt   # still allowed (Certbot needs it, Chapter 13)

How the rule works: location ~ /\.(?!well-known) is a regular-expression location. It matches any path that contains /. followed by anything except well-known, and deny all; answers 403. The real fix is still to keep only public files in the web root – but the rule is a safety net for the day someone forgets.

Common mistake – an add_header inside a location block silently drops the others

Nginx passes add_header lines from the server block down to a location block only if that location has no add_header of its own. Suppose a student adds a cache header for images:

location ~* \.(jpg|png|css)$ {
    add_header Cache-Control "public, max-age=86400";   # now ONLY this header is sent for images and CSS
}

curl -I http://<PUBLIC_IP>/style.css now shows Cache-Control but no X-Content-Type-Options or X-Frame-Options. Fix: repeat the security headers inside that location (or keep them in a small file and include it in every location that has its own add_header), then sudo nginx -t && sudo service nginx reload and check with curl -I again.

Ravindra Bagale's Tip

Many students git clone the whole project, including the .git folder, into the web root – and anyone can download /.git/config. Keep only public files in the web root, or block dot-files. I tell every batch this – remember it.

Practice task

Before and after hardening, run curl -I http://<PUBLIC_IP> and compare headers. Create /var/www/mysite/.env with dummy text and confirm it returns 403.

Thodkyaat sangaycha tar

  • Upload to home, sudo cp to /var/www/<site>, folders 755, files 644.
  • Amazon Linux: site files in conf.d/; Ubuntu: sites-available + enable (symlink or a2ensite), disable the default site.
  • Document root: Nginx root; Apache DocumentRoot + matching <Directory>. SELinux label for paths outside /var/www.
  • New port: listen / Listen, security group rule, SELinux port label on CentOS.
  • Index page: index / DirectoryIndex; keep listing off.
  • Harden: hide versions, security headers, block dot-files. Test before reload, every time.

Got it? Your website is live – congratulations! Part 3 is complete. Now let's move on to dynamic websites – PHP, LAMP and LEMP.