TLS/SSL and Let's Encrypt

TLS/SSL and Let's Encrypt

Bitnesia • Aug 28, 2026 97 •ID

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 readable by anyone successfully eavesdropping on the network path between them. On shared office WiFi networks, public hotspots, or even wired networks with a compromised device, an Attacker simply needs to run a packet sniffer like Wireshark or tcpdump to read the request payload as-is, including login passwords sent via HTML forms or session cookies that should remain confidential.

This chapter closes that vulnerability by adding a TLS (Transport Layer Security) layer to all built virtual hosts. We will start from basic concepts of HTTPS and digital certificates, followed by Certbot and Let's Encrypt as the most common method to obtain free certificates for actual public domains, then directly practice TLS configuration in Nginx and Apache using lab certificates, and conclude 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 via TLS before transmission across the network. The older name for this protocol is SSL (Secure Sockets Layer), but all SSL versions (2.0 and 3.0) have been declared deprecated and insecure for many years. The term "SSL" persists in everyday conversations including this chapter title, but actual implementations running on any modern server, including the ones configured in this chapter, are TLS versions 1.2 and 1.3.

17.1.1 Encryption and TLS Handshake in Brief

TLS combines two types of encryption simultaneously. Asymmetric encryption, utilizing a pair of public and private keys, is used at the start of a connection to mutually verify identity and agree upon a shared secret key, while symmetric encryption, which is computationally much faster, is used to encrypt actual data throughout the connection session. This negotiation process at connection startup is called a 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 then compute the same session key without ever sending the private key across the network, and only after that does actual application data begin flowing in an encrypted state.

The full details of this handshake changed significantly between TLS 1.2 and TLS 1.3, particularly regarding the number of round-trips required before data transmission begins, but for Sysadmins managing web servers, these details are handled entirely by underlying TLS libraries such as OpenSSL. Our job is simply to provide a valid certificate and enable secure protocol versions, steps to be practiced directly in Section 17.3.

17.1.2 X.509 Certificate Anatomy and 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 issuing authority. The most important fields within this certificate include Subject (certificate owner identity, typically the domain name), Issuer (the party issuing and signing the certificate), Validity Period (the Not Before and Not After ranges determining active periods), 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 within the Subject Alternative Name (SAN) extension, which also allows a single certificate to cover multiple domains simultaneously: a feature directly utilized in Section 17.3.1 to cover all lab domains under one certificate.

The entity issuing certificates is called a Certificate Authority (CA). Browsers and operating systems store a trusted root CA list by default within their respective trust stores. Root CAs almost never sign domain certificates directly, delegating that task to an intermediate CA while keeping root keys offline for security. This chain of signatures from domain certificate, to intermediate CA, up to the trusted root CA is known as the chain of trust. Once a browser receives a certificate from a server, it traces this chain until finding a root CA already in its trust store; if the chain is broken or leads to an unknown CA, the browser displays a security warning instead of blindly opening the connection.

A certificate signed by oneself without a third-party CA is called a self-signed certificate. Cryptographically, its encryption strength is identical to a public CA-issued certificate, but because no chain of trust leads to any root CA recognized by browsers, a self-signed certificate always triggers security warnings in Visitor browsers. Such certificates are suitable for 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 explored further in Section 17.2.3 when attempting to issue certificates for our own lab domains.

17.2 Certbot and Let's Encrypt

Let's Encrypt is a non-profit Certificate Authority operated by the Internet Security Research Group (ISRG) since 2015, issuing free and fully automated TLS certificates. Its presence became one of the primary drivers for massive HTTPS adoption across the public web, as commercial CA TLS certificates were historically paid with manual issuance processes. Certbot, developed by the Electronic Frontier Foundation (EFF), is the most popular official client for communicating with Let's Encrypt and will be used throughout this chapter.

17.2.1 ACME: How Let's Encrypt Verifies Domain Ownership

