HTTPS with Let's Encrypt

Chapter 1 — HTTPS with Let's Encrypt

HTTPS encrypts the connection between a visitor and your server. Without it, anyone on the same network can read usernames, passwords, cookies, and session tokens in plain text. Browsers mark plain HTTP sites as "Not Secure." Search engines rank HTTPS sites higher. Let's Encrypt provides free, trusted certificates — and with the DNS-01 challenge, you can get one even when port 80 is blocked by your ISP.

What this chapter covers: What TLS actually does and why it matters beyond the padlock icon. How certificate authorities and the trust chain work. Let's Encrypt overview and the ACME protocol. Why HTTP-01 challenge fails behind Virgin Media and how DNS-01 solves it. Getting a certificate using certbot with the Cloudflare DNS plugin. Configuring Apache for HTTPS. Automatic renewal via systemd timer. Cloudflare Origin Certificates as an alternative. Choosing the right Cloudflare SSL/TLS mode. Testing with openssl and SSL Labs.

Why HTTPS Matters

The padlock icon is the visible part. What's actually happening underneath is more important:

  • Encryption: all data exchanged between browser and server is encrypted. On plain HTTP, a coffee shop Wi-Fi operator, your ISP, or anyone running a packet sniffer can read login credentials and session cookies verbatim.
  • Authentication: TLS proves the server is who it claims to be. Without it, a man-in-the-middle attack can serve a fake version of your site to users — they'd have no way to tell.
  • Integrity: TLS detects tampering. ISPs are known to inject ads into plain HTTP pages. TLS prevents this.
  • HTTP/2 and HTTP/3: both require HTTPS. Without it you're stuck on HTTP/1.1 — significantly slower for sites with many assets.
  • Browser warnings: Chrome and Firefox show "Not Secure" in the address bar for HTTP pages with forms. Visitors see this and leave.
  • Cloudflare Tunnel already gives you HTTPS for visitors — but understanding TLS and having your own certificate enables Full (Strict) SSL mode and works outside the tunnel for SSH, direct connections, and local network access.
You already have HTTPS for visitors via Cloudflare Tunnel. Cloudflare handles the TLS for the visitor-to-Cloudflare leg. This chapter teaches you how to extend that to the full end-to-end path — and gives you a real certificate for when you need one (local network, Full Strict SSL mode, future direct connections).

How TLS Works (the short version)

TLS in 4 steps: 1. Client says hello, lists supported cipher suites 2. Server sends its certificate (signed by a CA the browser trusts) 3. Browser verifies: cert matches the domain, signed by trusted CA, not expired 4. Both sides derive a shared symmetric key — all further traffic is encrypted with it Certificate trust chain: Let's Encrypt Root CA (pre-installed in your browser/OS) └── Let's Encrypt Intermediate CA (signed by root) └── Your cert for osztromok.com (signed by intermediate) Browser checks: is the chain valid? Is the leaf cert for the domain I requested? Is it within the validity period? If all yes → padlock shown, connection encrypted.

The key concept is the trust chain. Your certificate is trusted because it's signed by Let's Encrypt's intermediate CA, which is trusted because it's signed by a root CA, which is pre-installed in operating systems and browsers. Self-signed certificates break this chain — the browser has no pre-installed trust for them and shows a scary warning instead of a padlock.

Let's Encrypt certificates are Domain Validation (DV) — they prove you control the domain, nothing more. That's sufficient for encryption. Extended Validation (EV) certs that show the company name in the address bar are largely obsolete in modern browsers.

Let's Encrypt and the ACME Protocol

Let's Encrypt is a non-profit Certificate Authority that issues free DV certificates. The process is automated via the ACME protocol — you run certbot, it talks to Let's Encrypt's servers, proves you control the domain, and receives a signed certificate. No forms, no waiting, no credit card.

  • Certificate validity: 90 days. Short by design — forces automation and limits damage if a cert is compromised. Auto-renewal handles this transparently.
  • Rate limits: 5 certificates per domain per week. For a personal site this is never a problem.
  • Wildcard certificates: supported — *.osztromok.com covers all subdomains. Requires DNS-01 challenge.
  • Cost: free, forever. Funded by Mozilla, Google, EFF, and others.

The Challenge Problem — Why HTTP-01 Doesn't Work Here

