When developing web applications or APIs locally or on a staging server, you often need HTTPS to test features like Service Workers, Geolocation API, Clipboard API, WebCrypto, or mixed-content policies. Obtaining a real SSL certificate at this stage is overkill — a self-signed certificate generated with OpenSSL is the fastest, free alternative. This guide walks through every step: installing OpenSSL, understanding certificate file types, generating keys and certificates with Subject Alternative Names (SAN), configuring Apache and Nginx, and adding the certificate to your system trust store so the browser warning disappears entirely.

What Is a Self-Signed SSL Certificate and When Should You Use It?

A self-signed SSL certificate is an X.509 certificate that you generate and sign yourself using OpenSSL rather than submitting a Certificate Signing Request (CSR) to a trusted Certificate Authority (CA) like DigiCert, Sectigo, or Let's Encrypt. Because browsers don't recognise the issuing authority, they display security warnings such as "Your connection is not private" or NET::ERR_CERT_AUTHORITY_INVALID.

Despite this limitation, self-signed certificates are the right tool in several scenarios:

Important: Never use a self-signed certificate on a public-facing production server. Every major browser will block visitors with a full-page warning before they can proceed, severely damaging trust and conversion rates.

Understanding Certificate File Types

Before generating certificates, it helps to understand what each file type is used for:

File / Extension Description Used by
server.key Private Key — keep this secret, never share Apache / Nginx web server
server.csr Certificate Signing Request — sent to a CA (or self-signed) CA submission or self-signing
server.crt Certificate in PEM format — the public certificate Web server + trust store import
server.pem PEM format — identical to .crt, some tools require this extension Linux CA store, Docker, Node.js
server.pfx / .p12 PKCS#12 — bundles key + certificate in a single binary file Windows IIS, .NET, Java KeyStore

For Linux-based web servers running Apache or Nginx, you will primarily work with .key (private key) and .crt (certificate) files in PEM format — a Base64-encoded text format that begins with -----BEGIN CERTIFICATE-----.

Verify OpenSSL and Prepare Your Environment

OpenSSL comes pre-installed on most Linux distributions and macOS. Windows users must install it separately. Confirm availability with:

openssl version

Expected output:

OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)

If OpenSSL is not installed, use the appropriate package manager:

# Ubuntu / Debian
sudo apt-get update && sudo apt-get install -y openssl

# CentOS / RHEL / Rocky Linux
sudo dnf install -y openssl

# macOS via Homebrew
brew install openssl

# Windows: download Win32/Win64 OpenSSL from https://slproweb.com/products/Win32OpenSSL.html
# Or use WSL (Windows Subsystem for Linux) to run Linux commands directly.

Create a working directory for your certificate files:

mkdir -p ~/ssl-certs && cd ~/ssl-certs

Method 1 — Quick One-Command Certificate

The fastest approach combines key generation and certificate creation in a single command. This is ideal for a quick dev environment setup:

openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \
  -days 365 -nodes \
  -subj "/C=TH/ST=Bangkok/L=Bangkok/O=MyCompany Dev/OU=Development/CN=localhost"

Parameter breakdown:

After running, you will have two files: server.key (private key) and server.crt (certificate), both ready to configure in a web server.

Method 2 — Certificate with Subject Alternative Names (SAN)

Modern browsers require a certificate to include Subject Alternative Names (SAN) or they will show an extra warning — "Subject Alternative Name Missing" — even if you have already accepted the certificate. The following approach creates a certificate covering multiple hostnames and IP addresses using a configuration file:

# Create SAN configuration file
cat > san.cnf << 'EOF'
[req]
default_bits       = 4096
prompt             = no
default_md         = sha256
distinguished_name = dn
x509_extensions    = v3_req

[dn]
C  = TH
ST = Bangkok
L  = Bangkok
O  = MyCompany Development
CN = localhost

[v3_req]
subjectAltName = @alt_names
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[alt_names]
DNS.1 = localhost
DNS.2 = *.localhost
DNS.3 = dev.local
DNS.4 = *.dev.local
DNS.5 = staging.myapp.com
IP.1  = 127.0.0.1
IP.2  = 192.168.1.100
EOF

# Generate certificate from config
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \
  -days 365 -nodes -config san.cnf

This single certificate covers localhost, *.localhost, dev.local, *.dev.local, staging.myapp.com, and the specified IP addresses — eliminating the need to generate a separate certificate for each hostname in your dev environment.

Method 3 — Step-by-Step Certificate Generation

For greater control or when you need to retain the CSR for submission to an internal CA, split the process into three explicit steps:

Step 1: Generate a Private Key

# RSA 4096-bit (recommended)
openssl genrsa -out server.key 4096

# Alternative: ECDSA P-256 (faster key generation and TLS handshake)
openssl ecparam -genkey -name prime256v1 -out server.key

Step 2: Create a CSR

openssl req -new -key server.key -out server.csr \
  -subj "/C=TH/ST=Bangkok/L=Bangkok/O=MyOrg/CN=localhost"

Step 3: Self-Sign the CSR

openssl x509 -req -in server.csr -signkey server.key \
  -out server.crt -days 365 \
  -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1")

Keeping the CSR (server.csr) is useful if you later decide to have it signed by your organisation's internal CA rather than self-signing.

Verify Your Certificate

Always inspect the generated certificate before deploying it to confirm the details are correct:

# View full certificate details
openssl x509 -in server.crt -text -noout

# View subject, issuer, and validity dates only
openssl x509 -in server.crt -subject -issuer -dates -noout

# Confirm private key matches certificate (MD5 hashes must be identical)
openssl x509 -noout -modulus -in server.crt | md5sum
openssl rsa -noout -modulus -in server.key | md5sum

# Confirm SAN entries
openssl x509 -in server.crt -text -noout | grep -A2 "Subject Alternative Name"

Key fields to verify in the output:

Configure Your Web Server

Copy the certificate files to a standard system location, then configure your web server to use them. The examples below assume certificates are stored in /etc/ssl/self-signed/:

# Copy certificate files
sudo mkdir -p /etc/ssl/self-signed
sudo cp server.key /etc/ssl/self-signed/server.key
sudo cp server.crt /etc/ssl/self-signed/server.crt
sudo chmod 600 /etc/ssl/self-signed/server.key
sudo chmod 644 /etc/ssl/self-signed/server.crt

Nginx Configuration

server {
    listen 80;
    server_name localhost dev.local;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name localhost dev.local;

    ssl_certificate     /etc/ssl/self-signed/server.crt;
    ssl_certificate_key /etc/ssl/self-signed/server.key;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    root  /var/www/html;
    index index.html index.php;

    location / {
        try_files $uri $uri/ =404;
    }
}

Apache Configuration

<VirtualHost *:443>
    ServerName localhost
    ServerAlias dev.local

    SSLEngine on
    SSLCertificateFile    /etc/ssl/self-signed/server.crt
    SSLCertificateKeyFile /etc/ssl/self-signed/server.key

    SSLProtocol           all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite        HIGH:!aNULL:!MD5

    DocumentRoot /var/www/html
    <Directory /var/www/html>
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

<VirtualHost *:80>
    ServerName localhost
    Redirect permanent / https://localhost/
</VirtualHost>

Test and reload your web server after making changes:

# Nginx
sudo nginx -t && sudo systemctl reload nginx

# Apache
sudo apachectl configtest && sudo systemctl reload apache2

Adding the Certificate to Your System Trust Store

To eliminate browser warnings entirely in your dev environment, import the certificate into your operating system's trust store. This must be done on each developer machine individually.

macOS

# Via command line
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain server.crt

# Via Keychain Access GUI:
# 1. Double-click server.crt to open Keychain Access
# 2. Find the certificate entry — double-click it
# 3. Expand Trust → "When using this certificate" → Always Trust

Linux — Ubuntu / Debian

sudo cp server.crt /usr/local/share/ca-certificates/dev-localhost.crt
sudo update-ca-certificates
# Output: 1 added, 0 removed; done.

Windows

