Generate a self-signed TLS certificate (Linux / macOS / Windows)
Create a self-signed TLS certificate and private key with OpenSSL for internal homelab services. Includes Subject Alternative Names so modern browsers accept it.
bash# Works on Linux, macOS, and Windows (with OpenSSL installed)
# Replace the values below with your own hostname, IP, and organisation
openssl req -x509 \
-newkey rsa:4096 \
-sha256 \
-days 3650 \
-nodes \
-keyout server.key \
-out server.crt \
-subj "/CN=homelab.lan/O=Homelab/C=US" \
-addext "subjectAltName=DNS:homelab.lan,DNS:*.homelab.lan,IP:192.168.1.1"
What this does
Generates two files in the current directory:
server.key— the RSA private key (keep this secret)server.crt— the self-signed X.509 certificate (valid 10 years)
The -addext subjectAltName flag is critical — modern browsers (Chrome 58+, Firefox, Safari) reject certificates that only have a CN and no Subject Alternative Names (SAN). Without a SAN, you will get a NET::ERR_CERT_COMMON_NAME_INVALID error regardless of trusting the cert.
Why this approach
Internal homelab services accessed over HTTP send all their traffic in plain text — login credentials, API tokens, session cookies — visible to anyone on the same network. For services you only access from your own machines on a trusted LAN, this risk is manageable. The moment you access them over Tailscale, through a reverse proxy with remote access, or from a guest network, plain HTTP is a real exposure.
A self-signed certificate costs nothing and takes thirty seconds, and it upgrades the connection from observable plaintext to encrypted TLS. The tradeoff is that browsers will warn about the certificate because it’s not signed by a recognized authority — you have to explicitly trust it on each device. For a homelab with a small number of client devices, this is a one-time step per device, not ongoing overhead.
The subjectAltName extension (added with -addext) is not optional despite looking like it might be. Modern browsers (Chrome 58+, all current Firefox and Safari versions) reject certificates with only a CN field and no SAN, producing a NET::ERR_CERT_COMMON_NAME_INVALID error even after you’ve explicitly trusted the certificate. Many guides online were written before this requirement and still show certificate generation without a SAN — those certs don’t work in current browsers regardless of what trust settings you configure.
If you need multiple internal services each to be trusted without per-service manual trust, the right next step is a private CA: generate one CA key and certificate, trust that CA root on all your devices once, then sign each service certificate with it. The Notes section of this playbook covers the outline. For one or two services, per-cert trust is simpler.
Prerequisites
OpenSSL is required on all platforms.
Linux: Usually pre-installed. If not:
sudo apt-get install -y openssl # Ubuntu / Debian
sudo dnf install -y openssl # Fedora / Rocky
macOS: Pre-installed. Verify: openssl version
Windows: Not built-in. Install via one of:
# Option 1: winget
winget install ShiningLight.OpenSSL
# Option 2: Chocolatey
choco install openssl
# Option 3: Git for Windows includes OpenSSL
# Use Git Bash to run the openssl command above
Verify: openssl version (should show 3.x)
Customise for your environment
Edit the -subj and -addext values before running:
| Field | Example | Replace with |
|---|---|---|
CN |
homelab.lan |
Your primary hostname |
O |
Homelab |
Your name or org |
C |
US |
Your 2-letter country code |
DNS:homelab.lan |
homelab.lan |
Hostname(s) that will use this cert |
DNS:*.homelab.lan |
*.homelab.lan |
Wildcard — covers all subdomains |
IP:192.168.1.1 |
192.168.1.1 |
IP address of the service |
Multiple SANs — add as many as needed, comma-separated:
-addext "subjectAltName=DNS:proxmox.homelab.lan,DNS:grafana.homelab.lan,IP:192.168.1.10,IP:192.168.1.68"
Inspect the certificate
# View the certificate contents (confirm SANs are present)
openssl x509 -in server.crt -text -noout | grep -A5 "Subject Alternative"
# Show expiry date
openssl x509 -in server.crt -noout -enddate
# Verify the key matches the certificate
openssl x509 -noout -modulus -in server.crt | openssl md5
openssl rsa -noout -modulus -in server.key | openssl md5
# Both lines must produce the same hash
Trust the certificate on each client
Self-signed certs will show a browser warning until you add them to your system’s trust store.
Linux (Ubuntu / Debian):
sudo cp server.crt /usr/local/share/ca-certificates/homelab.crt
sudo update-ca-certificates
Linux (Fedora / Rocky / openSUSE):
sudo cp server.crt /etc/pki/ca-trust/source/anchors/homelab.crt
sudo update-ca-trust
macOS:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain server.crt
Windows (PowerShell, run as Administrator):
Import-Certificate -FilePath "server.crt" `
-CertStoreLocation Cert:\LocalMachine\Root
After adding to the trust store, restart your browser.
Install on common homelab services
Nginx:
server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
}
Proxmox (replaces the default self-signed cert):
cp server.crt /etc/pve/local/pve-ssl.pem
cp server.key /etc/pve/local/pve-ssl.key
systemctl restart pveproxy
Docker / Portainer: mount server.crt and server.key into the container and point the service’s TLS config at them.
Notes
-nodesmeans “no DES” — the private key is not encrypted with a passphrase. For a service certificate this is normal (the service needs to read the key at startup without human input). Store the key file withchmod 600and restrict access- The 10-year validity (
-days 3650) is practical for homelab use; production certificates should use 1 year or less - For a proper internal CA (sign multiple service certs from one trusted root), generate a CA key/cert separately and use
openssl cato sign — one root cert to trust across all clients instead of trusting each service cert individually