Let's Encrypt issues certificates via a standardized protocol named ACME (Automated Certificate Management Environment, RFC 8555). Unlike traditional commercial CAs that manually verify applicant identities, ACME proves domain ownership automatically via challenges that clients, such as Certbot, must solve before a certificate is issued. The two most common challenge types used are:

  • HTTP-01: Certbot places a file containing a unique token at the path /.well-known/acme-challenge/ on the web server, then Let's Encrypt servers attempt to fetch that file over HTTP on port 80 of the requested domain. If retrieved successfully with matching content, domain ownership is considered proven.
  • DNS-01: Certbot is prompted to create a specific TXT record on the domain's public DNS zone, then Let's Encrypt servers verify its existence via standard DNS queries. This challenge type is mandatory for wildcard certificates (e.g., *.example.com) and useful when port 80 cannot be accessed directly from the internet.

A prerequisite applicable to both challenge types often overlooked by beginners: Let's Encrypt validation servers must be able to reach the requested domain over the public internet, either via HTTP in HTTP-01 or via public DNS in DNS-01. This point will be demonstrated in Section 17.2.3.

17.2.2 Certbot Installation

Ubuntu Server provides Certbot directly from official repositories, allowing standard APT installation without adding third-party repositories, consistent with package installation habits established since Chapter 6. Certbot from APT is split into core packages and separate plugin packages for each web server, preventing servers using only Nginx from installing Apache dependencies, and vice versa.

Practical Steps

  1. Update the package list, then install the core Certbot package.
    sudo apt update
    sudo apt install certbot
    This package alone suffices to issue certificates via standalone or webroot plugins, but cannot yet be used with --nginx or --apache flags to be tested in Section 17.2.3.
  2. Install the python3-certbot-nginx plugin so Certbot can read and edit Nginx configurations automatically via the --nginx flag. Also install python3-certbot-apache as our lab has used Apache since Chapter 13, although this package becomes relevant as a reference in Section 17.2.4.
    sudo apt install python3-certbot-nginx python3-certbot-apache
  3. Verify successful installation by checking its version.
    certbot --version

Verification and Troubleshooting

  • Displayed version numbers may vary depending on when Ubuntu repositories were last synchronized, so do not be surprised if numbers differ slightly from module examples, similar to the note for PHP versions in Section 14.1.1.
  • To confirm both plugins are installed, check via dpkg.
    dpkg -l | grep certbot
    Output should display three package lines with ii status in the first column: certbot, python3-certbot-nginx, and python3-certbot-apache.

17.2.3 Attempting to Issue Certificates for Lab Domains

Let's demonstrate firsthand what happens when attempting to issue a Let's Encrypt certificate for the www.example.local domain built since Chapter 12. Nginx from Chapter 16 should still be active on this server, so we use the newly installed --nginx plugin to edit the Nginx configuration automatically.

Practical Steps

  1. Run Certbot with the Nginx plugin, targeting both example.local and www.example.local domains simultaneously.
    sudo certbot --nginx -d example.local -d www.example.local
  2. Certbot will prompt for an email address for certificate expiration notifications, then request agreement to Terms of Service. Fill in as necessary and proceed.

Shortly after, this process will fail with an error message 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 functions as intended. The example.local domain resolves perfectly inside our lab network thanks to BIND9 from Chapter 9, but the .local TLD is not registered in public root DNS used across the internet. Therefore, Let's Encrypt servers attempting to lookup this domain from outside the lab network will always find it unknown. Not just 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 is a fundamental limitation of public CA-based trust systems, not merely a tooling limitation.

Verification and Troubleshooting

  • The same error appears for newly registered real public domains not yet pointed to the server's public IP via A/AAAA records, or servers with port 80 blocked by firewalls/NAT from the internet. The message DNS problem: NXDOMAIN indicates the domain itself was not found, whereas Fetching ... Connection refused or Connection timed out indicates the domain resolved but the server could not be reached on port 80.
  • In the field, mistakenly assuming internal domains like .local, .internal, or .lan can receive Let's Encrypt certificates is a common trap for new Sysadmins. Sections 17.2.4 and 17.3 address two different solutions for this scenario, 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 pointed via public DNS to server IPs (not private IPs like 192.168.1.20 used in this lab), the above Certbot workflow will run smoothly without obstacles. The command is identical to the attempt in Section 17.2.3, simply replacing lab domains with actual public domains.

