Add Some Security

Chapter 5 — SSL / HTTPS

Running a website over plain HTTP means every request and response — including login credentials — travels as readable text. SSL/TLS encrypts the connection, gives visitors the padlock icon in the browser, and is required for any site that handles passwords or personal data. This chapter covers two scenarios: obtaining a free, trusted certificate from Let's Encrypt for a publicly accessible domain, and generating a self-signed certificate for local development where no external CA is needed.

Let's Encrypt + Certbot
Free, publicly trusted certificate. Browser shows a green padlock. Requires a real domain name pointed at your server and port 80 open to the internet. Auto-renews every 90 days. Use this for any public-facing site.
Self-Signed Certificate
Generated locally in seconds. Browser shows a warning ("not trusted") because no CA has verified it. No domain or internet required. Use this for local development only — never for a site visitors will use.

Option A — Let's Encrypt with Certbot

Prerequisites: your domain (e.g. osztromok.com) must have its DNS A record pointing at this server's public IP address, and port 80 must be reachable from the internet. Certbot uses an HTTP challenge to verify you control the domain before issuing the certificate. If your server is behind Cloudflare Tunnel, see the note below.
◆ Let's Encrypt · Apache

Install Certbot

philip@debian — install Certbot for Apache
philip@debian:~$ sudo apt install -y certbot python3-certbot-apache Setting up certbot (2.1.0-1) ... Setting up python3-certbot-apache (2.1.0-1) ...

Obtain and install the certificate

philip@debian — run Certbot
philip@debian:~$ sudo certbot --apache -d osztromok.com -d www.osztromok.com Saving debug log to /var/log/letsencrypt/letsencrypt.log Enter email address (used for urgent renewal and security notices): emubantam@gmail.com Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA... (A)gree/(C)ancel: A Would you be willing to share your email address with EFF? (Y)es/(N)o: N Account registered. Requesting a certificate for osztromok.com and www.osztromok.com Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/osztromok.com/fullchain.pem Key is saved at: /etc/letsencrypt/live/osztromok.com/privkey.pem Please choose whether or not to redirect HTTP traffic to HTTPS: 1: No redirect 2: Redirect - Make all requests redirect to secure HTTPS access 2 Redirecting all traffic on port 80 to ssl in /etc/apache2/sites-enabled/osztromok.com.conf Congratulations! You have successfully enabled HTTPS on https://osztromok.com

Certbot edits your Apache virtual host automatically — adding the SSL directives and an HTTP → HTTPS redirect. It also enables mod_ssl and mod_rewrite if they weren't already active. You don't need to touch the virtual host file manually.

◆ Let's Encrypt · Nginx

Install Certbot for Nginx

philip@debian — install Certbot for Nginx
philip@debian:~$ sudo apt install -y certbot python3-certbot-nginx Setting up python3-certbot-nginx (2.1.0-1) ... philip@debian:~$ sudo certbot --nginx -d osztromok.com -d www.osztromok.com Requesting a certificate for osztromok.com and www.osztromok.com Successfully received certificate. Deploying certificate to VirtualHost /etc/nginx/sites-enabled/osztromok.com Please choose whether or not to redirect HTTP traffic to HTTPS: 2 Congratulations! HTTPS enabled at https://osztromok.com

What Certbot Creates

After a successful run, Let's Encrypt certificates are stored in /etc/letsencrypt/live/<domain>/. These are symlinks pointing to the actual versioned files in /etc/letsencrypt/archive/.

FilePurposeUsed in config as
fullchain.pem Your certificate + the Let's Encrypt intermediate chain SSLCertificateFile / ssl_certificate
privkey.pem Your private key — keep this secret SSLCertificateKeyFile / ssl_certificate_key
cert.pem Your certificate only (without chain) — rarely used directly
chain.pem The intermediate chain only — rarely used directly

Manual HTTP → HTTPS Redirect

If you chose not to let Certbot add the redirect, or if you want to review exactly what it looks like, here are the configurations for both web servers:

Apache — manual redirect

# /etc/apache2/sites-available/osztromok.com.conf # HTTP — redirect everything to HTTPS <VirtualHost *:80> ServerName osztromok.com ServerAlias www.osztromok.com Redirect permanent / https://osztromok.com/ </VirtualHost> # HTTPS — the real site <VirtualHost *:443> ServerName osztromok.com ServerAlias www.osztromok.com DocumentRoot /var/www/osztromok.com/public_html SSLEngine on SSLCertificateFile /etc/letsencrypt/live/osztromok.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/osztromok.com/privkey.pem <Directory /var/www/osztromok.com/public_html> AllowOverride All Require all granted </Directory> </VirtualHost>

Nginx — manual redirect

# /etc/nginx/sites-available/osztromok.com # HTTP — redirect to HTTPS server { listen 80; server_name osztromok.com www.osztromok.com; return 301 https://$host$request_uri; } # HTTPS — the real site server { listen 443 ssl; server_name osztromok.com www.osztromok.com; root /var/www/osztromok.com/public_html; index index.php index.html; ssl_certificate /etc/letsencrypt/live/osztromok.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/osztromok.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.2-fpm.sock; } }

Auto-Renewal

Let's Encrypt certificates expire after 90 days. Certbot installs a systemd timer that checks for renewal twice a day automatically — you don't need to do anything after the initial setup. You can verify the timer is running and test the renewal process with a dry run:

philip@debian — verify auto-renewal
# Check the renewal timer is active philip@debian:~$ sudo systemctl status certbot.timer ● certbot.timer - Run certbot twice daily Loaded: loaded (/lib/systemd/system/certbot.timer; enabled) Active: active (waiting) Trigger: Mon 2026-06-18 00:00:00 UTC; 11h left # Dry run — confirms renewal will work without actually renewing philip@debian:~$ sudo certbot renew --dry-run Simulating renewal of an existing certificate for osztromok.com Congratulations, all simulated renewals succeeded. # Check expiry of current certificate philip@debian:~$ sudo certbot certificates Found the following certs: Certificate Name: osztromok.com Domains: osztromok.com www.osztromok.com Expiry Date: 2026-09-15 (VALID: 89 days) Certificate Path: /etc/letsencrypt/live/osztromok.com/fullchain.pem

Option B — Self-Signed Certificate (Local Development)

When developing locally — no domain, no internet — a self-signed certificate lets you test HTTPS behaviour without Let's Encrypt. The browser will show a warning that the certificate is not trusted (because it isn't signed by a recognised CA), but you can click through and the connection is still encrypted.

◆ Self-Signed · OpenSSL

Generate the certificate

philip@debian — generate self-signed cert
# Create a directory for local certs philip@debian:~$ sudo mkdir -p /etc/ssl/local # Generate a 2048-bit private key and self-signed certificate (valid 365 days) philip@debian:~$ sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/ssl/local/selfsigned.key \ -out /etc/ssl/local/selfsigned.crt \ -subj "/C=GB/ST=England/L=Local/O=Dev/CN=localhost" Generating a RSA private key .........+++++ writing new private key to '/etc/ssl/local/selfsigned.key' # Restrict key permissions philip@debian:~$ sudo chmod 600 /etc/ssl/local/selfsigned.key

Configure Apache to use it

# /etc/apache2/sites-available/local-ssl.conf <VirtualHost *:443> ServerName localhost DocumentRoot /var/www/html SSLEngine on SSLCertificateFile /etc/ssl/local/selfsigned.crt SSLCertificateKeyFile /etc/ssl/local/selfsigned.key <Directory /var/www/html> AllowOverride All Require all granted </Directory> </VirtualHost>
philip@debian — enable SSL site
philip@debian:~$ sudo a2enmod ssl philip@debian:~$ sudo a2ensite local-ssl philip@debian:~$ sudo apache2ctl configtest Syntax OK philip@debian:~$ sudo systemctl reload apache2

Configure Nginx to use it

# /etc/nginx/sites-available/local-ssl server { listen 443 ssl; server_name localhost; root /var/www/html; index index.php index.html; ssl_certificate /etc/ssl/local/selfsigned.crt; ssl_certificate_key /etc/ssl/local/selfsigned.key; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.2-fpm.sock; } }
philip@debian — enable Nginx SSL site
philip@debian:~$ sudo ln -s /etc/nginx/sites-available/local-ssl /etc/nginx/sites-enabled/ philip@debian:~$ sudo nginx -t nginx: configuration file syntax is ok philip@debian:~$ sudo systemctl reload nginx

Cloudflare Tunnel — A Note

Philip's site uses a Cloudflare Tunnel to route traffic from the public internet to the home server. In this setup the SSL picture is slightly different:

  • Browser → Cloudflare: always HTTPS — Cloudflare handles this with their own certificate. Visitors get the padlock regardless of what your server does.
  • Cloudflare → your server (tunnel): traffic travels through the encrypted tunnel — you can run plain HTTP on the server itself if you wish, or add a self-signed cert for the server-side leg.
  • Let's Encrypt on a tunnelled server: the HTTP-01 challenge Certbot uses requires port 80 to be directly reachable from the internet, which a Cloudflare Tunnel doesn't provide. You would need to use the DNS-01 challenge instead (certbot certonly --manual --preferred-challenges dns), which requires adding a TXT record in Cloudflare DNS.
Simplest approach with Cloudflare Tunnel: set Cloudflare SSL/TLS mode to Full in the Cloudflare dashboard, generate a self-signed cert for your server (Option B above), and point the tunnel at https://localhost. Visitors get trusted HTTPS; your server gets an encrypted tunnel leg with a self-signed cert that Cloudflare accepts in Full mode.

Verify HTTPS is Working

philip@debian — verify certificate
# Check certificate details from the command line philip@debian:~$ echo | openssl s_client -connect osztromok.com:443 2>/dev/null \ | openssl x509 -noout -dates -subject notBefore=Jun 17 00:00:00 2026 GMT notAfter=Sep 15 00:00:00 2026 GMT subject=CN = osztromok.com # Check that HTTP redirects to HTTPS philip@debian:~$ curl -I http://osztromok.com HTTP/1.1 301 Moved Permanently Location: https://osztromok.com/

SSL Checklist

  • Certificate obtainedsudo certbot certificates shows domain name, expiry date, and cert path.
  • HTTPS loads correctly — browser shows padlock, no mixed-content warnings.
  • HTTP redirects to HTTPScurl -I http://<domain> returns 301 pointing at https://.
  • Auto-renewal activesudo systemctl status certbot.timer shows active (waiting).
  • Dry run passessudo certbot renew --dry-run completes without errors.
  • mod_ssl enabled (Apache)apache2ctl -M | grep ssl shows ssl_module.
  • Private key permissionsls -la /etc/letsencrypt/live/<domain>/ shows key readable only by root.
Next — Chapter 6: Firewall & Security Basics. With HTTPS in place, Chapter 6 hardens the rest of the server: UFW firewall rules, fail2ban brute-force protection, correct file system permissions on the web root, and stripping version information from HTTP response headers.