Create a Self-Signed SSL Certificate for Apache With SAN

Create a self-signed SSL certificate for Apache with openssl and a subjectAltName, enable HTTPS on Debian/Ubuntu or RHEL, then verify and trust it with curl.

The Transport Layer Security(TLS) and its predecessor SSL(secure socket layer) are web protocols that are used to wrap normal web traffic in a protected, encrypted wrapper.

Signing your own SSL certificates is usually done as an easy alternative to certificate authorities for internal communications or non-user facing sites that still need encryption. Here’s how to set one up with Apache on Debian/Ubuntu and on RHEL-family systems (Rocky Linux, AlmaLinux, RHEL).

The commands below were tested on Debian 12 (Apache 2.4, OpenSSL 3.0) and AlmaLinux 9 (Apache 2.4, OpenSSL 3.5). For a public site, use a free Let’s Encrypt certificate instead; a self-signed certificate will always show a browser warning to visitors who have not trusted it.

Related content:

Table of Content

  1. Install the required packages
  2. Generate and self sign an ssl certificate
  3. Configure Apache to use self signed certificates
  4. Verify the certificate
  5. Trust the certificate on your clients

1. Install required packages

Before proceeding, ensure your OS is up to date. For Debian based systems like Ubuntu

sudo apt update
sudo apt upgrade -y

We’ll use the openssl utility to generate the certificate and corresponding private key, and Apache to serve it. Use this command to install them if they’re not present:

For Ubuntu/Debian

sudo apt install -y openssl apache2

For Rocky Linux/AlmaLinux/RHEL, Apache is httpd and TLS support is a separate mod_ssl package:

sudo dnf install -y openssl httpd mod_ssl

2. Generate and Self Sign an SSL Certificate

Modern browsers ignore the certificate’s Common Name (CN) and only check the Subject Alternative Name (SAN) extension. A CN-only certificate, which older guides (including the first version of this one) produce, fails in Chrome with NET::ERR_CERT_COMMON_NAME_INVALID even after you trust it. So always add a SAN listing every name and IP clients will use.

Generate the certificate with the following command. Replace secure.example.com with your server’s domain name and the IP with its address:

On Ubuntu/Debian:

sudo openssl req -x509 -noenc -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/apache-selfsigned.key \
  -out /etc/ssl/certs/apache-selfsigned.crt \
  -subj "/C=KE/ST=Nairobi/L=Nairobi/O=Example Ltd/CN=secure.example.com" \
  -addext "subjectAltName=DNS:secure.example.com,DNS:localhost,IP:127.0.0.1"

On Rocky Linux/AlmaLinux/RHEL, certificates live under /etc/pki/tls:

sudo openssl req -x509 -noenc -days 365 -newkey rsa:2048 \
  -keyout /etc/pki/tls/private/apache-selfsigned.key \
  -out /etc/pki/tls/certs/apache-selfsigned.crt \
  -subj "/C=KE/ST=Nairobi/L=Nairobi/O=Example Ltd/CN=secure.example.com" \
  -addext "subjectAltName=DNS:secure.example.com,DNS:localhost,IP:127.0.0.1"
sudo chmod 600 /etc/pki/tls/private/apache-selfsigned.key

What the options do:

  • -x509 outputs a self-signed certificate instead of a certificate signing request.
  • -noenc leaves the private key unencrypted so Apache can start without a passphrase prompt. On OpenSSL 1.1 (for example Ubuntu 20.04) the flag is called -nodes, which OpenSSL 3 still accepts.
  • -days 365 makes it valid for a year.
  • -subj fills in the organization details non-interactively. Leave it out and openssl will prompt you for each field.
  • -addext "subjectAltName=..." adds the SAN. Use DNS: for names and IP: for addresses, comma separated. -addext needs OpenSSL 1.1.1 or later.

openssl will take a second to run and generate a new private RSA key, which is used to sign the certificate, and store it in /etc/ssl/private/apache-selfsigned.key. The certificate itself is stored in /etc/ssl/certs/apache-selfsigned.crt, and will be valid for a year.

Confirm the SAN made it into the certificate:

openssl x509 -in /etc/ssl/certs/apache-selfsigned.crt -noout -subject -enddate -ext subjectAltName
subject=C = KE, ST = Nairobi, L = Nairobi, O = Example Ltd, CN = secure.example.com
notAfter=Nov 10 08:24:38 2027 GMT
X509v3 Subject Alternative Name:
    DNS:secure.example.com, DNS:localhost, IP Address:127.0.0.1

If the last two lines are missing, browsers will reject the certificate. Regenerate it with -addext.

Next let’s generate a Diffie-Hellman group. This is only used by the DHE cipher suites; modern clients negotiate ECDHE (or TLS 1.3, which does not use this file at all), so it is optional. If you keep the DHE ciphers in the configuration below, a 2048-bit group is the current recommendation and takes a second or two to generate, where 4096 bits can take minutes. This requires Apache 2.4.8 or higher.

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

3. Configure Apache to Use Your Self-Signed Certificate

On Ubuntu/Debian

First, we will create an Apache configuration snippet to define some SSL settings. This will set Apache up with a strong SSL cipher suite and enable some security headers. The parameters we will set can be used by any Virtual Hosts enabling SSL.

Create a new snippet in the /etc/apache2/conf-available directory. We will name the file ssl-params.conf to make its purpose clear:

sudo nano /etc/apache2/conf-available/ssl-params.conf

Open it up in your favorite text editor, and paste in the following configuration. The protocol and cipher list follow the Mozilla “intermediate” profile: TLS 1.2 and 1.3 only, forward-secret ciphers only:

SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite          ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
SSLHonorCipherOrder     off
SSLSessionTickets       off
SSLOpenSSLConfCmd       DHParameters "/etc/ssl/certs/dhparam.pem"

Header always set X-Frame-Options DENY
Header always set X-Content-Type-Options nosniff

The first block sets the TLS parameters, and the SSLOpenSSLConfCmd line configures Apache to use the Diffie-Hellman group. You can omit this line if you skipped that step. The Header lines need mod_headers, which we enable below.

The first version of this guide also turned on OCSP stapling (SSLUseStapling on). Leave that out for a self-signed certificate: there is no OCSP responder to staple from, so it only adds errors to the log. SSLCompression off is already the default.

Next, open up the site SSL Virtual Host file at /etc/apache2/sites-available/default-ssl.conf. Change the following lines to point to your certificate and key file:

SSLCertificateFile      /etc/ssl/certs/apache-selfsigned.crt
SSLCertificateKeyFile   /etc/ssl/private/apache-selfsigned.key

In the same file, make sure the ServerName, ServerAdmin, and DocumentRoot directives point to the correct values for your site, for example ServerName secure.example.com.

Additionally, you’ll want to set up a redirect to forward HTTP traffic over to the encrypted HTTPS. Do this in the port 80 virtual host, /etc/apache2/sites-available/000-default.conf, inside the <VirtualHost *:80> block:

ServerName secure.example.com
Redirect permanent / https://secure.example.com/

A plain Redirect is simpler than mod_rewrite rules and does not need any extra module.

Now enable the SSL and headers modules, the snippet and the SSL site, check the syntax and restart Apache:

sudo a2enmod ssl headers
sudo a2enconf ssl-params
sudo a2ensite default-ssl
sudo apache2ctl configtest
sudo systemctl restart apache2

apache2ctl configtest must print Syntax OK. A warning such as AH00558: apache2: Could not reliably determine the server's fully qualified domain name is harmless; silence it by adding ServerName secure.example.com to /etc/apache2/apache2.conf.

On Rocky Linux/AlmaLinux/RHEL

mod_ssl ships a ready SSL virtual host in /etc/httpd/conf.d/ssl.conf, and the protocols and ciphers come from the system-wide crypto policy (SSLCipherSuite PROFILE=SYSTEM), which already disables TLS 1.0 and 1.1. You only need to point it at your certificate. Edit /etc/httpd/conf.d/ssl.conf and change these two lines:

SSLCertificateFile /etc/pki/tls/certs/apache-selfsigned.crt
SSLCertificateKeyFile /etc/pki/tls/private/apache-selfsigned.key

Then check the syntax, start Apache and open the HTTPS port in firewalld:

sudo httpd -t
sudo systemctl enable --now httpd
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