sudo certbot --nginx -d your-actual-domain.com -d www.your-actual-domain.com

For Apache, use the --apache plugin installed in Section 17.2.2, which works similarly by reading and editing relevant VirtualHost files directly.

sudo certbot --apache -d your-actual-domain.com -d www.your-actual-domain.com

Both plugins automatically add ssl_certificate (Nginx) or SSLCertificateFile (Apache) directives to existing virtual hosts, while offering options to create automatic HTTP to HTTPS redirects: two exact items to be configured manually in Section 17.3 using lab certificates.

Certbot never places certificates directly inside web server configuration directories. Every successfully issued certificate is stored in /etc/letsencrypt/live/<domain-name>/, specifically as four symlinks pointing to the latest revisions inside /etc/letsencrypt/archive/<domain-name>/:

  • cert.pem: the domain certificate itself.
  • chain.pem: intermediate CA certificate.
  • fullchain.pem: combination of cert.pem and chain.pem, referenced by ssl_certificate in Nginx and SSLCertificateFile in Apache.
  • privkey.pem: private key, referenced by ssl_certificate_key in Nginx and SSLCertificateKeyFile in Apache.

The --nginx and --apache plugins automatically populate both directives with paths pointing to fullchain.pem and privkey.pem inside the live directory rather than the archive directory, eliminating manual path adjustments as done with self-signed certificates in Section 17.3. This detail is also essential for auto-renewal in Section 17.4: whenever Certbot renews a certificate, the archive contents increment with new revisions, and symlinks in live automatically update to point to those revisions. Thus, web server configurations pointing to live paths require no modification post-renewal.

For servers not using Nginx or Apache as a reverse proxy upfront (e.g., mail or database servers needing standalone TLS certificates), Certbot provides the --standalone plugin to run a temporary web server on port 80 to complete challenges, and the --webroot plugin to place challenge files directly into an existing web server's document root without modifying configurations.

AspectPublic Domain + Let's EncryptInternal Domain/Lab + Self-Signed
Visitor Browser TrustAutomatically trusted, no warningsAlways triggers security warnings
CostFreeFree
Domain RequirementsMust be public and verifiable from internetAny, including .local domains
Storage Location/etc/letsencrypt/live/<domain>/Customizable, e.g., /etc/ssl/example.local/
Validity Period90 days, requires auto-renewalCustom defined on creation, typically 365 days
Use CasesPublic services accessed by general VisitorsLabs, staging, internal server communication

17.3 TLS Configuration 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 mechanisms 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. The only differences are certificate sources and storage locations, not web server configuration methods.

17.3.1 Creating a Multi-Domain Self-Signed Certificate with OpenSSL

Thanks to the SAN extension discussed in Section 17.1.2, a single certificate can cover all lab domains simultaneously (example.local, www.example.local, blog.example.local, and app.example.local), avoiding repetitive processes for each virtual host. Unlike Let's Encrypt certificates fixed at /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/.

Practical Steps

  1. Create a dedicated directory to store lab certificates and private keys.
    sudo mkdir -p /etc/ssl/example.local
  2. 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
  3. Generate private key and self-signed certificate simultaneously in one command, valid for 365 days.
    sudo 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
    The -nodes flag stores the private key without a passphrase: a standard choice for web server certificates, as passphrases prevent Nginx or Apache from starting automatically without manual intervention upon service restarts.
  4. Restrict private key permissions to root read-only, following standard security practices for cryptographic key files.
    sudo chmod 600 /etc/ssl/example.local/example.local.key

Verification and Troubleshooting

  • Inspect the newly created certificate, ensuring all four domains are listed in the SAN extension.
    openssl x509 -in /etc/ssl/example.local/example.local.crt -noout -text | grep -A1 "Subject Alternative Name"
    Output must show all four domains matching san.cnf entries.
  • If openssl req fails with errors regarding missing alt_names, recheck indentation and bracketed section headers in san.cnf, as OpenSSL strictly parses configuration syntax.

17.3.2 Enabling HTTPS in Nginx for All Virtual Hosts

