Lesson 2 — Server Security Hardening

Locking Down Your LAMP / LEMP Server

A no-skip, practical security hardening guide covering SSH, firewall rules, filesystem permissions, web server hardening, database security, and intrusion prevention — for servers built on the LAMP or LEMP stack.

LAMP · Apache LEMP · Nginx Ubuntu 24.04 LTS Security Hardening
Created: March 2026  ·  Written by Nicole M. Taylor  ·  Contact
📋
Prerequisites — Complete These First
This guide assumes you have a fully working LAMP or LEMP stack. If you haven't set one up yet, complete the appropriate guide first:
LAMP: Building a LAMP Server from Scratch (Lesson 1)
LEMP: Building a LEMP Server from Scratch (Lesson 1)
⚠️
Disclaimer — Software Changes Over Time This guide was written in March 2026 for Ubuntu 24.04 LTS. Package names, configuration paths, and command syntax may differ in future Ubuntu releases or on other distributions. Always verify against the official documentation for your installed versions. Written by Nicole M. Taylor.
01

Why Harden? The Threat Landscape

A freshly installed LAMP or LEMP server exposed to the internet will receive its first port-scan within minutes — not hours. Automated scanners crawl the entire IPv4 address space continuously, probing for weak SSH credentials, exposed admin panels, outdated PHP versions, and misconfigured web servers.

The Lesson 1 guides got your stack running. This lesson gets it secure. Skipping hardening is not a question of "if" something goes wrong, but "when."

What Attackers Are Actually Looking For

Attack Vector What They Want How We Stop It
Brute-forced SSH password Root shell access Key-only auth + Fail2Ban
Open ports (MySQL, Redis, etc.) Direct DB access, data theft UFW default-deny + bind to localhost
Directory listing / sensitive files Config files, passwords, source code Apache/Nginx config hardening
Outdated PHP / web software Remote code execution (RCE) Unattended-upgrades + PHP tuning
Weak database credentials Data exfiltration, ransom mysql_secure_installation
SQL injection / XSS via PHP apps Data theft, account takeover ModSecurity WAF + PHP hardening
World-writable web root files Defacement, malware drop Strict file permissions
ℹ️
Both Stacks Share Most Hardening Steps About 80% of this guide applies identically to both LAMP and LEMP servers — OS-level, SSH, firewall, PHP, and database hardening are the same. Sections specific to one stack are clearly tagged LAMP or LEMP. Steps that apply to both are tagged BOTH.

02

SSH Hardening — Your Most Critical Entry Point BOTH

SSH is the primary remote access method for your server. It is also the most-targeted service on the internet. A server with a simple password-based SSH login is routinely broken into within hours of going online. The steps below replace password auth with key-based auth and lock down the daemon itself.

🚨
Do Not Lock Yourself Out Complete every step of this section in order. Do not disable password authentication until you have confirmed your SSH key login works in a second, separate terminal window.

Generate an SSH Key Pair (on your client machine — NOT the server)

BASH · Your Local Machine
# On your desktop or laptop — Linux, macOS, or Windows (WSL/PowerShell)
ssh-keygen -t ed25519 -C "your_email@example.com"

# When prompted:
#   Save location: press Enter to accept default (~/.ssh/id_ed25519)
#   Passphrase:    ALWAYS set a strong passphrase — adds a second layer

# Copy your public key to the server
ssh-copy-id -i ~/.ssh/id_ed25519.pub yourusername@YOUR_SERVER_IP

# Now test key-based login in a NEW terminal window before continuing
ssh -i ~/.ssh/id_ed25519 yourusername@YOUR_SERVER_IP

Harden the SSH Daemon Configuration

With key login confirmed, open the SSH daemon config on the server:

BASH · Server
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo nano /etc/ssh/sshd_config

Find and set (or add) the following directives. Lines beginning with # must be uncommented:

📄/etc/ssh/sshd_config
# Change the default port — not foolproof but reduces noise from automated scanners
Port 2222

# Disable root login entirely — you should always sudo from a normal user
PermitRootLogin no

# Disable password authentication — key-only after this
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no

# Allow only your specific user(s)
AllowUsers yourusername

# Limit auth attempts per connection
MaxAuthTries 3