On Debian/Ubuntu with ufw enabled, the equivalent is sudo ufw allow 80,443/tcp.

Self-signed certificates are most commonly used for private servers, so it’s not a bad idea to whitelist access to only your private LAN and use a VPN server to connect to it.

4. Verify the Certificate

Test from the server first with curl. Without telling curl about the certificate, it refuses, which proves TLS is being served:

curl https://localhost/
curl: (60) SSL certificate problem: self-signed certificate

Now pass the certificate as the trusted CA. This checks the whole chain and that the SAN matches the hostname:

curl --cacert /etc/ssl/certs/apache-selfsigned.crt -o /dev/null -w '%{http_code}\n' https://localhost/
200

To test the real domain name before DNS points at the server, map it with --resolve:

curl --cacert /etc/ssl/certs/apache-selfsigned.crt \
  --resolve secure.example.com:443:127.0.0.1 https://secure.example.com/

curl -k skips verification entirely. It is fine for a quick “is Apache answering” check, but it proves nothing about the certificate.

openssl s_client shows exactly what Apache serves and the negotiated protocol:

openssl s_client -connect localhost:443 -servername secure.example.com \
  -CAfile /etc/ssl/certs/apache-selfsigned.crt </dev/null 2>/dev/null \
  | grep -E 'subject=|issuer=|^New,|Verify return code'
subject=C = KE, ST = Nairobi, L = Nairobi, O = Example Ltd, CN = secure.example.com
issuer=C = KE, ST = Nairobi, L = Nairobi, O = Example Ltd, CN = secure.example.com
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)

Subject and issuer are identical because the certificate signed itself. Without -CAfile you get Verify return code: 18 (self-signed certificate), which is expected. To confirm old protocols are off, openssl s_client -connect localhost:443 -tls1_1 should fail with tlsv1 alert protocol version.

Finally check the redirect:

curl -sI http://localhost/ | grep -iE '^(HTTP|Location)'
HTTP/1.1 301 Moved Permanently
Location: https://secure.example.com/

5. Trust the Certificate on Your Clients

When you navigate to your site in a browser that does not trust the certificate yet, you should see this warning:

Your connection is not private

This is normal, so don’t worry. Clicking through it works, but it trains users to ignore the warning. A better approach for an internal server is to add the certificate to the trust store of the machines that use it.

Copy apache-selfsigned.crt (never the .key) to the client, then:

On Debian/Ubuntu (the file must end in .crt):

sudo cp apache-selfsigned.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

On Rocky Linux/AlmaLinux/RHEL/Fedora:

sudo cp apache-selfsigned.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

After that, curl https://localhost/ works without --cacert. On macOS, open the certificate in Keychain Access, add it to the System keychain and set it to “Always Trust”. On Windows, import it into “Trusted Root Certification Authorities”. Firefox keeps its own certificate store: import it under Settings, Privacy & Security, Certificates, View Certificates, Authorities.

When to use mkcert instead

If you trust self-signed certificates on several machines, or need certificates for many internal hostnames, consider mkcert. It creates a small local certificate authority, installs it into the system, browser and Java trust stores with mkcert -install, then issues SAN certificates signed by it, for example mkcert secure.example.com localhost 127.0.0.1. You trust the CA once and every certificate it issues afterwards is accepted. Point SSLCertificateFile and SSLCertificateKeyFile at the files it writes. Keep the CA key (mkcert -CAROOT shows where) private: anyone holding it can issue certificates your machines will trust.

Troubleshooting

  • Browser says NET::ERR_CERT_COMMON_NAME_INVALID after trusting the certificate: it has no SAN, or the name in the address bar is not in it. Check with openssl x509 -noout -ext subjectAltName and regenerate with -addext.
  • curl: (60) SSL: no alternative certificate subject name matches target host name: you connected with a name or IP that is not listed in the SAN.
  • Apache fails to start after enabling the site: run sudo apache2ctl configtest (or sudo httpd -t) and read the error; journalctl -u apache2 (or -u httpd) has the details. A typo in a certificate path or a key that does not match the certificate are the usual causes.
  • Invalid command 'Header': mod_headers is not enabled; run sudo a2enmod headers.
Last updated on Nov 10, 2026 12:49 +0300
comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy