Self-Signed Certificate #
When we build a web application, enabling HTTPS in the local development environment is crucial to simulate production conditions as accurately as possible. Many modern web features — such as Service Workers, the Web Cryptography API, Geolocation, HTTP/2, and cookies with the Secure and SameSite=None attributes — require an encrypted (HTTPS) connection and will refuse to run over plain HTTP.
For local environments, we don’t need to buy a commercial SSL certificate or use Let’s Encrypt, which requires an active public domain name. The practical solution is to use a Self-Signed Certificate. In this article, we’ll discuss in depth when to use a self-signed certificate, how to create one using OpenSSL (complete with modern SAN configuration), how to apply it in Nginx, how to handle browser security warnings on various operating systems, and the best alternative using the modern mkcert tool.
When to Use a Self-Signed Certificate? #
Before we get into the practical steps, we must understand the limits of using self-signed certificates. Functionally, the encryption produced by a self-signed certificate is just as strong as paid certificates or Let’s Encrypt, because it uses the same cryptographic algorithms. The main difference lies in the trust aspect.
| Scenario | Self-Signed Allowed? | Reason & Alternative |
|---|---|---|
| Local Development (localhost / myapp.local) | Yes | Controlled environment; we can force our own system to trust the certificate. |
| Internal Server-to-Server Connections (Microservices) | Yes | Server-to-server within a Virtual Private Cloud (VPC) closed off from the public. |
| Staging/UAT Environment (Internal Team Access) | Yes | As long as the whole team registers the certificate in each of their trust stores. |
| Production Website (Public Access) | No | Users’ browsers will show a scary red warning page. Users will think our site was hacked. Use Let’s Encrypt or a public CA. |
The Common Name (CN) Limitation and the Subject Alternative Name (SAN) Requirement #
In the past, when creating a self-signed certificate, you could simply fill in the Common Name (CN) field with your domain name (e.g., localhost or myapp.local). However, this method is outdated and no longer supported by modern browsers like Google Chrome (since version 58) and Apple Safari.
Modern browsers now require the use of Subject Alternative Name (SAN). If you create a certificate without the SAN extension, browsers will still show the ERR_CERT_COMMON_NAME_INVALID error even if you’ve already imported the certificate into the system trust store. Therefore, all of our certificate creation scripts below will explicitly include SAN configuration.
Guide to Creating a Self-Signed Certificate with OpenSSL #
OpenSSL is a versatile command-line tool installed on almost all Unix-based operating systems (Linux and macOS). We’ll use OpenSSL to create a new certificate.
Step 1: Preparing the Directory Structure #
First, let’s create a dedicated directory on the Nginx server to securely store the SSL keys and certificates:
# Create the ssl directory inside the Nginx configuration
sudo mkdir -p /etc/nginx/ssl
# Navigate into that directory
cd /etc/nginx/ssl
Step 2: Creating the OpenSSL Configuration File for SAN #
To make our certificate support SAN, we need to create a temporary configuration file (let’s call it openssl-san.cnf). This file tells OpenSSL which domains and IPs are allowed to use this certificate.
# Create the configuration file using cat
cat <<EOF | sudo tee /etc/nginx/ssl/openssl-san.cnf
[req]
default_bits = 2048
default_keyfile = myapp.key
distinguished_name = req_distinguished_name
req_extensions = v3_req
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = ID
ST = Jakarta
L = South Jakarta
O = Badri Creative Tech
OU = Development Department
CN = myapp.local
[v3_req]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = myapp.local
DNS.3 = *.myapp.local
IP.1 = 127.0.0.1
IP.2 = 192.168.1.100
EOF
In the configuration above:
CN = myapp.local: Our certificate’s common name.[alt_names]: Defines additional domains (localhost,myapp.local, the wildcard*.myapp.localfor subdomains) as well as local IP addresses (127.0.0.1and our server’s LAN IP192.168.1.100). Browsers validate the URL against this list.
Step 3: Generating the Private Key and Certificate #
Now, we run the OpenSSL command to generate the Private Key (secret key) and Certificate (public certificate) at once, based on the SAN configuration file we created:
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/myapp.key \
-out /etc/nginx/ssl/myapp.crt \
-config /etc/nginx/ssl/openssl-san.cnf
Explanation of the command parameters above:
req -x509: Requests the direct creation of an X.509 type certificate (self-signed), not a Certificate Signing Request (CSR) that’s normally sent to an external CA.-nodes: Short for No DES. This parameter ensures the private key isn’t encrypted with a passphrase. If the private key were encrypted, Nginx would ask us for the password every time the server is started or reloaded, which would break server automation.-days 365: Certificate validity of one year (365 days).-newkey rsa:2048: Creates a new private key with the 2048-bit RSA algorithm.-keyout: Custom private key file location (myapp.key).-out: Custom certificate file location (myapp.crt).-config: Points to the SAN configuration file we created in Step 2.
Step 4: Setting File Access Permissions (Hardening) #
The private key is highly sensitive data. If an outside party manages to copy our private key, they can decrypt all data traffic. Let’s restrict its access so only the root user and the Nginx service can read it:
# The private key can only be read & written by its owner (root)
sudo chmod 600 /etc/nginx/ssl/myapp.key
# The public certificate can be read by the public/Nginx
sudo chmod 644 /etc/nginx/ssl/myapp.crt
Configuring Nginx to Use the Self-Signed Certificate #
After both certificate files (myapp.crt and myapp.key) are ready, the next step is configuring Nginx to listen on the HTTPS port (443) and load those files.
Let’s create a new virtual host configuration file at /etc/nginx/conf.d/myapp.conf:
# Server Block for HTTPS
server {
listen 443 ssl;
server_name myapp.local www.myapp.local;
# Specify the certificate and private key file locations
ssl_certificate /etc/nginx/ssl/myapp.crt;
ssl_certificate_key /etc/nginx/ssl/myapp.key;
# Allowed SSL/TLS protocols (Use modern TLS)
ssl_protocols TLSv1.2 TLSv1.3;
# Secure cipher suites (Mozilla Intermediate Profile)
ssl_ciphers 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;
ssl_prefer_server_ciphers off;
# Application root location
root /var/www/myapp;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# Logging
access_log /var/log/nginx/myapp_ssl_access.log;
error_log /var/log/nginx/myapp_ssl_error.log;
}
# Server Block for HTTP to HTTPS Redirect
server {
listen 80;
server_name myapp.local www.myapp.local;
return 301 https://$host$request_uri;
}
Before we reload Nginx, make sure the root directory /var/www/myapp and a simple HTML file already exist for testing purposes:
sudo mkdir -p /var/www/myapp
echo "<h1>Hello from Local HTTPS Nginx!</h1>" | sudo tee /var/www/myapp/index.html
# Test the Nginx configuration to make sure there are no syntax errors
sudo nginx -t
# If successful, reload the Nginx configuration
sudo systemctl reload nginx
How to Add the Certificate to OS & Browser Trust Stores #
When we try to access https://myapp.local through a browser, we’ll be greeted by a security warning page stating the certificate isn’t trusted.
To permanently remove this warning on our local computer, we must tell the operating system or browser that we trust the self-signed certificate we created.
flowchart TD
A["Access https://myapp.local"] --> B{"Browser Checks Certificate"}
B -->|Not from a Trusted CA| C["Show Security Warning (Red)"]
C -->|Manual Solution| D["Import myapp.crt into the OS/Browser Trust Store"]
D --> E["Browser Verifies the Local Trust Chain"]
E --> F["Website Opens with a Green / Secure Padlock"]
classDef danger fill:#ef4444,stroke:#dc2626,color:#ffffff;
classDef success fill:#10b981,stroke:#059669,color:#ffffff;
class C danger;
class F success;Here are the steps to register the myapp.crt certificate on various operating systems and browsers:
1. macOS (Via Terminal / Keychain Access) #
On macOS, we can import the certificate directly into the system keychain using the command line:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain /etc/nginx/ssl/myapp.crt
Or visually:
- Open the Keychain Access app.
- Select the System category in the left sidebar, then choose the Certificates tab.
- Drag and drop the
myapp.crtfile into the Keychain Access window. - Double-click the newly imported certificate, then open the Trust section.
- Change the “When using this certificate” option to Always Trust.
2. Linux (Ubuntu / Debian) #
For local development Linux systems, run the following command to copy the certificate into the system’s trusted certificate directory:
# Copy the certificate file with the .crt extension
sudo cp /etc/nginx/ssl/myapp.crt /usr/local/share/ca-certificates/myapp.crt
# Update the system certificate database
sudo update-ca-certificates
3. Windows (Via PowerShell / GUI) #
Open PowerShell as Administrator and run the command:
Import-Certificate -FilePath "C:\path\to\myapp.crt" -CertStoreLocation Cert:\LocalMachine\Root
Or visually:
- Double-click the
myapp.crtfile. - Click Install Certificate…
- Select Local Machine as the Store Location.
- Select the Place all certificates in the following store option and click Browse.
- Choose the Trusted Root Certification Authorities folder.
- Click Next and Finish.
4. Mozilla Firefox Browser #
Mozilla Firefox doesn’t use the operating system’s built-in trust store; it manages its own trusted certificate database.
- Open Firefox and go to the Settings menu.
- Search for “Certificates” in the search box, then click the View Certificates… button.
- In the Authorities tab, click the Import… button.
- Select our
myapp.crtfile. - Check the “Trust this CA to identify websites” option.
- Click OK.
The Best Alternative for Local Development: mkcert #
Although creating certificates with OpenSSL is great for understanding the basic concepts, the process of creating SAN configurations, managing expiry dates, and manually importing certificates into various trust stores is quite tedious.
The modern industry standard for local development today is mkcert.
mkcert is a simple tool that automatically creates a custom local Certificate Authority (CA) on our machine, registers it to the operating system trust store (macOS, Windows, Linux) as well as browser trust stores (Firefox, Chrome, Safari) automatically, then generates trusted local certificates without any security warnings at all.
flowchart TD
A["Install mkcert"] --> B["Run 'mkcert -install'"]
B --> C["mkcert Creates a Custom Local Root CA"]
C --> D["Local Root CA Automatically Added to OS & Browser Trust Stores"]
D --> E["Run 'mkcert localhost myapp.local'"]
E --> F["Generates Trusted Certificate & Key without Warnings"]
classDef step fill:#1e293b,stroke:#3b82f6,color:#ffffff;
class A,B,C,D,E,F step;How to Install and Use mkcert #
Here’s the installation and usage flow for mkcert on various operating systems:
1. macOS (Using Homebrew) #
brew install mkcert
brew install nss # Needed if we use Firefox
2. Ubuntu / Debian #
sudo apt update
sudo apt install mkcert libnss3-tools -y
3. Windows (Using Chocolatey) #
choco install mkcert
Steps to Use mkcert #
Once installed, follow these steps to integrate it with Nginx:
Step 1: Initialize the Local Root CA Run this command once on our computer. It creates a local Root CA and adds it to our OS and browser security systems:
mkcert -install
Success output: “The local CA is now installed in the system trust store! ⚡”
Step 2: Generate a Certificate for Our Domains
Now we can create a certificate for any local domain name we want. For example localhost, myapp.local, and wildcards:
# Create the Nginx SSL directory if it doesn't exist yet
sudo mkdir -p /etc/nginx/ssl
# Create a certificate using mkcert
sudo mkcert -cert-file /etc/nginx/ssl/mkcert.crt \
-key-file /etc/nginx/ssl/mkcert.key \
localhost myapp.local "*.myapp.local" 127.0.0.1
The command above produces two files in /etc/nginx/ssl/:
mkcert.crt(Certificate)mkcert.key(Private Key)
This certificate is automatically trusted by our system because it’s signed by the custom Root CA registered in Step 1.
Step 3: Adjust the Nginx Configuration Now, update our Nginx server block configuration file to point to the new files:
server {
listen 443 ssl;
server_name myapp.local www.myapp.local;
ssl_certificate /etc/nginx/ssl/mkcert.crt;
ssl_certificate_key /etc/nginx/ssl/mkcert.key;
# ... the rest of the configuration stays the same
}
Run the configuration test and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Open a browser and access https://myapp.local. We’ll see a clean green/gray locked padlock icon with no security warning messages at all!
Troubleshooting Table for Local SSL Errors #
When working with self-signed certificates or mkcert in Nginx, we might run into issues. Here’s the handling guide:
| Error Type | Likely Cause | How to Fix |
|---|---|---|
ERR_CERT_COMMON_NAME_INVALID | The certificate was created using only the Common Name (CN) without Subject Alternative Name (SAN) configuration. Modern browsers reject it. | Recreate the certificate using the OpenSSL SAN guide above, or use mkcert. |
ERR_CERT_AUTHORITY_INVALID or SEC_ERROR_UNKNOWN_ISSUER | The certificate is correctly installed in Nginx, but the root certificate hasn’t been imported or trusted in our OS/browser trust store. | Follow the “How to Add the Certificate to OS & Browser Trust Stores” section above to import our .crt file. |
curl: (60) SSL certificate problem: self signed certificate | The curl CLI utility rejects the connection because it doesn’t trust our custom server certificate. | 1. Use the -k or --insecure flag to ignore security (only for quick testing): curl -k https://localhost2. Or register the certificate in the operating system trust store so curl trusts it permanently. |
Nginx Error: KEY_VALUES_MISMATCH on reload | The certificate file (myapp.crt) and private key file (myapp.key) don’t match each other (maybe one was recreated separately). | Verify the md5 hash match of the modulus of both files:`openssl x509 -noout -modulus -in myapp.crt |
| “Certificate Expired” Warning in the Browser | The validity period of our self-signed certificate has ended (exceeding the -days number at creation). | Rerun the OpenSSL certificate creation command or run mkcert to renew the certificate’s validity. |
Summary #
- SAN is mandatory: Modern browsers like Chrome and Safari reject SSL certificates that only use the Common Name (CN). Subject Alternative Name (SAN) configuration is a must.
- Secure the Private Key: Always use the
chmod 600command on the custom private key file so other users on the server system can’t read it.- Use
-nodesfor Automation: Don’t encrypt the private key with a password when creating certificates for Nginx, so Nginx can start automatically without manual intervention.mkcertis the New Standard: Usemkcertfor day-to-day local development to avoid the complexity of manual OpenSSL configuration and the certificate import process into systems/browsers.