All virtual hosts built since Chapter 12, namely example.local, www.example.local, blog.example.local in Nginx and Apache, and app.example.local in Chapter 16, still serve traffic over plain HTTP. Every request and response between Visitor and server runs as plain text that can be read by anyone who eavesdrops on the network path between them. On shared office WiFi networks, public hotspots, or even wired networks where a device has been compromised, an Attacker simply runs a packet sniffer like Wireshark or tcpdump to read request contents as-is, including login passwords sent via HTML forms or session cookies that should remain secret.
This chapter closes that gap by adding a TLS (Transport Layer Security) layer to all virtual hosts we have built. We will start from basic concepts of HTTPS and digital certificates, followed by Certbot and Let's Encrypt as the most common way to obtain free certificates for real public domains, then practice TLS configuration in Nginx and Apache using lab certificates, and wrap up with auto-renewal strategies so certificates never expire unnoticed.
17.1 HTTPS Concepts and Digital Certificates
HTTPS is essentially the exact same HTTP protocol used since Chapter 12, except all its data is wrapped in TLS before being sent over the network. The old name for this protocol is SSL (Secure Sockets Layer), but all SSL versions (2.0 and 3.0) have been declared obsolete and insecure for years. The term "SSL" persists in everyday conversation including this chapter title, but actual implementations running on any modern server, including those configured in this chapter, are TLS versions 1.2 and 1.3.
17.1.1 Encryption and the TLS Handshake in Brief
TLS combines two types of encryption simultaneously. Asymmetric encryption, using a pair of public and private keys, is used at connection startup to mutually verify identity and agree on a shared secret key, whereas symmetric encryption, which is computationally faster, encrypts actual data throughout the connection session. This initial negotiation process is called the TLS handshake: the client sends a list of supported ciphers via a ClientHello message, the server responds by selecting a cipher and sending its certificate via a ServerHello message, both parties calculate the same session key without transmitting private keys over the network, and only then does actual application data flow encrypted.
Full handshake details changed significantly between TLS 1.2 and TLS 1.3, especially regarding the number of round trips required before data transmission begins, but for Sysadmins managing web servers, these details are handled behind the scenes by TLS libraries such as OpenSSL. Our task is simply providing a valid certificate and enabling secure protocol versions, which will be practiced directly in Section 17.3.
17.1.2 Anatomy of X.509 Certificates and the Chain of Trust
Digital certificates used by HTTPS follow the X.509 standard, a format binding a domain identity to a public key, digitally signed by the issuer. The most important fields in this certificate include Subject (certificate owner identity, usually a domain name), Issuer (the party issuing and signing the certificate), Validity Period (Not Before and Not After ranges determining validity start and end times), Public Key, and the Issuer's digital signature at the end. In recent years, modern browsers like Chrome and Firefox no longer trust the Common Name (CN) field alone, requiring registered domain names inside the Subject Alternative Name (SAN) extension. This capability allows one certificate to cover multiple domains simultaneously, a feature utilized in Section 17.3.1 to cover all lab domains in a single certificate.
The party issuing certificates is called a Certificate Authority (CA). Browsers and operating systems store a list of trusted root CAs natively in their trust stores. Root CAs rarely sign domain certificates directly; instead, they delegate this task to intermediate CAs while keeping root keys offline for security. This sequence of signatures from domain certificate to intermediate CA up to the trusted root CA is called the chain of trust. Once a browser receives a certificate from a server, it traces this chain until finding a root CA present in its trust store; if the chain is broken or ends at an unknown CA, the browser displays a security warning instead of connecting directly.
Certificates signed by oneself, without a third-party CA, are called self-signed certificates. Cryptographically, their encryption strength equals certificates issued by public CAs, but because no chain of trust leads to a root CA recognized by browsers, self-signed certificates trigger security warnings for Visitors. Such certificates suit internal needs like labs, staging environments, or server-to-server communications fully controlled by Sysadmins, but are never suitable for public services accessed by general Visitors, a point discussed further in Section 17.2.3 when issuing certificates for our lab domain.
17.2 Certbot and Let's Encrypt
Let's Encrypt is a non-profit Certificate Authority managed by the Internet Security Research Group (ISRG) since 2015, issuing free and fully automated TLS certificates. Its presence became a major driver for massive HTTPS adoption across the public web, as commercial CA TLS certificates were previously paid and manually issued. Certbot, developed by the Electronic Frontier Foundation (EFF), is the most popular official client for communicating with Let's Encrypt and will be used in this chapter.
17.2.1 ACME: How Let's Encrypt Verifies Domain Ownership
Let's Encrypt issues certificates via a standardized protocol called ACME (Automated Certificate Management Environment, RFC 8555). Unlike legacy commercial CAs that manually verify applicant identities, ACME proves domain ownership automatically through challenges completed by clients like Certbot before certificate issuance. The two most common challenge types are:
- HTTP-01: Certbot places a file containing a unique token at
/.well-known/acme-challenge/on the web server, and Let's Encrypt servers attempt to retrieve this file via HTTP on port 80 of the requested domain. If retrieved successfully with matching content, domain ownership is proven. - DNS-01: Certbot is requested to create a specific
TXTrecord in the domain's public DNS zone, and Let's Encrypt servers verify its existence via standard DNS queries. This challenge type is required for wildcard certificates (e.g.,*.example.com) and useful when port 80 cannot be reached directly from the internet.
A prerequisite applying to both challenge types often overlooked by beginners: validation servers owned by Let's Encrypt must reach the requested domain over the public internet, either via HTTP in HTTP-01 or public DNS in DNS-01. This point will be demonstrated in Section 17.2.3.
17.2.2 Installing Certbot
Ubuntu Server provides Certbot directly from official repositories, allowing installation using standard APT without adding third-party repositories, consistent with package installation practices established since Chapter 6. Certbot in APT is split into core packages and separate plugin packages for each web server, ensuring Nginx-only servers avoid Apache dependencies and vice versa.
Hands-on Steps
- Update package lists and install the core Certbot package.
This package suffices for issuing certificates viasudo apt update sudo apt install certbotstandaloneorwebrootplugins, but cannot yet be used with--nginxor--apacheflags tested in Section 17.2.3. - Install
python3-certbot-nginxso Certbot can read and edit Nginx configurations automatically via the--nginxflag. Also installpython3-certbot-apachesince our lab includes Apache since Chapter 13, though this package becomes relevant in Section 17.2.4.sudo apt install python3-certbot-nginx python3-certbot-apache - Verify installation success by checking its version.
certbot --version
Verification and Troubleshooting
- Displayed version numbers may vary depending on when Ubuntu repositories were last synced, so do not be surprised if numbers differ from module examples, similar to PHP version notes in Section 14.1.1.
- To ensure both plugins are installed properly, verify via
dpkg.
Output should show three package lines with statusdpkg -l | grep certbotiiin the first column:certbot,python3-certbot-nginx, andpython3-certbot-apache.
17.2.3 Attempting Certificate Issuance for Lab Domains
Let us directly demonstrate what happens when attempting to issue a Let's Encrypt certificate for domain www.example.local built in Chapter 12. Nginx from Chapter 16 should remain active on this server, so we use the --nginx plugin installed earlier to auto-edit Nginx configuration.
Hands-on Steps
- Run Certbot with the Nginx plugin, targeting domains
example.localandwww.example.localsimultaneously.sudo certbot --nginx -d example.local -d www.example.local - Certbot prompts for an email address for expiration notifications, followed by Terms of Service agreement. Fill out as needed and proceed.
Shortly after, this process fails with an error similar to the following:
Certbot failed to authenticate some domains (authenticator: nginx). The Certificate Authority reported these problems:
Domain: www.example.local
Type: dns
Detail: DNS problem: NXDOMAIN looking up A for www.example.local - check that a DNS record exists for this domain
Hint: The Certificate Authority failed to download the challenge files from the temporary standalone webserver started by Certbot on port 80. Ensure that the listed domains point to this machine and that it can accept inbound connections from the internet.This failure is not a bug, but proof that the HTTP-01 mechanism in Section 17.2.1 operates as intended. Domain example.local resolves perfectly within our lab network thanks to BIND9 from Chapter 9, but TLD .local is entirely unregistered in public root DNS used worldwide. Therefore, Let's Encrypt servers attempting external lookups will find the domain unknown. Beyond Certbot or Let's Encrypt, any public CA complying with CA/Browser Forum rules will never issue certificates for domains whose ownership cannot be verified from the public internet. This represents a fundamental limitation of public CA trust systems, not a mere tooling limitation.
Verification and Troubleshooting
- The same error occurs for newly registered public domains not yet pointing to server public IPs via
A/AAAArecords, or servers where port 80 remains blocked by firewalls/NAT. The messageDNS problem: NXDOMAINindicates the domain itself was not found, whileFetching ... Connection refusedorConnection timed outindicates the domain resolved but the server was unreachable on port 80. - In practice, assuming internal domains like
.local,.internal, or.lancan obtain Let's Encrypt certificates is a common pitfall for new Sysadmins. Sections 17.2.4 and 17.3 address two different solutions depending on whether the domain is genuinely public or purely internal.
17.2.4 Certbot Workflow for Real Public Domains
For Sysadmins managing production servers with registered public domains pointing via public DNS to server IPs (not private IPs like 192.168.1.20 used in this lab), the Certbot workflow above executes smoothly. Commands are identical to Section 17.2.3, replacing lab domains with actual public domains.
sudo certbot --nginx -d real-domain.com -d www.real-domain.comFor Apache, use the --apache plugin installed in Section 17.2.2. It operates similarly by directly reading and editing relevant VirtualHost files.
sudo certbot --apache -d real-domain.com -d www.real-domain.comBoth plugins automatically add ssl_certificate (Nginx) or SSLCertificateFile (Apache) directives to existing virtual hosts, offering options to configure automatic HTTP-to-HTTPS redirects: two tasks we will manually configure in Section 17.3 using lab certificates.
Certbot never places certificates directly into web server configuration directories. Issued certificates are stored in /etc/letsencrypt/live/<domain-name>/ as four symlinks pointing to the latest certificate revisions in /etc/letsencrypt/archive/<domain-name>/:
cert.pem: domain certificate itself.chain.pem: intermediate CA certificate.fullchain.pem: combination ofcert.pemandchain.pem, referenced byssl_certificatein Nginx orSSLCertificateFilein Apache.privkey.pem: private key, referenced byssl_certificate_keyin Nginx orSSLCertificateKeyFilein Apache.
The --nginx and --apache plugins automatically populate directives with paths pointing to fullchain.pem and privkey.pem inside the live directory rather than archive. This detail is crucial for auto-renewal in Section 17.4: each time Certbot renews a certificate, new revisions are added to archive and live symlinks update automatically, requiring no web server configuration changes after renewal.
For servers not using Nginx or Apache as reverse proxies (e.g., mail or database servers requiring dedicated TLS certificates), Certbot provides a --standalone plugin running a temporary web server on port 80 to complete challenges, as well as a --webroot plugin placing challenge files directly into existing web server document roots without configuration edits.
| Aspect | Public Domain + Let's Encrypt | Internal Domain/Lab + Self-Signed |
|---|---|---|
| Browser Trust | Automatically trusted, no warnings | Triggers security warnings |
| Cost | Free | Free |
| Domain Requirement | Must be public and internet-verifiable | Any domain, including .local |
| Storage Path | /etc/letsencrypt/live/<domain>/ | User-defined, e.g., /etc/ssl/example.local/ |
| Validity Period | 90 days, auto-renewal required | User-defined upon creation, typically 365 days |
| Use Case | Public services accessed by general Visitors | Labs, staging, internal server-to-server traffic |
17.3 Configuring TLS in Nginx and Apache with Self-Signed Certificates
Because our lab domain is purely internal, this section uses self-signed certificates to practice actual TLS configuration mechanics in Nginx and Apache. Directives used, such as ssl_certificate in Nginx and SSLCertificateFile in Apache, match those automatically added by Certbot for real public domains in Section 17.2.4. Differences lie only in certificate source and storage locations, not web server configuration methodology.
17.3.1 Creating Multi-Domain Self-Signed Certificates with OpenSSL
Thanks to the SAN extension discussed in Section 17.1.2, we can generate a single certificate covering all lab domains: example.local, www.example.local, blog.example.local, and app.example.local, avoiding repetitive generation per virtual host. Unlike Let's Encrypt certificates standardized in /etc/letsencrypt/live/, self-signed certificates can be stored anywhere; we use /etc/ssl/example.local/ to distinguish them from system default certificates in /etc/ssl/certs/.
Hands-on Steps
- Create a dedicated directory to store lab certificates and private keys.
sudo mkdir -p /etc/ssl/example.local - Create an OpenSSL configuration file defining certificate identity and SAN entries.
sudo nano /etc/ssl/example.local/san.cnf[req] distinguished_name = req_distinguished_name x509_extensions = v3_req prompt = no [req_distinguished_name] C = ID ST = DKI Jakarta L = Jakarta O = Linux Server 101 Lab CN = example.local [v3_req] subjectAltName = @alt_names [alt_names] DNS.1 = example.local DNS.2 = www.example.local DNS.3 = blog.example.local DNS.4 = app.example.local - Generate private key and self-signed certificate simultaneously in a single command, valid for 365 days.
Thesudo openssl req -x509 -nodes -newkey rsa:2048 \ -keyout /etc/ssl/example.local/example.local.key \ -out /etc/ssl/example.local/example.local.crt \ -days 365 \ -config /etc/ssl/example.local/san.cnf-nodesflag stores private keys without passphrases, standard for web server certificates as passphrases prevent Nginx or Apache from starting automatically upon service restarts without manual intervention. - Restrict private key permissions so only root can read it, following standard security practices for cryptographic key files.
sudo chmod 600 /etc/ssl/example.local/example.local.key
Verification and Troubleshooting
- Inspect generated certificate contents to ensure all four domains exist in the SAN extension.
Output should display all four domains as configured inopenssl x509 -in /etc/ssl/example.local/example.local.crt -noout -text | grep -A1 "Subject Alternative Name"san.cnf. - If
openssl reqfails with errors regarding missingalt_names, recheck section header formatting (brackets) and indentation insan.cnf, as OpenSSL strictly enforces configuration syntax.
17.3.2 Enabling HTTPS in Nginx for All Virtual Hosts
With certificates ready, next add a new server block listening on port 443 for each Nginx virtual host, while redirecting all HTTP traffic on port 80 to HTTPS so Visitors cannot access unencrypted versions.
Hands-on Steps
- Allow port 443 in the firewall, as Chapter 12 only opened port 80.
sudo ufw allow 443/tcp - Edit the
example.localvirtual host configuration, modifying the existingserverblock to HTTPS and adding a new block for HTTP redirection.sudo nano /etc/nginx/sites-available/example.local
Theserver { listen 443 ssl; listen [::]:443 ssl; server_name example.local www.example.local; root /var/www/example.local/html; ssl_certificate /etc/ssl/example.local/example.local.crt; ssl_certificate_key /etc/ssl/example.local/example.local.key; ssl_protocols TLSv1.2 TLSv1.3; location / { try_files $uri $uri/ =404; } error_page 404 /404.html; location = /404.html { internal; } } server { listen 80; listen [::]:80; server_name example.local www.example.local; return 301 https://$host$request_uri; }ssl_protocolsdirective restricts Nginx to accept TLS 1.2 and TLS 1.3 only, rejecting insecure older versions like TLS 1.0 and 1.1. The second block serves no content, enforcing HTTPS redirection via301 Moved Permanentlystatus. - Repeat the pattern for
blog.example.local.sudo nano /etc/nginx/sites-available/blog.example.localserver { listen 443 ssl; listen [::]:443 ssl; server_name blog.example.local; root /var/www/blog.example.local/html; ssl_certificate /etc/ssl/example.local/example.local.crt; ssl_certificate_key /etc/ssl/example.local/example.local.key; ssl_protocols TLSv1.2 TLSv1.3; location / { try_files $uri $uri/ =404; } } server { listen 80; listen [::]:80; server_name blog.example.local; return 301 https://$host$request_uri; } - Apply the pattern to
app.example.localfrom Chapter 16 by addingssl_certificate,ssl_certificate_key, andssl_protocolsdirectives to the existingserverblock, updatinglistendirectives to443 ssl, and adding a port 80 redirect block. Theupstreamblock andproxy_passdirective remain unchanged. - Test configuration syntax and reload.
sudo nginx -t sudo systemctl reload nginx - Test from another machine on the same network. The
-kflag prevents curl from failing due to self-signed certificates.curl -k https://www.example.local curl -k https://blog.example.local curl -k https://app.example.local - Verify HTTP to HTTPS redirection works properly.
Response should show acurl -I http://www.example.local301status with aLocationheader pointing tohttps://.
Verification and Troubleshooting
- Without the
-kflag, curl rejects connections withcurl: (60) SSL certificate problem: self-signed certificate. This behavior is expected when CAs are untrusted by curl, matching browser warning behaviors. - If
nginx: [emerg] cannot load certificateappears duringnginx -t, verify file paths inssl_certificateand check directory access permissions for/etc/ssl/example.local/. - To inspect certificates served during handshakes, execute
openssl s_clientdirectly in the terminal:
This command displays subject details along withecho | openssl s_client -connect www.example.local:443 -servername www.example.local 2>/dev/null | openssl x509 -noout -dates -subjectnotBeforeandnotAfterdates.
17.3.3 Enabling HTTPS in Apache and Redirecting HTTP to HTTPS
Apache virtual hosts for example.local and blog.example.local built in Chapter 13 remain unmodified. This section enables HTTPS using the same SAN certificate generated in Section 17.3.1.
Hands-on Steps
- Stop Nginx and start Apache, matching the port 80 toggle pattern established in Chapter 13 and Chapter 14.
sudo systemctl stop nginx sudo systemctl start apache2 - Enable the
sslmodule providingSSL*directives required by Apache.sudo a2enmod ssl - Edit virtual host
example.local.conf, adding a newVirtualHostblock for port 443 below the existing block, and converting port 80 to redirect.sudo nano /etc/apache2/sites-available/example.local.conf
The<VirtualHost *:80> ServerName example.local ServerAlias www.example.local Redirect permanent / https://example.local/ </VirtualHost> <VirtualHost *:443> ServerName example.local ServerAlias www.example.local DocumentRoot /var/www/example.local/html SSLEngine on SSLCertificateFile /etc/ssl/example.local/example.local.crt SSLCertificateKeyFile /etc/ssl/example.local/example.local.key SSLProtocol -all +TLSv1.2 +TLSv1.3 <Directory /var/www/example.local/html> Options -Indexes AllowOverride None Require all granted </Directory> ErrorLog ${APACHE_LOG_DIR}/example.local.error.log CustomLog ${APACHE_LOG_DIR}/example.local.access.log combined </VirtualHost>SSLProtocol -all +TLSv1.2 +TLSv1.3directive matches Nginx'sssl_protocols TLSv1.2 TLSv1.3;functionality by disabling old protocols before activating secure ones. - Repeat the pattern for
blog.example.local.conf.sudo nano /etc/apache2/sites-available/blog.example.local.conf<VirtualHost *:80> ServerName blog.example.local Redirect permanent / https://blog.example.local/ </VirtualHost> <VirtualHost *:443> ServerName blog.example.local DocumentRoot /var/www/blog.example.local/html SSLEngine on SSLCertificateFile /etc/ssl/example.local/example.local.crt SSLCertificateKeyFile /etc/ssl/example.local/example.local.key SSLProtocol -all +TLSv1.2 +TLSv1.3 <Directory /var/www/blog.example.local/html> Options -Indexes AllowOverride None Require all granted </Directory> ErrorLog ${APACHE_LOG_DIR}/blog.example.local.error.log CustomLog ${APACHE_LOG_DIR}/blog.example.local.access.log combined </VirtualHost> - Test configuration syntax and restart Apache. Module changes require full restarts rather than simple reloads.
sudo apache2ctl configtest sudo systemctl restart apache2 - Test from another machine on the network.
curl -k https://example.local curl -I http://example.local - Revert active services back to Nginx to match ending state conditions from Chapter 14 and Chapter 17 practices.
sudo systemctl stop apache2 sudo systemctl start nginx
Verification and Troubleshooting
- The UFW profile
Apache Fullenabled in Chapter 13 includes port 443, eliminating additional firewall rule requirements. - If
apache2ctl configtestthrowsSSL Library Errorrelated to certificate files, check permissions on/etc/ssl/example.local/. - The
Redirect permanentdirective from modulemod_aliascomes enabled by default in Apache, requiring no extraa2enmodsetup.
17.4 Certificate Auto-renewal
Let's Encrypt certificates carry short 90-day lifespans, driving ecosystem automation over error-prone manual renewal processes. This section covers automated renewals for Let's Encrypt certificates alongside manual expiration tracking for self-signed lab certificates.
17.4.1 Let's Encrypt Auto-renewal via Certbot
The certbot APT package installed in Section 17.2.2 configures periodic renewal background jobs automatically. Scheduled via systemd timer (covered in Chapter 5), checks run twice daily across issued certificates. Certbot only renews certificates expiring within 30 days or fewer, keeping Let's Encrypt servers free from unnecessary requests.
Hands-on Steps
- Check active renewal timers configured upon installation.
Output indicates the next scheduled execution ofsystemctl list-timers | grep certbotcertbot.timer.
For servers running active Let's Encrypt certificates from Section 17.2.4, these two management commands are essential:
sudo certbot renew --dry-run
sudo certbot certificatesThe --dry-run flag simulates renewal processes and ACME challenges without overwriting active production certificates. Running certbot certificates lists managed certificates, expiration windows, and active paths in /etc/letsencrypt/live/.
Verification and Troubleshooting
- Because validation failed in Section 17.2.3,
certbot renew --dry-runreports no managed certificates on this lab instance. - If
certbot.timeris missing fromsystemctl list-timers, verify timer service status via:systemctl status certbot.timer
17.4.2 Monitoring Self-Signed Certificate Expiration
Self-signed certificates created in Section 17.3.1 are unmanaged by Certbot, placing expiration tracking entirely on Sysadmins.
Hands-on Steps
- Check expiration dates directly from certificate files.
Output displays a singleopenssl x509 -enddate -noout -in /etc/ssl/example.local/example.local.crtnotAfter=line matching 365 days from creation. - To track upcoming expirations programmatically, evaluate remaining valid days using OpenSSL commands wrapped in automated checks:
Theopenssl x509 -checkend 2592000 -noout -in /etc/ssl/example.local/example.local.crt && echo "Certificate valid for over 30 days" || echo "Certificate expires within 30 days"-checkendflag evaluates seconds (2592000 seconds equals 30 days), matching Certbot's internal threshold.
Verification and Troubleshooting
- When lab certificates near expiration, rerun the
openssl reqcommand from Section 17.3.1 using originalsan.cnfconfigurations, followed by web server service reloads. - For larger infrastructure demands, deploying private CA solutions like
step-caallows internal root certificate distribution across networks, streamlining local certificate issuance.
All virtual hosts built since Chapter 12 (example.local, www.example.local, blog.example.local, and app.example.local) now serve HTTPS with HTTP auto-redirection. Section IV covering web services concludes here. Chapter 18 opens Section V, shifting focus toward database administration with PostgreSQL 18.