# Disconnect idle sessions after 5 minutes of inactivity
ClientAliveInterval 300
ClientAliveCountMax 2

# Disable legacy X11 forwarding if you don't use it
X11Forwarding no

# Disable TCP forwarding if you don't use SSH tunneling
AllowTcpForwarding no
⚠️
Changing the SSH Port — If you change the port from 22 to 2222, remember to update your UFW firewall rule (Section 3) to allow the new port before restarting SSH. Your next SSH connection must specify the port: ssh -p 2222 yourusername@SERVER_IP
BASH · Server — Apply Changes
# Verify the config has no syntax errors before restarting
sudo sshd -t

# If the above produces no output, restart SSH
sudo systemctl restart ssh

# Verify SSH is running on the new port
sudo ss -tlnp | grep ssh

Optional: Two-Factor Authentication via Google Authenticator

For an additional layer, you can require a time-based OTP (TOTP) alongside the SSH key:

BASH · Server
sudo apt install libpam-google-authenticator -y
google-authenticator   # Run as your normal user — follow the prompts
ℹ️
After running google-authenticator, scan the QR code with an authenticator app (Google Authenticator, Aegis, or Bitwarden Authenticator). Then edit /etc/pam.d/sshd to enable it and set AuthenticationMethods publickey,keyboard-interactive in sshd_config. Full PAM config is beyond this guide's scope but is well-documented in the Ubuntu SSH hardening docs.

03

Firewall with UFW — Default Deny Everything BOTH

UFW (Uncomplicated Firewall) is a front-end for iptables built into Ubuntu. The strategy is simple: deny all inbound traffic by default, then explicitly allow only what the server legitimately needs to serve.

Set Default Policies and Allow Only What's Needed

BASH · Server
# Set default: deny all incoming, allow all outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH — CHANGE 2222 to your actual SSH port
sudo ufw allow 2222/tcp comment 'SSH'

# Allow HTTP and HTTPS for your web server
sudo ufw allow 80/tcp  comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

# Enable the firewall
sudo ufw enable

# Verify rules are active
sudo ufw status verbose

Explicitly Block Database Ports from the Outside World

MySQL (port 3306) and MariaDB should never be reachable from the internet. Confirm these are blocked:

BASH · Server
# These should NOT appear in ufw status — if they do, delete them
sudo ufw status | grep 3306

# Confirm MySQL is bound to localhost only (see Section 9 for the my.cnf setting)
sudo ss -tlnp | grep 3306
# Should show: 127.0.0.1:3306 — NOT 0.0.0.0:3306

Rate-Limit SSH Connections via UFW

UFW has a built-in rate limiter for SSH that blocks IPs that connect more than 6 times in 30 seconds:

BASH · Server
# Replace 2222 with your SSH port
sudo ufw delete allow 2222/tcp
sudo ufw limit 2222/tcp comment 'SSH rate-limited'
ℹ️
The UFW rate limiter is lightweight. For a more sophisticated brute-force blocker, see Section 10 — Fail2Ban, which can also protect HTTP, MySQL, and other services.

04

Automatic Security Updates BOTH

The single most effective thing you can do to prevent exploitation is keep your software current. Ubuntu's unattended-upgrades package handles this automatically for security patches.

BASH · Server
# Install the package (usually pre-installed on Ubuntu 24.04)
sudo apt install unattended-upgrades apt-listchanges -y

# Enable automatic security updates
sudo dpkg-reconfigure -plow unattended-upgrades
# Choose "Yes" at the prompt

# Optional: Verify current configuration
cat /etc/apt/apt.conf.d/20auto-upgrades
📄/etc/apt/apt.conf.d/20auto-upgrades
// These two lines should be present after running dpkg-reconfigure:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
ℹ️
Auto-reboot for kernel updates — Kernel security patches require a reboot to take effect. Edit /etc/apt/apt.conf.d/50unattended-upgrades and uncomment Unattended-Upgrade::Automatic-Reboot "true"; to allow automatic reboots at a scheduled time. For production servers, use Automatic-Reboot-Time "03:00"; to keep reboots in your maintenance window.

05

Filesystem & Web Root Permissions BOTH

