Running an HTTPS endpoint protects credentials, cookies, and API payloads from being read or modified in transit. In internal networks and short-lived development environments, a self-signed certificate provides encryption without waiting on external Certificate Authority issuance. The trade-off is trust: browsers and clients warn unless the certificate (or a private CA) is explicitly trusted.

Apache terminates TLS through mod_ssl on port 443, presenting an X.509 certificate during the handshake and using the corresponding private key to prove ownership. Each HTTPS VirtualHost points to a certificate file (commonly .crt) via SSLCertificateFile and a key file (commonly .key) via SSLCertificateKeyFile. A self-signed certificate signs its own public key, so the chain of trust ends at the server unless the certificate is installed as a trust anchor on clients.

Commands and paths target the Ubuntu and Debian Apache packaging, which uses /etc/apache2/ plus the a2enmod and a2ensite helpers. Modern clients validate hostnames from the subjectAltName extension, so relying on a Common Name alone can trigger certificate errors. Keep the private key readable only by root, and use a publicly trusted certificate for Internet-facing sites.

Steps to set up a self-signed TLS certificate for Apache:

  1. Open a terminal.
  2. Create a directory for the certificate and private key.
    $ sudo mkdir -p /etc/apache2/ssl
  3. Generate a self-signed certificate and private key with OpenSSL using subjectAltName for the hostname.
    $ sudo openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \
      -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt \
      -subj "/CN=host.example.net" -addext "subjectAltName=DNS:host.example.net,DNS:www.host.example.net"
    Generating a RSA private key
    ##### snipped #####
    -----

    The --nodes option writes an unencrypted private key, so strict file permissions are required.

    Add more names by extending the value, for example subjectAltName=DNS:example.com,DNS:www.example.com.

  4. Restrict the private key to root-only read access.
    $ sudo chmod 600 /etc/apache2/ssl/apache.key
    $ sudo ls -l /etc/apache2/ssl/apache.key
    -rw------- 1 root root 1704 Jan 10 13:43 /etc/apache2/ssl/apache.key
  5. Enable mod_ssl.
    $ sudo a2enmod ssl
    Considering dependency mime for ssl:
    Module mime already enabled
    Considering dependency socache_shmcb for ssl:
    Enabling module socache_shmcb.
    Enabling module ssl.
    See /usr/share/doc/apache2/README.Debian.gz on how to configure SSL and create self-signed certificates.
    To activate the new configuration, you need to run:
      systemctl restart apache2

    If HTTPS refuses connections, confirm Listen 443 exists in /etc/apache2/ports.conf/.

  6. Edit /etc/apache2/sites-available/host.example.net.conf/.
    $ sudo vi /etc/apache2/sites-available/host.example.net.conf

    Back up existing site configuration files before edits to avoid overwriting a working configuration.

  7. Add an HTTPS VirtualHost on port 443 that references the generated certificate and key.
    <VirtualHost *:443>
      ServerName host.example.net
      DocumentRoot /var/www/html
     
      SSLEngine on
      SSLCertificateFile /etc/apache2/ssl/apache.crt
      SSLCertificateKeyFile /etc/apache2/ssl/apache.key
    </VirtualHost>
  8. Enable the site configuration.
    $ sudo a2ensite host.example.net.conf
    Enabling site host.example.net.
    To activate the new configuration, you need to run:
      systemctl reload apache2
  9. Optionally redirect HTTP traffic to HTTPS in the port 80 VirtualHost.
    <VirtualHost *:80>
      ServerName host.example.net
      Redirect permanent / https://host.example.net/
    </VirtualHost>

    A redirect is best placed in the existing port 80 site for the same ServerName.

  10. Test the Apache configuration for syntax errors.
    $ sudo apache2ctl configtest
    Syntax OK
  11. Restart Apache to load mod_ssl with the updated VirtualHost configuration.
    $ sudo systemctl restart apache2

    On RHEL-based distributions the service is commonly named httpd.

  12. Verify TLS negotiation and certificate presentation over HTTPS.
    $ curl -skvI https://host.example.net/
    * Host host.example.net:443 was resolved.
    * IPv6: (none)
    * IPv4: 192.0.2.40
    *   Trying 192.0.2.40:443...
    * Connected to host.example.net (192.0.2.40) port 443
    * ALPN: curl offers h2,http/1.1
    ##### snipped #####
    * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
    * Server certificate:
    *  subject: CN=host.example.net
    *  issuer: CN=host.example.net
    *  SSL certificate verify result: self-signed certificate (18), continuing anyway.
    ##### snipped #####
    > HEAD / HTTP/1.1
    > Host: host.example.net
    ##### snipped #####
    HTTP/1.1 200 OK
    Date: Sat, 10 Jan 2026 05:43:44 GMT
    Server: Apache/2.4.58 (Ubuntu)
    ##### snipped #####

    Remove --insecure from curl after the certificate is trusted by the client.

  13. Optionally trust the self-signed certificate on a Debian or Ubuntu client to avoid browser warnings.
    $ sudo cp /etc/apache2/ssl/apache.crt /usr/local/share/ca-certificates/host.example.net-self-signed.crt
    $ sudo update-ca-certificates
    Updating certificates in /etc/ssl/certs...
    rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
    1 added, 0 removed; done.
    Running hooks in /etc/ca-certificates/update.d...
    done.

    Some applications, including Firefox, use a separate trust store that requires an explicit import.

  14. Verify certificate trust without --insecure after installing the certificate into the trust store.
    $ curl -svI https://host.example.net/
    * Host host.example.net:443 was resolved.
    * IPv6: (none)
    * IPv4: 192.0.2.40
    *   Trying 192.0.2.40:443...
    * Connected to host.example.net (192.0.2.40) port 443
    * ALPN: curl offers h2,http/1.1
    ##### snipped #####
    * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
    * Server certificate:
    *  subject: CN=host.example.net
    *  subjectAltName: host "host.example.net" matched cert's "host.example.net"
    *  issuer: CN=host.example.net
    *  SSL certificate verify ok.
    ##### snipped #####
    < HTTP/1.1 200 OK
    < Date: Sat, 10 Jan 2026 05:43:44 GMT
    < Server: Apache/2.4.58 (Ubuntu)
    ##### snipped #####