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/.
| File | Purpose | Used 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 obtained —
sudo certbot certificates shows domain name, expiry date, and cert path.
- HTTPS loads correctly — browser shows padlock, no mixed-content warnings.
- HTTP redirects to HTTPS —
curl -I http://<domain> returns 301 pointing at https://.
- Auto-renewal active —
sudo systemctl status certbot.timer shows active (waiting).
- Dry run passes —
sudo certbot renew --dry-run completes without errors.
- mod_ssl enabled (Apache) —
apache2ctl -M | grep ssl shows ssl_module.
- Private key permissions —
ls -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.