Incorrect file permissions are a top cause of web server compromises. The goal is to make files readable by the web server process but not writable — and to prevent the web server from executing anything it shouldn't.

Permission Principles

WhatOwnerPermissionsRationale
Web root directory root:www-data 755 Traversable by web server, not writable
PHP / HTML files root:www-data 644 Readable by web server, not writable
Upload directories www-data:www-data 755 Writable by web server for uploads only
Config files with credentials root:www-data 640 Readable by app, not world-readable
SSL private key files root:root 600 Never readable by web process
BASH · Server — Fix Web Root Permissions
# Replace /var/www/yourdomain.com with your actual web root path
# Set directory ownership: root owns, web server (www-data) is group
sudo chown -R root:www-data /var/www/yourdomain.com

# Directories: executable bit needed to traverse
sudo find /var/www/yourdomain.com -type d -exec chmod 755 {} \;

# Files: readable but not executable or writable
sudo find /var/www/yourdomain.com -type f -exec chmod 644 {} \;

# For CMS upload directories (e.g., WordPress wp-content/uploads)
sudo chown -R www-data:www-data /var/www/yourdomain.com/wp-content/uploads
sudo find /var/www/yourdomain.com/wp-content/uploads -type d -exec chmod 755 {} \;
sudo find /var/www/yourdomain.com/wp-content/uploads -type f -exec chmod 644 {} \;
🚨
Never Use 777 Permissionschmod 777 makes a file or directory writable by every user and process on the system, including the web server. If PHP is compromised, an attacker can write malware directly into your web root. If you see tutorials suggesting 777, ignore them.

06

Apache Hardening LAMP Only

Apache's default configuration is functional but exposes more information than necessary and enables several features that are better disabled on a production server.

Hide Server Version and OS Identity

📄/etc/apache2/conf-available/security.conf
# Hide Apache and OS version from HTTP response headers
ServerTokens Prod
ServerSignature Off

# Disable TRACE method (used in cross-site tracing attacks)
TraceEnable Off
BASH · Server
sudo nano /etc/apache2/conf-available/security.conf
# Set the three directives above, then apply:
sudo a2enconf security
sudo systemctl reload apache2

Disable Directory Listing

Directory listing lets anyone browsing your server see a full file listing of directories that lack an index file. This must be disabled.

📄/etc/apache2/apache2.conf (or your VirtualHost config)
<Directory /var/www/>
    # Remove Indexes to disable directory listing
    Options -Indexes +FollowSymLinks

    # Prevent access to .htaccess and .htpasswd files directly
    AllowOverride All

    # Deny access to hidden files (filenames starting with .)
    <FilesMatch "^\.ht">
        Require all denied
    </FilesMatch>
</Directory>

Enable Key Security Modules

BASH · Server
# Enable mod_headers for security header control
sudo a2enmod headers
sudo a2enmod rewrite   # Required for .htaccess rewrites (WordPress, etc.)
sudo a2enmod ssl

# Disable modules you don't need — reduces attack surface
sudo a2dismod autoindex   # Disables directory listing at the module level
sudo a2dismod status      # Disables /server-status endpoint

sudo systemctl reload apache2

Add Security Headers to All Virtual Hosts

Add the following to your VirtualHost <VirtualHost *:443> block (or a separate conf file):

📄/etc/apache2/sites-available/yourdomain.com-ssl.conf
<IfModule mod_headers.c>
    # Prevent clickjacking
    Header always set X-Frame-Options "SAMEORIGIN"

    # Block MIME-type sniffing
    Header always set X-Content-Type-Options "nosniff"

    # Enable XSS filter in older browsers
    Header always set X-XSS-Protection "1; mode=block"

    # Enforce HTTPS — only add after SSL is working
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

    # Control referrer information
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    # Remove the X-Powered-By header (also done in php.ini)
    Header unset X-Powered-By
    Header always unset X-Powered-By
</IfModule>
BASH · Server — Test and Reload
sudo apache2ctl configtest   # Should output: Syntax OK
sudo systemctl reload apache2

07

Nginx Hardening LEMP Only

Nginx is leaner than Apache out of the box but still benefits from explicit hardening of its server block configuration and global settings.

Hide Nginx Version