To prove you control a domain, Let's Encrypt uses a "challenge." You have two main options — and one of them is blocked by Virgin Media.

HTTP-01 Challenge — NOT available
Let's Encrypt places a file at http://osztromok.com/.well-known/acme-challenge/TOKEN and then fetches it from outside to verify it's really there.

Why it fails here:
  • Requires port 80 to be reachable from the internet
  • Virgin Media Hub 3.0 blocks all inbound connections on port 80
  • Cloudflare Tunnel could theoretically help, but certbot and tunnel timing is unreliable
  • Bottom line: if you can't receive an inbound HTTP request, HTTP-01 is unavailable
DNS-01 Challenge — This is what we use
Let's Encrypt gives you a token and tells you to place it as a TXT record in your DNS: _acme-challenge.osztromok.com TXT "TOKEN". It then queries DNS to verify the token.

Why it works here:
  • No inbound HTTP required — purely DNS-based
  • Works regardless of ISP port blocking
  • DNS is on Cloudflare → certbot-dns-cloudflare plugin creates/deletes the TXT record automatically
  • Supports wildcard certificates (HTTP-01 cannot)
  • Entire process is automated once configured

Installing certbot and the Cloudflare Plugin

# Install certbot and the Cloudflare DNS plugin $ sudo apt update $ sudo apt install certbot python3-certbot-dns-cloudflare -y # Verify both are installed $ certbot --version certbot 2.x.x $ python3 -c "import certbot_dns_cloudflare; print('plugin OK')" plugin OK

Create a Cloudflare API token for DNS editing

In the Cloudflare dashboard → My Profile → API Tokens → Create Token. Use the Edit zone DNS template:

  • Permissions: Zone → DNS → Edit
  • Zone Resources: Include → Specific zone → osztromok.com
  • Copy the token — shown only once.
# Create a secure credentials file for certbot to read $ sudo mkdir -p /etc/letsencrypt $ sudo nano /etc/letsencrypt/cloudflare.ini
# /etc/letsencrypt/cloudflare.ini # Cloudflare API credentials for certbot DNS-01 challenge dns_cloudflare_api_token = your-cloudflare-api-token-here
# Lock down the file — it contains a credential $ sudo chmod 600 /etc/letsencrypt/cloudflare.ini $ sudo chown root:root /etc/letsencrypt/cloudflare.ini

Getting the Certificate — Complete Walkthrough

Full Walkthrough · certbot DNS-01 via Cloudflare
Obtain a Let's Encrypt certificate for osztromok.com and www.osztromok.com using DNS-01 challenge — no open ports required.
1
Request the certificate. The --dns-cloudflare-propagation-seconds 60 flag gives DNS time to propagate the TXT record before Let's Encrypt checks it.
$ sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ --dns-cloudflare-propagation-seconds 60 \ -d osztromok.com \ -d www.osztromok.com \ --email emubantam@gmail.com \ --agree-tos \ --no-eff-email Saving debug log to /var/log/letsencrypt/letsencrypt.log Requesting a certificate for osztromok.com and www.osztromok.com Waiting 60 seconds for DNS changes to propagate 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 This certificate expires on 2026-09-14. These files will be updated when the certificate renews.
2
Check what certbot created.
$ sudo ls -la /etc/letsencrypt/live/osztromok.com/ cert.pem ← your certificate (leaf only) chain.pem ← intermediate CA certificate fullchain.pem ← cert + chain combined (use this in Apache) privkey.pem ← your private key (keep secret, never share) $ sudo certbot certificates Found the following certs: Certificate Name: osztromok.com Domains: osztromok.com www.osztromok.com Expiry Date: 2026-09-14 (VALID: 89 days) Certificate Path: /etc/letsencrypt/live/osztromok.com/fullchain.pem Private Key Path: /etc/letsencrypt/live/osztromok.com/privkey.pem
3
Enable mod_ssl in Apache.
$ sudo a2enmod ssl $ sudo a2enmod headers # needed for HSTS headers in Chapter 2 $ sudo apache2ctl configtest && sudo systemctl restart apache2 # restart (not reload) after enabling new modules
4
Add an HTTPS VirtualHost to the Apache config. Open /etc/apache2/sites-available/osztromok.com.conf and add the *:443 block below the existing *:80 block:
<VirtualHost *:443> ServerName osztromok.com ServerAlias www.osztromok.com DocumentRoot /var/www/osztromok.com/public_html <Directory /var/www/osztromok.com/public_html> Options FollowSymLinks AllowOverride All Require all granted </Directory> SSLEngine on SSLCertificateFile /etc/letsencrypt/live/osztromok.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/osztromok.com/privkey.pem ErrorLog ${APACHE_LOG_DIR}/osztromok_error.log CustomLog ${APACHE_LOG_DIR}/osztromok_access.log combined </VirtualHost>
5
Validate and reload.
$ sudo apache2ctl configtest Syntax OK $ sudo systemctl reload apache2
6
Test locally with openssl.
$ echo | openssl s_client -connect localhost:443 -servername osztromok.com 2>/dev/null | grep -E "subject|issuer|Verify" subject=CN=osztromok.com issuer=C=US, O=Let's Encrypt, CN=R10 Verify return code: 0 (ok) # Verify return code 0 = valid cert, trusted chain
If using the Cloudflare Tunnel, also switch Cloudflare SSL/TLS mode to Full (Strict) now that Apache has a valid cert (see SSL Modes section below).
The certificate files in /etc/letsencrypt/live/ are symlinks to the latest version in /etc/letsencrypt/archive/. When certbot renews, it updates the archive and the symlinks automatically. Apache reads the symlinks, so it picks up the renewed cert on the next reload — no config changes needed.