With certificates ready, the next step adds a new server block listening on port 443 for each Nginx virtual host built, while redirecting all HTTP traffic on port 80 to HTTPS so Visitors cannot access unencrypted versions.

Practical Steps

  1. Allow port 443 in the firewall, as Chapter 12 only opened port 80.
    sudo ufw allow 443/tcp
  2. Edit the example.local virtual host configuration, converting existing server blocks to HTTPS blocks and adding a new block for HTTP redirects.
    sudo nano /etc/nginx/sites-available/example.local
    server {
        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;
    }
    The ssl_protocols directive restricts Nginx to accept TLS 1.2 and TLS 1.3 only, rejecting older insecure versions like TLS 1.0 and 1.1. The second block serves no content, tasked solely with forcing HTTPS redirects via 301 Moved Permanently status codes.
  3. Repeat the pattern for blog.example.local.
    sudo nano /etc/nginx/sites-available/blog.example.local
    server {
        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;
    }
  4. Apply the pattern to app.example.local from Chapter 16: add ssl_certificate, ssl_certificate_key, and ssl_protocols directives to the existing server block, update listen to 443 ssl, and add the port 80 redirect block. The upstream block and proxy_pass directive remain unchanged.
  5. Test overall configuration syntax, then reload.
    sudo nginx -t
    sudo systemctl reload nginx
  6. Test from another machine on the same network. The -k flag allows curl to proceed despite self-signed certificates.
    curl -k https://www.example.local
    curl -k https://blog.example.local
    curl -k https://app.example.local
  7. Ensure HTTP to HTTPS redirection works.
    curl -I http://www.example.local
    The response must display status 301 with a Location header pointing to the https:// version of the domain.

Verification and Troubleshooting

  • Without the -k flag, curl rejects connections with curl: (60) SSL certificate problem: self-signed certificate. This indicates proper curl behavior when encountering unverified issuer CAs, matching warnings displayed in Visitor browsers without manual exceptions.
  • If nginx: [emerg] cannot load certificate occurs during nginx -t, verify file paths in ssl_certificate and ensure Nginx master processes running as root can access /etc/ssl/example.local/.
  • To view certificate details sent during handshakes, run openssl s_client directly from the terminal:
    echo | openssl s_client -connect www.example.local:443 -servername www.example.local 2>/dev/null | openssl x509 -noout -dates -subject
    This command displays the subject and notBefore/notAfter dates, verifying that the server serves the newly generated certificate.

17.3.3 Enabling HTTPS in Apache and Redirecting HTTP to HTTPS

Apache virtual hosts from Chapter 13 for example.local and blog.example.local remain untouched until now. This section adds HTTPS to both, reusing the SAN certificate generated in Section 17.3.1.

Practical Steps

  1. Stop Nginx and start Apache, following the port 80 toggle routine established since Chapters 13 and 14.
    sudo systemctl stop nginx
    sudo systemctl start apache2
  2. Enable the ssl module, which provides SSL* directives required for Apache to serve HTTPS.
    sudo a2enmod ssl
  3. Edit example.local.conf, adding a port 443 VirtualHost block below the existing block, then converting the old block into a redirect.
    sudo nano /etc/apache2/sites-available/example.local.conf
    <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>
    The SSLProtocol -all +TLSv1.2 +TLSv1.3 directive mirrors Nginx's ssl_protocols TLSv1.2 TLSv1.3;, disabling older protocol versions before enabling secure ones.
  4. Repeat 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>
  5. Test configuration syntax, then restart Apache. Module changes require full service restarts rather than simple reloads, following practices from Section 13.3.1.
    sudo apache2ctl configtest
    sudo systemctl restart apache2
  6. Test connectivity from another network machine.
    curl -k https://example.local
    curl -I http://example.local
  7. After Apache testing completes, restore Nginx as the active web server to align with Chapter 14 states and Chapter 17 practices.
    sudo systemctl stop apache2
    sudo systemctl start nginx

Verification and Troubleshooting

  • The Apache Full UFW profile enabled in Chapter 13 includes port 443, eliminating additional firewall rule setup for Apache in this section.
  • If apache2ctl configtest reports SSL Library Error concerning certificate files, recheck directory permissions on /etc/ssl/example.local/. Unlike BIND9 in Chapter 9, Apache on Ubuntu does not run under strict AppArmor profile directory restrictions, making standard Linux permission errors the primary cause.
  • The Redirect permanent directive from mod_alias is enabled by default in Apache without additional a2enmod commands, unlike mod_rewrite redirects which require separate module activation for advanced routing needs.

17.4 Certificate Auto-renewal

Let's Encrypt certificates carry a short 90-day validity lifetime by design, encouraging ecosystem automation rather than error-prone manual renewals. This section covers automatic renewal operations for Let's Encrypt certificates alongside expiration monitoring methods 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 includes periodic renewal jobs enabled by default post-installation without manual Sysadmin configuration. Scheduled via systemd timer mechanisms detailed in Chapter 5, the job runs twice daily to check all Certbot-issued certificates on the server. Certbot only renews certificates with 30 days or less remaining validity, preventing unnecessary server loads on Let's Encrypt infrastructure.

Practical Steps

  1. Inspect active renewal timers.
    systemctl list-timers | grep certbot
    Output displays the next execution time for certbot.timer, confirming background renewal processing.

For servers operating active Let's Encrypt certificates issued in Section 17.2.4, the following commands manage manual renewal operations:

sudo certbot renew --dry-run
sudo certbot certificates

The --dry-run flag simulates the renewal process (including ACME challenges) without replacing active certificates: a safe method to test automated execution prior to certificate expiration. Running certbot certificates lists managed certificates, their validity periods, and file paths to fullchain.pem and privkey.pem inside /etc/letsencrypt/live/.

Verification and Troubleshooting

  • Since lab domain verification failed in Section 17.2.3, running certbot renew --dry-run and certbot certificates on this test server will report no managed certificates, which is expected behavior. Output appears only on servers carrying valid Let's Encrypt certificates from Section 17.2.4.
  • If certbot.timer is missing from systemctl list-timers, check if the service unit is active and enabled:
    systemctl status certbot.timer

17.4.2 Expiration Monitoring for Self-Signed Certificates

Self-signed certificates created in Section 17.3.1 are not managed by Certbot, meaning automatic renewal services do not process them. Tracking expiration remains a manual Sysadmin responsibility.

Practical Steps

  1. Check expiration dates directly from certificate files.
    openssl x509 -enddate -noout -in /etc/ssl/example.local/example.local.crt
    Output displays a single notAfter= line indicating the expiration date, set 365 days from creation in Section 17.3.1.
  2. To avoid missing expiration windows, set calendar reminders or write check scripts executed via systemd timers (as covered in Chapter 5):
    openssl x509 -checkend 2592000 -noout -in /etc/ssl/example.local/example.local.crt && echo "Certificate valid for more than 30 days" || echo "Certificate expires within 30 days"
    The -checkend flag accepts seconds (2592000 equals 30 days), matching the threshold used by Certbot internally in Section 17.4.1.

Verification and Troubleshooting

  • When lab certificates approach expiration, re-run the openssl req command from Section 17.3.1 using the existing san.cnf file to generate updated certificates, then reload Nginx and Apache to read new files from disk.
  • For environments beyond personal labs (such as multiple internal servers requiring TLS certificates), individual self-signed certificate generation becomes cumbersome and prone to human error. A robust alternative involves running an internal private CA (e.g., via tools like step-ca). Distributing one internal root certificate across office devices allows internal servers to request certificates directly from that CA: an advanced topic beyond this series scope.

At this point, all virtual hosts built since Chapter 12 (example.local, www.example.local, blog.example.local in Nginx/Apache, and app.example.local from Chapter 16) serve HTTPS complete with automatic HTTP redirects. Core differences between Let's Encrypt public domain certificates and self-signed internal certificates have been addressed, including storage path distinctions. Part IV of the web services series concludes here. Chapter 18 opens Part V, transitioning to database servers using PostgreSQL 18: components running behind web applications served via Nginx and Apache throughout this section.