📄/etc/nginx/nginx.conf — inside the http {} block
# Hide version number from Server: response header
server_tokens off;

Harden Your Server Block

Add the following to your HTTPS server block:

📄/etc/nginx/sites-available/yourdomain.com
server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/yourdomain.com/public;
    index index.php index.html;

    # Disable directory listing
    autoindex off;

    # Security headers
    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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    more_clear_headers X-Powered-By;

    # Deny access to hidden files and sensitive paths
    location ~ /\. {
        deny all;
        return 404;
    }

    # Deny direct access to configuration files
    location ~* \.(env|git|htaccess|htpasswd|ini|log|sh|sql|conf)$ {
        deny all;
        return 404;
    }

    # Limit request methods to GET, HEAD, POST only
    if ($request_method !~ ^(GET|HEAD|POST)$) {
        return 444;
    }

    # Limit request body size (adjust for file upload needs)
    client_max_body_size 10m;

    # PHP-FPM block
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    }
}
BASH · Server — Test and Reload
sudo nginx -t          # Should output: configuration file ... syntax is ok
sudo systemctl reload nginx
ℹ️
more_clear_headers requires the nginx-extras package (sudo apt install nginx-extras). If you prefer not to install it, use fastcgi_hide_header X-Powered-By; inside your PHP location block instead.

08

PHP Hardening — Both Stacks BOTH

PHP's default configuration prioritizes convenience over security. These settings should be applied to php.ini on every production server regardless of whether you use Apache or Nginx.

BASH · Server — Locate Your php.ini
# For Apache (LAMP) — PHP runs as mod_php or php-fpm
sudo nano /etc/php/8.3/apache2/php.ini

# For Nginx (LEMP) — PHP runs as PHP-FPM
sudo nano /etc/php/8.3/fpm/php.ini

Key php.ini Security Settings

📄php.ini — Security Section
; Hide PHP version from HTTP headers and error messages
expose_php = Off

; Disable functions that are almost never legitimately needed in web apps
; and commonly abused for remote code execution
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,
    curl_exec,curl_multi_exec,parse_ini_file,show_source,eval

; Disable allow_url_fopen and allow_url_include
; Prevents PHP from loading remote files (used in RFI attacks)
allow_url_fopen = Off
allow_url_include = Off

; Restrict PHP file access to within the web root
; Replace /var/www with your web root parent directory
open_basedir = /var/www:/tmp:/usr/share/php

; Prevent PHP from running in upload directories
; (set to 0 to disable globally — override per-directory with .user.ini)
; upload_tmp_dir = /tmp

; Don't display errors to end users — log them instead
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php_errors.log

; Set a reasonable maximum upload and memory limit
upload_max_filesize = 20M
post_max_size = 25M
memory_limit = 128M

; Set a session save path with restricted permissions
session.save_path = "/var/lib/php/sessions"
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
BASH · Server — Restart PHP and Web Server
# LAMP — restart Apache
sudo systemctl restart apache2

# LEMP — restart PHP-FPM then Nginx
sudo systemctl restart php8.3-fpm
sudo systemctl reload nginx

# Verify expose_php is off — the Server header should no longer say "PHP/8.x"
curl -I https://yourdomain.com | grep -i "x-powered\|php"
# No output = success

09

MySQL / MariaDB Security Hardening BOTH

The database is where your real data lives. A compromised database can mean complete data loss, GDPR violations, or ransom demands. These steps address the most critical database attack vectors.

