You’re setting up a new service in your homelab. Maybe it’s Proxmox, maybe it’s your private Git server, maybe it’s that sketchy Docker container you found on GitHub. Either way, you need HTTPS, and you’re tired of clicking through “Your connection is not private” warnings like it’s 2005.
The solution? Become your own Certificate Authority. It’s easier than it sounds, and by the end of this guide, your browser will trust your self-signed certificates like they came from DigiCert themselves.
The Big Picture: How Certificate Trust Works
A brief detour into PKI land
Before we dive into commands, let’s understand what we’re building:
┌─────────────────────────────────────────────────────────────────┐
│ Your Private CA │
│ (The Root of Trust) │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ CA Certificate │ ← Install this on devices │
│ │ (ca.crt) │ │
│ └─────────────────────┘ │
│ │ │
│ Signs ▼ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Server Cert │ │ Client Cert │ │
│ │ (web, mail) │ │ (mTLS auth) │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The trust chain:
- You create a CA (Certificate Authority) with its own key pair
- You install the CA certificate on all devices that should trust it
- You use the CA to sign server/client certificates
- Devices see a certificate signed by your CA → “I trust this CA” → Connection trusted
Part 1: Creating Your Certificate Authority
With great power comes great responsibility
Step 1: Generate the CA Private Key
1. Create a directory for your CA
mkdir -p ~/myca/{certs,private,newcerts}
cd ~/myca
1. Generate a 4096-bit RSA private key for the CA
openssl genrsa -aes256 -out private/ca.key 4096
You’ll be prompted for a passphrase. Use a strong one and don’t lose it. This key is the crown jewel – anyone with access to it can sign certificates that your devices will trust.
Pro tip: Store this key offline or in a password manager. You only need it when signing new certificates.
Step 2: Create the CA Certificate
1. Generate the CA certificate (valid for 10 years)
openssl req -new -x509 -days 3650 -key private/ca.key -sha256 -out certs/ca.crt
You’ll be prompted for certificate details:
Country Name (2 letter code) [AU]: DE
State or Province Name []: NRW
Locality Name []: Dusseldorf
Organization Name []: Homelab CA
Organizational Unit Name []: IT Department
Common Name []: Homelab Root CA
Email Address []: admin@homelab.local
Fun fact: The Common Name (CN) is what shows up in certificate viewers. Make it descriptive so you recognize it later.
Step 3: Verify Your CA Certificate
1. View the certificate details
openssl x509 -in certs/ca.crt -text -noout
Look for:
IssuerandSubjectshould be identical (it’s self-signed)CA:TRUEin the Basic Constraints- Validity dates that make sense
Part 2: Creating Server Certificates
For your HTTPS-enabled services
Step 1: Generate a Private Key for the Server
1. Generate server private key (no passphrase for automated services)
openssl genrsa -out server.key 2048
Step 2: Create a Certificate Signing Request (CSR)
1. Generate CSR
openssl req -new -key server.key -out server.csr
Fill in the details. The Common Name should match your server’s hostname (e.g., gitea.homelab.local).
Step 3: Create a Config File for SANs
Modern browsers require Subject Alternative Names (SANs). Create a file called server.ext:
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = gitea.homelab.local
DNS.2 = gitea
DNS.3 = *.homelab.local
IP.1 = 192.168.1.100
IP.2 = 127.0.0.1
Step 4: Sign the Certificate with Your CA
1. Sign the server certificate (valid for 1 year)
openssl x509 -req -in server.csr \
-CA ~/myca/certs/ca.crt \
-CAkey ~/myca/private/ca.key \
-CAcreateserial \
-out server.crt \
-days 365 \
-sha256 \
-extfile server.ext
Step 5: Verify the Server Certificate
1. Verify the certificate chain
openssl verify -CAfile ~/myca/certs/ca.crt server.crt
Should output: server.crt: OK
Part 3: Creating Client Certificates
For mutual TLS (mTLS) authentication
Client certificates let you authenticate users/devices instead of (or in addition to) passwords. Perfect for:
- VPN authentication
- API access control
- Zero-trust architectures
- Making your security team happy
Step 1: Generate Client Private Key
1. Generate client key
openssl genrsa -out client.key 2048
Step 2: Create Client CSR
openssl req -new -key client.key -out client.csr
For client certs, the Common Name typically identifies the user or device (e.g., john.doe@company.com or laptop-001).
Step 3: Create Client Extension File
Create client.ext:
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
subjectAltName = @alt_names
[alt_names]
email = john.doe@company.com
Step 4: Sign the Client Certificate
openssl x509 -req -in client.csr \
-CA ~/myca/certs/ca.crt \
-CAkey ~/myca/private/ca.key \
-CAcreateserial \
-out client.crt \
-days 365 \
-sha256 \
-extfile client.ext
Step 5: Create PKCS#12 Bundle (for browsers/devices)
Most devices import client certificates as .p12 or .pfx files:
1. Bundle key + cert + CA into a .p12 file
openssl pkcs12 -export \
-out client.p12 \
-inkey client.key \
-in client.crt \
-certfile ~/myca/certs/ca.crt
You’ll set an export password. The user needs this to import the certificate.
Part 4: Trusting Your CA on Different Platforms
The “make the warnings go away” section
Now comes the fun part: making every device trust your CA.
macOS: Adding CA to System Keychain
Method 1: GUI (Keychain Access)
- Double-click
ca.crtto open Keychain Access - Select “System” keychain (not “login” – we want system-wide trust)
- Find your certificate, double-click it
- Expand “Trust” section
- Set “When using this certificate” to Always Trust
- Close and enter your password
Method 2: Command Line
1. Add CA to System keychain and trust it
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain ca.crt
Verify it worked:
security find-certificate -a -c "Homelab Root CA" /Library/Keychains/System.keychain
To remove later:
sudo security delete-certificate -c "Homelab Root CA" /Library/Keychains/System.keychain
Windows: Adding CA to Certificate Store
Method 1: GUI (Certificate Manager)
- Double-click
ca.crt - Click “Install Certificate…”
- Select “Local Machine” (requires admin)
- Choose “Place all certificates in the following store”
- Click “Browse” and select Trusted Root Certification Authorities
- Finish the wizard
Method 2: PowerShell (Admin)
1. Import CA certificate to Trusted Root store
Import-Certificate -FilePath "C:\path\to\ca.crt" `
-CertStoreLocation Cert:\LocalMachine\Root
Method 3: certutil
certutil -addstore -f "Root" ca.crt
Verify it worked:
Get-ChildItem Cert:\LocalMachine\Root | Where-Object {$_.Subject -like "*Homelab*"}
To remove later:
1. Find the thumbprint first
Get-ChildItem Cert:\LocalMachine\Root | Where-Object {$_.Subject -like "*Homelab*"}
1. Remove by thumbprint
Remove-Item Cert:\LocalMachine\Root\[THUMBPRINT]
Linux: Adding CA to System Trust Store
Linux varies by distribution. Here are the major ones:
Debian/Ubuntu:
1. Copy CA cert to the trusted directory
sudo cp ca.crt /usr/local/share/ca-certificates/homelab-ca.crt
1. Update the CA store
sudo update-ca-certificates
RHEL/CentOS/Fedora:
1. Copy CA cert
sudo cp ca.crt /etc/pki/ca-trust/source/anchors/homelab-ca.crt
1. Update the CA store
sudo update-ca-trust
Arch Linux:
1. Copy CA cert
sudo cp ca.crt /etc/ca-certificates/trust-source/anchors/homelab-ca.crt
1. Update the CA store
sudo trust extract-compat
Verify it worked:
1. Test with curl
curl https://your-server.homelab.local
1. Or check the trust store
trust list | grep -i homelab
To remove later:
1. Debian/Ubuntu
sudo rm /usr/local/share/ca-certificates/homelab-ca.crt
sudo update-ca-certificates --fresh
1. RHEL/CentOS/Fedora
sudo rm /etc/pki/ca-trust/source/anchors/homelab-ca.crt
sudo update-ca-trust
Android: Adding CA to User/System Trust
Method 1: User Trust Store (No Root Required)
- Copy
ca.crtto your device (email, cloud, USB) - Go to Settings → Security → Encryption & credentials
- Tap Install a certificate → CA certificate
- Select your
ca.crtfile - Confirm the security warning
Important: User-installed CAs show a persistent “Network may be monitored” warning. Apps can also choose to ignore user CAs (Android 7+).
Method 2: System Trust Store (Root Required)
1. On a rooted Android device or emulator
1. Convert to Android format (hash-named)
HASH=$(openssl x509 -inform PEM -subject_hash_old -in ca.crt | head -1)
cp ca.crt ${HASH}.0
1. Push to device
adb root
adb remount
adb push ${HASH}.0 /system/etc/security/cacerts/
adb shell chmod 644 /system/etc/security/cacerts/${HASH}.0
adb reboot
For Android 14+ (read-only /system):
1. Mount system as writable (Magisk or similar required)
adb shell
su
mount -o rw,remount /system
1. Then copy the certificate as above
iOS/iPadOS: Adding CA via Profile
iOS requires certificates to be installed via a configuration profile.
Method 1: Direct Installation
- Email the
ca.crtto yourself or host it on a web server - Open the certificate on your iOS device
- Go to Settings → General → VPN & Device Management
- You’ll see your certificate under “Downloaded Profile”
- Tap it and select Install
- Important: Go to Settings → General → About → Certificate Trust Settings
- Enable full trust for your root certificate
Method 2: Apple Configurator / MDM
For enterprise deployment, create a .mobileconfig profile:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadCertificateFileName</key>
<string>homelab-ca.crt</string>
<key>PayloadContent</key>
<data>
[BASE64-ENCODED-CERTIFICATE-HERE]
</data>
<key>PayloadDescription</key>
<string>Adds Homelab Root CA</string>
<key>PayloadDisplayName</key>
<string>Homelab Root CA</string>
<key>PayloadIdentifier</key>
<string>com.homelab.ca</string>
<key>PayloadType</key>
<string>com.apple.security.root</string>
<key>PayloadUUID</key>
<string>A1B2C3D4-E5F6-7890-ABCD-EF1234567890</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>Homelab CA Profile</string>
<key>PayloadIdentifier</key>
<string>com.homelab.ca.profile</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>12345678-90AB-CDEF-1234-567890ABCDEF</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
Generate the base64 content:
base64 -i ca.crt
To remove later: Settings → General → VPN & Device Management → Select profile → Remove Profile
Bonus: Certificate Generator Script
Because automation is the DevOps way
Save this script as generate-cert.sh and never type those OpenSSL commands again:
#!/bin/bash
1.
1. Certificate Generator Script
1. Creates CA, server, and client certificates with minimal effort
1.
1. Usage:
1. ./generate-cert.sh ca # Create new CA
1. ./generate-cert.sh server
<hostname> # Create server cert
1. ./generate-cert.sh client
<name> # Create client cert
1.
set -e
1. Configuration
CA_DIR="${CA_DIR:-$HOME/myca}"
CA_KEY="$CA_DIR/private/ca.key"
CA_CERT="$CA_DIR/certs/ca.crt"
DAYS_CA=3650
DAYS_CERT=365
KEY_SIZE_CA=4096
KEY_SIZE_CERT=2048
1. Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
1. Create CA directory structure
init_ca_dir() {
mkdir -p "$CA_DIR"/{certs,private,newcerts}
chmod 700 "$CA_DIR/private"
touch "$CA_DIR/index.txt"
[ -f "$CA_DIR/serial" ] || echo 1000 > "$CA_DIR/serial"
}
1. Generate new Certificate Authority
generate_ca() {
log_info "Creating new Certificate Authority..."
init_ca_dir
if [ -f "$CA_KEY" ]; then
log_warn "CA already exists at $CA_DIR"
read -p "Overwrite? (y/N) " -n 1 -r
echo
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 0
fi
1. Generate CA private key
log_info "Generating CA private key (you will be prompted for a passphrase)..."
openssl genrsa -aes256 -out "$CA_KEY" $KEY_SIZE_CA
chmod 400 "$CA_KEY"
1. Generate CA certificate
log_info "Generating CA certificate..."
openssl req -new -x509 -days $DAYS_CA \
-key "$CA_KEY" \
-sha256 \
-out "$CA_CERT" \
-subj "/CN=Homelab Root CA/O=Homelab/C=DE"
log_info "CA created successfully!"
log_info "CA Certificate: $CA_CERT"
log_info "CA Private Key: $CA_KEY"
echo
log_warn "IMPORTANT: Back up $CA_KEY securely and remember your passphrase!"
}
1. Generate server certificate
generate_server() {
local hostname="$1"
[ -z "$hostname" ] && log_error "Usage: $0 server
<hostname>"
1. Check CA exists
[ ! -f "$CA_KEY" ] || [ ! -f "$CA_CERT" ] && \
log_error "CA not found. Run '$0 ca' first."
local output_dir="./certs/$hostname"
mkdir -p "$output_dir"
log_info "Generating server certificate for: $hostname"
1. Generate private key
openssl genrsa -out "$output_dir/$hostname.key" $KEY_SIZE_CERT
chmod 600 "$output_dir/$hostname.key"
1. Create extension file for SANs
cat > "$output_dir/$hostname.ext" << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $hostname
DNS.2 = *.$hostname
IP.1 = 127.0.0.1
EOF
1. Generate CSR
openssl req -new \
-key "$output_dir/$hostname.key" \
-out "$output_dir/$hostname.csr" \
-subj "/CN=$hostname/O=Homelab/C=DE"
1. Sign with CA
log_info "Signing certificate with CA (enter CA passphrase)..."
openssl x509 -req \
-in "$output_dir/$hostname.csr" \
-CA "$CA_CERT" \
-CAkey "$CA_KEY" \
-CAcreateserial \
-out "$output_dir/$hostname.crt" \
-days $DAYS_CERT \
-sha256 \
-extfile "$output_dir/$hostname.ext"
1. Create fullchain (cert + CA)
cat "$output_dir/$hostname.crt" "$CA_CERT" > "$output_dir/$hostname.fullchain.crt"
1. Cleanup CSR and ext file
rm -f "$output_dir/$hostname.csr" "$output_dir/$hostname.ext"
log_info "Server certificate created successfully!"
echo
echo "Files created in $output_dir/:"
echo " - $hostname.key (Private key)"
echo " - $hostname.crt (Certificate)"
echo " - $hostname.fullchain.crt (Certificate + CA chain)"
echo
echo "Example nginx config:"
echo " ssl_certificate $output_dir/$hostname.fullchain.crt;"
echo " ssl_certificate_key $output_dir/$hostname.key;"
}
1. Generate client certificate
generate_client() {
local name="$1"
[ -z "$name" ] && log_error "Usage: $0 client
<name>"
1. Check CA exists
[ ! -f "$CA_KEY" ] || [ ! -f "$CA_CERT" ] && \
log_error "CA not found. Run '$0 ca' first."
local output_dir="./certs/clients"
mkdir -p "$output_dir"
log_info "Generating client certificate for: $name"
1. Generate private key
openssl genrsa -out "$output_dir/$name.key" $KEY_SIZE_CERT
chmod 600 "$output_dir/$name.key"
1. Create extension file
cat > "$output_dir/$name.ext" << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
1. Generate CSR
openssl req -new \
-key "$output_dir/$name.key" \
-out "$output_dir/$name.csr" \
-subj "/CN=$name/O=Homelab/C=DE"
1. Sign with CA
log_info "Signing certificate with CA (enter CA passphrase)..."
openssl x509 -req \
-in "$output_dir/$name.csr" \
-CA "$CA_CERT" \
-CAkey "$CA_KEY" \
-CAcreateserial \
-out "$output_dir/$name.crt" \
-days $DAYS_CERT \
-sha256 \
-extfile "$output_dir/$name.ext"
1. Create PKCS#12 bundle
log_info "Creating PKCS#12 bundle (set an export password)..."
openssl pkcs12 -export \
-out "$output_dir/$name.p12" \
-inkey "$output_dir/$name.key" \
-in "$output_dir/$name.crt" \
-certfile "$CA_CERT" \
-name "$name"
1. Cleanup
rm -f "$output_dir/$name.csr" "$output_dir/$name.ext"
log_info "Client certificate created successfully!"
echo
echo "Files created in $output_dir/:"
echo " - $name.key (Private key)"
echo " - $name.crt (Certificate)"
echo " - $name.p12 (PKCS#12 bundle for import)"
}
1. Show certificate info
show_info() {
local cert="$1"
[ -z "$cert" ] && log_error "Usage: $0 info <certificate.crt>"
[ ! -f "$cert" ] && log_error "Certificate not found: $cert"
openssl x509 -in "$cert" -text -noout
}
1. Verify certificate against CA
verify_cert() {
local cert="$1"
[ -z "$cert" ] && log_error "Usage: $0 verify <certificate.crt>"
[ ! -f "$cert" ] && log_error "Certificate not found: $cert"
[ ! -f "$CA_CERT" ] && log_error "CA certificate not found at $CA_CERT"
openssl verify -CAfile "$CA_CERT" "$cert"
}
1. Main
case "${1:-}" in
ca)
generate_ca
;;
server)
generate_server "$2"
;;
client)
generate_client "$2"
;;
info)
show_info "$2"
;;
verify)
verify_cert "$2"
;;
*)
echo "Certificate Generator Script"
echo
echo "Usage: $0
<command> [options]"
echo
echo "Commands:"
echo " ca Create new Certificate Authority"
echo " server
<host> Create server certificate"
echo " client
<name> Create client certificate"
echo " info
<cert> Show certificate details"
echo " verify
<cert> Verify certificate against CA"
echo
echo "Environment variables:"
echo " CA_DIR CA directory (default: ~/myca)"
echo
echo "Examples:"
echo " $0 ca # Create CA"
echo " $0 server gitea.homelab.local"
echo " $0 client john.doe"
echo " $0 verify ./certs/server.crt"
;;
esac
Usage examples:
1. Make executable
chmod +x generate-cert.sh
1. Create your CA (one-time setup)
./generate-cert.sh ca
1. Generate server certificate
./generate-cert.sh server gitea.homelab.local
1. Generate client certificate (creates .p12 for easy import)
./generate-cert.sh client john.doe
1. Verify a certificate
./generate-cert.sh verify ./certs/gitea.homelab.local/gitea.homelab.local.crt
Quick Reference Card
╔══════════════════════════════════════════════════════════════════╗
║ CERTIFICATE AUTHORITY CHEAT SHEET ║
╠══════════════════════════════════════════════════════════════════╣
║ CREATE CA ║
║ openssl genrsa -aes256 -out ca.key 4096 ║
║ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt ║
╠══════════════════════════════════════════════════════════════════╣
║ CREATE SERVER CERT ║
║ openssl genrsa -out server.key 2048 ║
║ openssl req -new -key server.key -out server.csr ║
║ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ ║
║ -CAcreateserial -out server.crt -days 365 -extfile ext ║
╠══════════════════════════════════════════════════════════════════╣
║ CREATE CLIENT CERT ║
║ openssl genrsa -out client.key 2048 ║
║ openssl req -new -key client.key -out client.csr ║
║ openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \ ║
║ -CAcreateserial -out client.crt -days 365 -extfile ext ║
║ openssl pkcs12 -export -out client.p12 -inkey client.key \ ║
║ -in client.crt -certfile ca.crt ║
╠══════════════════════════════════════════════════════════════════╣
║ TRUST CA ON SYSTEMS ║
║ macOS: sudo security add-trusted-cert -d -r trustRoot \ ║
║ -k /Library/Keychains/System.keychain ca.crt ║
║ Windows: certutil -addstore -f "Root" ca.crt ║
║ Ubuntu: sudo cp ca.crt /usr/local/share/ca-certificates/ \ ║
║ && sudo update-ca-certificates ║
║ RHEL: sudo cp ca.crt /etc/pki/ca-trust/source/anchors/ \ ║
║ && sudo update-ca-trust ║
╠══════════════════════════════════════════════════════════════════╣
║ VERIFY CERTIFICATE ║
║ openssl x509 -in cert.crt -text -noout ║
║ openssl verify -CAfile ca.crt server.crt ║
╚══════════════════════════════════════════════════════════════════╝
Appendix: Platform Trust Installation Reference
| Platform | Location/Command | Requires Admin | Notes |
|---|---|---|---|
| macOS | Keychain Access → System | 🟢 Yes | GUI or security command |
| Windows | certutil / MMC | 🟢 Yes | Use LocalMachine\Root store |
| Ubuntu/Debian | /usr/local/share/ca-certificates/ |
🟢 Yes | Run update-ca-certificates |
| RHEL/CentOS | /etc/pki/ca-trust/source/anchors/ |
🟢 Yes | Run update-ca-trust |
| Arch | /etc/ca-certificates/trust-source/anchors/ |
🟢 Yes | Run trust extract-compat |
| Android (User) | Settings → Security → Install cert | 🟡 No | Shows “monitored” warning |
| Android (System) | /system/etc/security/cacerts/ |
🔴 Root | Requires root access |
| iOS/iPadOS | Profile install + Trust Settings | 🟡 No | Two-step process required |
Legend:
- 🟢 Standard admin access
- 🟡 User can do it, with limitations
- 🔴 Requires special access (root/jailbreak)
Security Considerations
Don’t shoot yourself in the foot
- Protect your CA private key – If compromised, attackers can issue trusted certificates for any domain
- Use strong passphrases – The CA key passphrase should be complex and stored securely
- Set reasonable validity periods – CA: 5-10 years, Server certs: 1 year, Client certs: 1 year or less
- Keep an inventory – Track what certificates you’ve issued and when they expire
- Don’t use self-signed CAs for public services – This is for internal/homelab use only
- Revocation is hard – Self-signed CAs don’t have CRL/OCSP infrastructure. If a cert is compromised, you need to manually remove trust or reissue
Conclusion
You now have the power to:
- Create your own Certificate Authority with proper key management
- Issue server certificates for HTTPS services in your homelab
- Issue client certificates for mutual TLS authentication
- Deploy CA trust across macOS, Windows, Linux, Android, and iOS
No more certificate warnings. No more “proceed anyway” clicks. No more browser shame.
Fun fact: The average enterprise manages over 50,000 certificates. Your homelab probably has fewer than 50. Congratulations, you’re already ahead of most Fortune 500 IT departments in terms of certificate-to-headache ratio.
Stay encrypted, stay curious 🔐
Leave a Reply