# PowerShell (run as Administrator)
Import-Certificate -FilePath "C:\path\to\server.crt" `
  -CertStoreLocation Cert:\LocalMachine\Root

# Or via GUI:
# 1. Double-click server.crt → Install Certificate
# 2. Store Location: Local Machine → Next
# 3. Place all certificates in: Trusted Root Certification Authorities → OK → Finish

Pro tip — mkcert for teams: If your team generates self-signed certificates frequently, consider mkcert (github.com/FiloSottile/mkcert). It creates a local CA automatically and installs it into the system trust store of every OS with a single command: mkcert -install. Then generate a trusted certificate with mkcert localhost 127.0.0.1 ::1 — no OpenSSL configuration files required. Distribute the CA certificate (rootCA.pem) to all team members once, and every subsequent certificate generated by mkcert will be trusted automatically.

Using Self-Signed SSL with Docker and Node.js

Self-signed certificates are frequently needed in containerised development environments. Here are the most common patterns:

Node.js HTTPS Server

const https = require('https');
const fs    = require('fs');

const options = {
  key:  fs.readFileSync('/path/to/server.key'),
  cert: fs.readFileSync('/path/to/server.crt'),
};

https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('Hello HTTPS\n');
}).listen(443, () => console.log('HTTPS server running on :443'));

Docker Compose with Nginx + SSL

version: '3.8'
services:
  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./ssl/server.crt:/etc/ssl/certs/server.crt:ro
      - ./ssl/server.key:/etc/ssl/private/server.key:ro
      - ./html:/usr/share/nginx/html:ro

Disabling SSL Verification in Development Tools

Some tools reject self-signed certificates by default. Use these flags in development only — always re-enable certificate verification in production environments:

# curl
curl -k https://localhost/api/health

# wget
wget --no-check-certificate https://localhost/file.zip

# Python requests
import requests
response = requests.get('https://localhost', verify=False)

# Node.js environment variable
export NODE_TLS_REJECT_UNAUTHORIZED=0

# Git (set per-repository, not globally)
git -c http.sslVerify=false clone https://localhost/repo.git

Creating a Private Certificate Authority for Your Team

If your team frequently works with internal HTTPS services, a better long-term solution is to create a private Root CA, distribute its certificate to every team member once, and then sign all server certificates with it. New certificates will be automatically trusted without any additional imports.

# Step 1: Create the Root CA private key
openssl genrsa -out ca.key 4096

# Step 2: Create the Root CA certificate (10-year validity)
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -out ca.crt \
  -subj "/C=TH/ST=Bangkok/O=MyCompany Dev CA/CN=MyCompany Development Root CA"

# Step 3: Create the server private key
openssl genrsa -out server.key 4096

# Step 4: Create the server CSR
openssl req -new -key server.key -out server.csr \
  -subj "/C=TH/ST=Bangkok/O=MyCompany/CN=dev.mycompany.local"

# Step 5: Sign the server CSR with the CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt -days 365 -sha256 \
  -extfile <(printf "subjectAltName=DNS:dev.mycompany.local,DNS:*.dev.mycompany.local,IP:192.168.1.100")

Distribute ca.crt to every team member to install in their trust store once. All server certificates subsequently signed by this CA will be trusted automatically — no additional per-certificate imports needed.

Renewing Self-Signed Certificates

Unlike CA-issued certificates, self-signed certificates have no renewal process — you simply generate a new one when the old one expires or when subject information changes. Check expiry with:

# Check expiry date
openssl x509 -in server.crt -noout -enddate
# notAfter=Jun  9 00:00:00 2027 GMT

# Regenerate using saved config file
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \
  -days 365 -nodes -config san.cnf

# Reload web server after renewal
sudo systemctl reload nginx

For automated environments, add this command to a cron job that runs 30 days before the certificate expires, followed by a web server reload.

Frequently Asked Questions

What is the difference between a self-signed SSL and a CA-issued certificate?

A self-signed SSL certificate is generated and signed by you using OpenSSL — no Certificate Authority is involved. Browsers do not recognise the issuing authority, so they display "Your connection is not private" warnings. CA-issued certificates (from DigiCert, Let's Encrypt, etc.) are trusted by browsers worldwide. Use self-signed certificates only for development, staging, or internal tools where you control who accesses the service.

Can I use a self-signed SSL on a production server?

It is strongly discouraged. Every major browser — Chrome, Firefox, Safari, and Edge — will display a full-page "Not Secure" warning before visitors can reach your site. This destroys user trust and conversion rates. API clients also reject self-signed certificates by default unless you explicitly disable verification. For production, use a trusted CA certificate or the free Let's Encrypt service.

How do I make my browser trust a self-signed SSL certificate?

Import the .crt file into your operating system's trust store. On macOS, use Keychain Access or sudo security add-trusted-cert. On Windows, use certmgr.msc → Trusted Root Certification Authorities → Import. On Linux (Ubuntu/Debian), copy to /usr/local/share/ca-certificates/ and run sudo update-ca-certificates. This works only on machines you control — it cannot make external users trust the certificate.

How long is a self-signed SSL certificate valid?

You set the validity period yourself with the -days parameter. Common values are 365 (one year) or 3650 (ten years) for dev environments. However, modern browsers — especially Safari on iOS and macOS — enforce a maximum certificate lifetime of approximately 825 days. Certificates with longer validity periods may trigger additional security warnings. As a best practice, regenerate your certificate annually or whenever the Subject information changes.

SSL Certificates for Every Use Case at AsiaGB

DV, OV, EV, and Wildcard SSL certificates starting from 1,000 THB/year with full installation support included.

View SSL Certificates