Run the Security Script (If You Haven't Already)

BASH · Server
# For MySQL:
sudo mysql_secure_installation

# For MariaDB (LEMP):
sudo mariadb-secure-installation

# Answer YES to all of the following when prompted:
#   - Set root password / validate password plugin
#   - Remove anonymous users
#   - Disallow root login remotely
#   - Remove test database
#   - Reload privilege tables

Bind the Database to Localhost Only

Your database should never be accessible from outside the server.

📄/etc/mysql/mysql.conf.d/mysqld.cnf (MySQL) /etc/mysql/mariadb.conf.d/50-server.cnf (MariaDB)
[mysqld]
# Bind to localhost only — refuse all external connections
bind-address = 127.0.0.1
BASH · Server
sudo systemctl restart mysql   # or: sudo systemctl restart mariadb
sudo ss -tlnp | grep 3306
# Must show 127.0.0.1:3306 — not 0.0.0.0:3306

Create Per-Application Database Users — Never Use Root

Your web application should use a dedicated user with only the privileges it needs. Never connect PHP to MySQL as root.

SQL · MySQL / MariaDB Shell
sudo mysql -u root -p

-- Create a database for your app
CREATE DATABASE myapp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Create a dedicated user with a strong password
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'Use_A_Very_Strong_Passw0rd!';

-- Grant ONLY what this app needs on ONLY its own database
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp_db.* TO 'myapp_user'@'localhost';

-- Apply changes
FLUSH PRIVILEGES;
EXIT;

Enable Binary Logging for Audit Trail

📄/etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
# Enable binary log — records all data changes for audit and recovery
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 7
max_binlog_size = 100M

10

Fail2Ban — Automated Intrusion Prevention BOTH

Fail2Ban monitors log files for repeated failed authentication attempts and automatically blocks the offending IP using firewall rules. It is one of the most effective tools against brute-force attacks.

BASH · Server — Install Fail2Ban
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Configure jail.local

📄/etc/fail2ban/jail.local — key sections
[DEFAULT]
# Ban time: 1 hour (3600 seconds). Increase for repeat offenders with bantime.increment
bantime  = 3600
findtime = 600
# Block after 5 failures within findtime
maxretry = 5

# Whitelist your own IP so you can't lock yourself out
ignoreip = 127.0.0.1/8 ::1 YOUR.HOME.IP.ADDRESS

# Use UFW as the ban action backend
banaction = ufw

# Enable SSH jail — change port to match your SSH port
[sshd]
enabled = true
port    = 2222
filter  = sshd
logpath = /var/log/auth.log
maxretry = 3

# Enable Apache/Nginx authentication failure protection
[apache-auth]
enabled  = true
port     = http,https
filter   = apache-auth
logpath  = /var/log/apache2/error.log
maxretry = 5

# LEMP: enable nginx-http-auth instead
[nginx-http-auth]
enabled  = true
port     = http,https
filter   = nginx-http-auth
logpath  = /var/log/nginx/error.log

# Block scanner bots probing for admin panels
[apache-botsearch]
enabled  = true
port     = http,https
filter   = apache-botsearch
logpath  = /var/log/apache2/error.log
maxretry = 2
BASH · Server — Start and Verify Fail2Ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Check status of all jails
sudo fail2ban-client status

# Check status of a specific jail
sudo fail2ban-client status sshd

# Manually unban an IP (if you lock yourself out)
sudo fail2ban-client set sshd unbanip YOUR.IP.ADDRESS

11

ModSecurity — Web Application Firewall (WAF) BOTH

ModSecurity is a WAF that sits in front of your web server and inspects every HTTP request, blocking common attacks like SQL injection, XSS, and remote file inclusion before they ever reach your PHP code.

⚠️
Detection Mode First — Install ModSecurity in detection-only mode before switching to enforcement. This lets you review what would be blocked and avoid false positives that break your application. Run in detection mode for a week before enabling enforcement.

Install ModSecurity

BASH · Server · LAMP (Apache)
# Install ModSecurity for Apache
sudo apt install libapache2-mod-security2 -y
sudo a2enmod security2
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
sudo nano /etc/modsecurity/modsecurity.conf
BASH · Server · LEMP (Nginx)
# Nginx requires ModSecurity compiled as a dynamic module
sudo apt install nginx-extras libmodsecurity3 -y

# Clone the OWASP Core Rule Set
sudo git clone https://github.com/coreruleset/coreruleset /etc/nginx/modsec/coreruleset
sudo cp /etc/nginx/modsec/coreruleset/crs-setup.conf.example \
        /etc/nginx/modsec/coreruleset/crs-setup.conf

Switch to Detection Mode, Then Load OWASP CRS

📄/etc/modsecurity/modsecurity.conf
# Start in DetectionOnly — change to On after testing
SecRuleEngine DetectionOnly

# Log all matched rules
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsec_audit.log
BASH · Server — Install OWASP Core Rule Set (CRS)
# Install the OWASP Core Rule Set (CRS) — the industry-standard WAF ruleset
sudo apt install modsecurity-crs -y

# For Apache: enable CRS
sudo a2enconf modsecurity-crs
sudo systemctl restart apache2

# Watch the audit log to review what would have been blocked
sudo tail -f /var/log/modsec_audit.log
When Ready to Enforce — After reviewing logs and confirming no legitimate traffic is blocked, change SecRuleEngine DetectionOnly to SecRuleEngine On and restart your web server. ModSecurity will now actively block malicious requests.

12

SSL / TLS Best Practices BOTH

Your Lesson 1 guide covered installing a Let's Encrypt certificate via Certbot. This section goes further, ensuring your TLS configuration is hardened against known protocol weaknesses.

Disable Old TLS Versions (TLS 1.0 and 1.1)

TLS 1.0 and 1.1 are deprecated and have known vulnerabilities. Only TLS 1.2 and 1.3 should be allowed.

📄Apache — your HTTPS VirtualHost block
# Allow only TLS 1.2 and TLS 1.3
SSLProtocol -all +TLSv1.2 +TLSv1.3

# Use only strong cipher suites
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305

# Prefer server cipher order
SSLHonorCipherOrder off   # TLS 1.3 handles this; let the client choose for 1.2

# Disable compression (CRIME attack)
SSLCompression off
📄Nginx — your server block
# Allow only TLS 1.2 and TLS 1.3
ssl_protocols TLSv1.2 TLSv1.3;

# Strong cipher suite (OWASP recommended)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

# Enable OCSP stapling for faster certificate validation
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

Test Your TLS Configuration

Use the free SSL Labs server test to grade your configuration from an external perspective:

🔗
Visit ssllabs.com/ssltest and enter your domain. Aim for an A+ rating. The most common reason for scoring below A+ is missing HSTS (the Strict-Transport-Security header) or an overly short HSTS max-age. Use at least max-age=31536000 (one year).

13

Log Monitoring & Auditing BOTH

Security hardening doesn't end after configuration. You need to watch what your server is doing. These are the most important logs and how to monitor them.

Key Log Locations

ServiceLog PathWhat to Watch For
SSH / Auth /var/log/auth.log Failed logins, sudo attempts
Apache access /var/log/apache2/access.log 404 floods, scanner patterns
Apache errors /var/log/apache2/error.log PHP errors, mod_security blocks
Nginx access /var/log/nginx/access.log 404 floods, scanner patterns
Nginx errors /var/log/nginx/error.log PHP-FPM errors, upstream failures
MySQL / MariaDB /var/log/mysql/error.log Failed auth, unusual queries
Fail2Ban /var/log/fail2ban.log Bans, unbans, jail activity
ModSecurity /var/log/modsec_audit.log Blocked requests, rule matches
System (kernel/daemons) /var/log/syslog Kernel errors, daemon crashes

Install Logwatch for Daily Reports

BASH · Server
sudo apt install logwatch -y

# Send daily email reports (requires a mail server or relay)
# Test immediately:
sudo logwatch --output stdout --format text --range today --detail med | less

Install Lynis — Security Audit Scanner

BASH · Server
sudo apt install lynis -y
sudo lynis audit system
# Lynis scores your system and produces a list of hardening suggestions.
# Run it after completing this guide to find anything you missed.

Watch for Rootkits with rkhunter

BASH · Server
sudo apt install rkhunter -y
sudo rkhunter --update
sudo rkhunter --check
# Add to cron for weekly checks:
# 0 3 * * 0 root rkhunter --check --skip-keypress --report-warnings-only

14

Final Security Checklist

Use this checklist to verify your server has been fully hardened. Every item should be confirmed before exposing the server to production traffic.

System & OS BOTH

Web Server BOTH

PHP BOTH

Database BOTH

You're Done — But Security Is Ongoing
Completing this checklist means your server is significantly hardened compared to a default installation. But hardening isn't a one-time event. Set a calendar reminder to: review Lynis monthly, check Fail2Ban ban logs weekly, rotate application passwords quarterly, and review SSL Labs scores after any web server or certificate change. Staying ahead of threats is a continuous practice.