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:
- How to Install and Set up PHP and Apache (LAMP stack) on Ubuntu 22.04
- How to Secure Nginx with Let’s Encrypt on Rocky Linux/Alma Linux 9
- How to create a Kubernetes TLS/SSL Secret
Table of Content
- Install the required packages
- Generate and self sign an ssl certificate
- Configure Apache to use self signed certificates
- Verify the certificate
- 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:
-x509outputs a self-signed certificate instead of a certificate signing request.-noencleaves 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 365makes it valid for a year.-subjfills in the organization details non-interactively. Leave it out andopensslwill prompt you for each field.-addext "subjectAltName=..."adds the SAN. UseDNS:for names andIP:for addresses, comma separated.-addextneeds 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_INVALIDafter trusting the certificate: it has no SAN, or the name in the address bar is not in it. Check withopenssl x509 -noout -ext subjectAltNameand 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(orsudo 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_headersis not enabled; runsudo a2enmod headers.