Wildcard Certificates

A wildcard certificate (*.osztromok.com) covers all first-level subdomains with a single cert. Every new subdomain you add is automatically covered — no re-issuing needed. DNS-01 is the only challenge type that supports wildcards.

# Request a wildcard cert (covers *.osztromok.com AND osztromok.com as a SAN) $ sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ --dns-cloudflare-propagation-seconds 60 \ -d osztromok.com \ -d "*.osztromok.com" \ --email emubantam@gmail.com \ --agree-tos \ --no-eff-email # Note: *.osztromok.com does NOT cover the apex (osztromok.com) by itself. # Always include -d osztromok.com alongside -d "*.osztromok.com"
Wildcards only cover one level. *.osztromok.com covers blog.osztromok.com but not www.blog.osztromok.com. For deeper subdomains, request them explicitly. In practice, for a personal site, a wildcard cert for *.osztromok.com covers every subdomain you'll ever create.

Automatic Renewal

Let's Encrypt certificates expire after 90 days. Certbot installs a systemd timer automatically when it's installed on Debian/Ubuntu — you just need to verify it's working.

# Check that the certbot renewal timer is active $ sudo systemctl status certbot.timer ● certbot.timer - Run certbot twice daily Active: active (waiting) Trigger: Mon 2026-06-15 12:00:00 BST; 14h left # Test that renewal would work (--dry-run doesn't actually issue a cert) $ sudo certbot renew --dry-run Simulating renewal of an existing certificate for osztromok.com Congratulations, all simulated renewals succeeded: /etc/letsencrypt/live/osztromok.com/fullchain.pem (success) # View renewal configuration certbot created $ sudo cat /etc/letsencrypt/renewal/osztromok.com.conf [renewalparams] authenticator = dns-cloudflare dns_cloudflare_credentials = /etc/letsencrypt/cloudflare.ini dns_cloudflare_propagation_seconds = 60

Reload Apache after renewal

Certbot renews the cert files but doesn't reload Apache automatically. Apache continues serving the old cert from memory until it's reloaded. Add a deploy hook to reload Apache whenever a cert is renewed:

$ sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh
#!/bin/bash # /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh # Runs automatically after every successful cert renewal systemctl reload apache2
$ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh # Test it runs without error $ sudo /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh # If no output and exit code 0, it's working
Certbot renews certs when they're within 30 days of expiry. With a 90-day cert and checks twice daily, you'll get renewal attempts starting at day 60. If both DNS and Apache are working, renewal is completely hands-off. You can monitor with sudo certbot certificates anytime to check the current expiry date.

Cloudflare Origin Certificates — The Alternative

Cloudflare offers their own free certificates called Origin Certificates. Unlike Let's Encrypt, these are issued by Cloudflare's own CA — they're only trusted when traffic goes through Cloudflare's proxy. Browsers connecting directly to your server's IP would see an "untrusted" warning.

FeatureLet's EncryptCloudflare Origin Cert
Validity period90 days (auto-renewed)Up to 15 years (set once, forget it)
Trusted by browsersYes — universallyOnly via Cloudflare proxy
Works for direct connectionsYesNo — not browser-trusted
Works for local network HTTPSYesNo
Requires renewal automationYes (every 90 days)No (15-year validity)
Enables Full (Strict) SSL modeYesYes
Works with Cloudflare TunnelYesYes

Creating a Cloudflare Origin Certificate

Go to Cloudflare dashboard → SSL/TLS → Origin Server → Create Certificate. Choose RSA key, validity 15 years, and add the hostnames (osztromok.com, *.osztromok.com). Cloudflare shows you the cert and key — copy them to your server.

# Save the cert and key Cloudflare gives you to your server $ sudo nano /etc/ssl/cloudflare/osztromok.com.pem # paste the certificate here $ sudo nano /etc/ssl/cloudflare/osztromok.com.key # paste the private key here $ sudo chmod 600 /etc/ssl/cloudflare/osztromok.com.key # Then point Apache SSLCertificateFile and SSLCertificateKeyFile at these paths
Recommendation: use Let's Encrypt if you want flexibility (local network HTTPS, future direct connections, full browser trust regardless of Cloudflare). Use Cloudflare Origin Cert if you're 100% committed to routing everything through Cloudflare and want to avoid renewal automation entirely. For a learning setup, Let's Encrypt is the better choice.

Cloudflare SSL/TLS Modes — Which to Choose

Go to Cloudflare dashboard → SSL/TLS → Overview. This setting controls how Cloudflare communicates with your origin server (the leg behind Cloudflare, not the visitor-to-Cloudflare leg which is always HTTPS).

Off
HTTP only. Visitors see no padlock. Never use this.
Flexible
Visitor → Cloudflare: HTTPS (padlock shown). Cloudflare → your server: plain HTTP. No certificate required on your server. This is the default for tunnel setups. The tunnel connection itself is encrypted regardless, so this is acceptable — but it's not end-to-end encryption. Mixed content issues can arise.
Full
Both legs use HTTPS. Your server needs a certificate — even a self-signed one. Cloudflare doesn't verify the cert is valid or matches the domain. Better than Flexible but still doesn't guarantee authenticity of the cert.
Full (Strict)
Recommended once you have a cert. Both legs use HTTPS and Cloudflare verifies your certificate is valid and issued by a trusted CA (or Cloudflare Origin CA). End-to-end encryption with authenticity. Requires a real Let's Encrypt or Cloudflare Origin cert on your Apache server.
Flexible (before this chapter): Visitor ──HTTPS──▶ Cloudflare ──HTTP──▶ cloudflared ──HTTP──▶ Apache (encrypted) (proxied) (plain) (tunnel, but (no cert) internally plain) Full (Strict) (after this chapter): Visitor ──HTTPS──▶ Cloudflare ──HTTPS──▶ cloudflared ──HTTPS──▶ Apache (encrypted) (proxied) (encrypted) (tunnel, TLS (with LE cert) end-to-end)
Don't switch to Full (Strict) before the cert is installed. If you enable Full (Strict) and Apache doesn't have a valid certificate, Cloudflare will refuse to connect and visitors will see a 526 error. Always install and verify the cert first (using the openssl test command above), then change the Cloudflare SSL mode.

Testing Your Certificate

# Test the certificate from the server itself $ echo | openssl s_client -connect localhost:443 -servername osztromok.com 2>/dev/null \ | openssl x509 -noout -dates -subject -issuer notBefore=Jun 14 21:00:00 2026 GMT notAfter=Sep 14 21:00:00 2026 GMT subject=CN=osztromok.com issuer=C=US, O=Let's Encrypt, CN=R10 # Check expiry in human-readable days $ echo | openssl s_client -connect localhost:443 -servername osztromok.com 2>/dev/null \ | openssl x509 -noout -enddate notAfter=Sep 14 21:00:00 2026 GMT # Verify the full chain is correct (no "unable to verify" errors) $ curl -vI https://localhost --resolve localhost:443:127.0.0.1 2>&1 | grep -E "SSL|TLS|cert|issuer" * TLSv1.3 (OUT), TLS handshake * Server certificate: * subject: CN=osztromok.com * issuer: C=US; O=Let's Encrypt; CN=R10 * SSL certificate verify ok.
SSL Labs test: once your domain is publicly accessible (via Cloudflare Tunnel + Full Strict mode), visit ssllabs.com/ssltest and enter osztromok.com. SSL Labs gives a grade (A, A+, B) and flags any configuration weaknesses — weak cipher suites, missing HSTS, certificate chain issues. Aim for an A or A+ rating.

Troubleshooting

certbot: "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.osztromok.com"
The TXT record wasn't visible in DNS when Let's Encrypt checked. Causes: (1) DNS not yet on Cloudflare — the Cloudflare plugin creates TXT records in Cloudflare, but if your nameservers still point to IONOS those records won't be seen. Verify with dig NS osztromok.com +short — should return Cloudflare nameservers. (2) Propagation delay — increase --dns-cloudflare-propagation-seconds to 120. (3) Wrong API token scope — check the token has Zone → DNS → Edit permission for osztromok.com.
certbot: "Error: Could not read credentials from /etc/letsencrypt/cloudflare.ini"
Permissions on the credentials file are wrong. Run: sudo chmod 600 /etc/letsencrypt/cloudflare.ini && sudo chown root:root /etc/letsencrypt/cloudflare.ini. The file must be readable only by root — certbot refuses to use a credentials file that's world-readable as a security measure.
Apache: "SSLCertificateFile: file '/etc/letsencrypt/live/...' does not exist or is empty"
The path in the Apache config doesn't match where certbot saved the cert. Run sudo certbot certificates to see the exact paths, then update SSLCertificateFile and SSLCertificateKeyFile in the vhost config accordingly. Also check that the directory is accessible: sudo ls /etc/letsencrypt/live/osztromok.com/.
Visitors see 526 (Cloudflare: Invalid SSL) after switching to Full (Strict)
Cloudflare can reach your Apache but the certificate isn't valid or isn't trusted. Check: (1) is mod_ssl enabled? sudo apache2ctl -M | grep ssl — should show ssl_module. (2) Did Apache restart (not just reload) after enabling mod_ssl? (3) Run the openssl test: echo | openssl s_client -connect localhost:443 -servername osztromok.com 2>/dev/null | grep Verify — should say Verify return code: 0 (ok). (4) Temporarily switch Cloudflare back to Flexible to restore access, then diagnose.
Renewal dry-run fails: "Challenge failed for domain ..."
The renewal config at /etc/letsencrypt/renewal/osztromok.com.conf stores the original certbot options. If the Cloudflare API token changed or expired, update it in /etc/letsencrypt/cloudflare.ini and run sudo certbot renew --dry-run again. Also check that the API token hasn't been deleted in the Cloudflare dashboard (My Profile → API Tokens).

Quick Reference — Chapter 1

CommandPurpose
sudo certbot certonly --dns-cloudflare ...Obtain a cert using Cloudflare DNS-01 challenge — no open ports needed
sudo certbot certificatesList all certs managed by certbot with their expiry dates
sudo certbot renew --dry-runTest renewal without actually issuing a new cert — run this monthly
sudo systemctl status certbot.timerVerify the auto-renewal timer is active and see when it next fires
sudo a2enmod ssl headersEnable SSL and headers modules in Apache (restart required after)
sudo apache2ctl -M | grep sslConfirm ssl_module is loaded in the running Apache
echo | openssl s_client -connect localhost:443 -servername osztromok.com 2>/dev/null | grep VerifyQuick cert validity check — "Verify return code: 0 (ok)" = good
File / PathPurpose
/etc/letsencrypt/live/osztromok.com/fullchain.pemUse in Apache SSLCertificateFile — cert + intermediate chain
/etc/letsencrypt/live/osztromok.com/privkey.pemUse in Apache SSLCertificateKeyFile — never share or commit this
/etc/letsencrypt/cloudflare.iniCloudflare API token for certbot DNS-01 — chmod 600, owner root
/etc/letsencrypt/renewal/osztromok.com.confRenewal configuration — stores the original certbot options
/etc/letsencrypt/renewal-hooks/deploy/Scripts here run after every successful renewal — use for Apache reload
/var/log/letsencrypt/letsencrypt.logFull certbot log — first place to look when something goes wrong