The fastest way to set up a private CA for your homelab, Home Assistant, self-hosted services, and Kubernetes — no cloud, no cost, no vendor lock-in.
Stop copy-pasting OpenSSL commands from Stack Overflow. private-ca gives you a production-grade two-tier CA in minutes: interactive setup, automatic cert renewal via cert-manager, and one-command Kubernetes deployment.
Perfect for:
- 🏠 Homelab — Proxmox, TrueNAS, Nginx Proxy Manager, Grafana, any self-hosted service
- 🤖 Home Assistant — trusted HTTPS on your local domain (e.g.
homeassistant.local) - 🔒 Self-signed certificates — for development, internal APIs, and private networks
- 🌐 Private domain certs —
*.home.lab,*.internal, any domain you control - ☸️ Kubernetes / cert-manager — ClusterIssuer ready, automates TLS for all your ingresses
- Overview
- Requirements
- Quick Start — John Doe Example
- Menu Reference
- CLI Reference
- Certificate Architecture
- Configuration Reference
- Installing Root CA on Devices
- Kubernetes cert-manager Integration
- Security Best Practices
- Troubleshooting
Creates a two-tier CA hierarchy:
- Root CA — trust anchor, AES-256 encrypted key, 50 years validity, store offline
- Intermediate CA — used for daily signing, 25 years validity, used by cert-manager
- Server Certificates — TLS/HTTPS, 1 year validity, includes SAN
┌─────────────────────────────────────────┐
│ Root CA (Self-Signed) │
│ Validity: 50 years │
│ Key Size: 4096-bit RSA (AES-256 enc.) │
│ Storage: OFFLINE │
└──────────────────┬──────────────────────┘
│ Signs
▼
┌─────────────────────────────────────────┐
│ Intermediate CA │
│ Validity: 25 years │
│ Key Size: 4096-bit RSA (no password) │
│ Usage: Daily signing, cert-manager │
└──────────────────┬──────────────────────┘
│ Signs
▼
┌─────────────────────────────────────────┐
│ Server Certificates │
│ Validity: 1 year │
│ Key Size: 2048-bit RSA │
│ SAN: Required (CN ignored) │
└─────────────────────────────────────────┘
Intermediate CA has no passphrase by design — required for automated signing in Kubernetes cert-manager.
# Check required tools
which bash openssl base64- Linux / macOS: bash ≥ 4, openssl, base64 — usually pre-installed
- Windows: use WSL, Git Bash, or Cygwin
John Doe wants a TLS certificate for johndoe.com on his home server.
./cert_generator.sh
# Select: 1. Initialize CA (Root + Intermediate)The script will interactively ask for all identity information:
Organization (O): John Doe
Country (C) - 2-letter ISO code: PL
State / Province (ST) [optional]: Mazovia
City / Locality (L) [optional]:
Email address [optional]:
Root CA Common Name (CN) [John Doe - Root CA]:
Intermediate CA Common Name (CN) [John Doe - Intermediate CA]:
Proceed with these values? (yes/no): yes
Enter Root CA passphrase (min 4 chars): ****
Confirm Root CA passphrase: ****
If you answer
noto the summary, all questions restart from the beginning.
# Encrypt and back up the Root CA key — do this immediately after generation
tar -czf root-ca-backup-$(date +%Y%m%d).tar.gz certs/root-ca/
gpg -c root-ca-backup-$(date +%Y%m%d).tar.gz
# Store the encrypted backup in at least two places:
# - USB drive kept offline
# - Password manager attachment
# - Physical safeNever lose this key. If compromised, you must regenerate the entire CA and reinstall it on all devices.
./cert_generator.sh
# Select: 2. Generate Server Certificate
# Common Name: johndoe.com
# DNS names: johndoe.com,www.johndoe.comOr via CLI:
./cert_generator.sh server "johndoe.com" "johndoe.com,www.johndoe.com"Files created in certs/server/johndoe.com/:
| File | Use |
|---|---|
server.key |
Private key — keep on server only, never share |
server.crt |
Certificate |
server-fullchain.crt |
Certificate + full CA chain — use this in nginx/Apache |
For browsers to trust johndoe.com, install certs/root-ca/root-ca.crt on every device.
Phone (Android):
- Copy
root-ca.crtto phone via USB or cloud - Settings → Security → Encryption & credentials → Install a certificate → CA certificate
- Select the file, name it "John Doe Root CA"
Phone (iOS):
- AirDrop or email
root-ca.crtto yourself, tap to install - Settings → General → VPN & Device Management → tap the profile → Install
- Settings → General → About → Certificate Trust Settings → enable the certificate
Linux:
sudo cp certs/root-ca/root-ca.crt /usr/local/share/ca-certificates/johndoe-root-ca.crt
sudo update-ca-certificatesmacOS:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain \
certs/root-ca/root-ca.crtWindows (PowerShell as Administrator):
certutil -addstore -f "ROOT" certs\root-ca\root-ca.crtFirefox (uses own certificate store):
Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import → select root-ca.crt → Trust to identify websites
nginx:
server {
listen 443 ssl;
server_name johndoe.com www.johndoe.com;
ssl_certificate /path/to/certs/server/johndoe.com/server-fullchain.crt;
ssl_certificate_key /path/to/certs/server/johndoe.com/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
}1. Initialize CA (Root + Intermediate) — first-time setup; prompts for identity and passphrase
if Root CA exists, checks Intermediate CA validity
2. Generate Server Certificate — prompts for domain and SANs
3. List All Certificates — shows Root CA → Intermediate CA → server certs
4. Export to Kubernetes — generates manifests, offers to apply to cluster
5. Delete CA Infrastructure — two-step confirmation required
6. Exit
# Initialize CA (uses values from cert.conf, no interactive prompts)
./cert_generator.sh init
# Generate server certificate
./cert_generator.sh server <common-name> <dns-names>
./cert_generator.sh server "johndoe.com" "johndoe.com,www.johndoe.com"
./cert_generator.sh server "johndoe.com" "johndoe.com,*.johndoe.com"
# Export Kubernetes manifests and optionally apply to cluster
./cert_generator.sh k8s-export
# List all certificates
./cert_generator.sh list
# Delete CA infrastructure (requires interactive confirmation)
./cert_generator.sh deletecert.conf controls all defaults:
| Section | Key | Default | Description |
|---|---|---|---|
[root_ca] |
name |
— | Root CA Common Name |
organization |
— | Organization (O) | |
country |
— | 2-letter ISO country code | |
validity_days |
18250 | 50 years | |
key_size |
4096 | RSA key size | |
[intermediate_ca] |
name |
— | Intermediate CA CN |
validity_days |
9125 | 25 years | |
key_size |
4096 | RSA key size | |
[server_cert] |
validity_days |
365 | 1 year |
key_size |
2048 | RSA key size | |
[kubernetes] |
namespace |
cert-manager | Target namespace |
ca_secret_name |
private-ca-secret | Secret name | |
ca_issuer_name |
private-ca-issuer | ClusterIssuer name |
See Step 4 in the Quick Start for platform-specific instructions.
If your app does not trust user-installed CAs (Android 7.0+), add a network security config:
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system"/>
<certificates src="user"/>
</trust-anchors>
</base-config>
</network-security-config><!-- AndroidManifest.xml -->
<application android:networkSecurityConfig="@xml/network_security_config">Before you begin:
- Generate the CA first — run
./cert_generator.shand choose option 1 (Initialize CA).- The environment where you run the script must have a kubeconfig configured as default (i.e.
~/.kube/configor$KUBECONFIGpointing to your cluster), so thatkubectlcan communicate with the cluster without additional flags.
# Install cert-manager via Helm (always installs latest stable)
helm repo add jetstack https://charts.jetstack.io --force-update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=true
# Wait until ready
kubectl wait --for=condition=available --timeout=300s \
deployment/cert-manager -n cert-managerAlternatively, check cert-manager releases for the latest
kubectl applyURL.
Step 1 — Generate the CA (if not done yet):
./cert_generator.sh
# Select: 1. Initialize CA (Root + Intermediate)Step 2 — Export and apply to Kubernetes:
./cert_generator.sh
# Select: 4. Export to Kubernetes
# Answer: yes — to apply immediately
kubectlmust be available and your kubeconfig must point to the target cluster before running step 2.
This creates:
- Secret
private-ca-secretin namespacecert-manager— contains Intermediate CA key + chain + Root CA cert - ClusterIssuer
private-ca-issuer— cluster-wide, used via annotation
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: johndoe-ingress
annotations:
cert-manager.io/cluster-issuer: "private-ca-issuer"
spec:
ingressClassName: nginx
tls:
- hosts:
- johndoe.com
secretName: johndoe-tls
rules:
- host: johndoe.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: johndoe-app
port:
number: 80apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: johndoe-cert
namespace: default
spec:
secretName: johndoe-tls-secret
issuerRef:
name: private-ca-issuer
kind: ClusterIssuer
commonName: johndoe.com
dnsNames:
- johndoe.com
- www.johndoe.com
duration: 8760h
renewBefore: 720hRe-run option 4 after generating a new Intermediate CA. The Secret is deleted and recreated with new data; ClusterIssuer is updated in place. Existing certificates remain valid until expiry — cert-manager renews them automatically 30 days before.
cert-manager will issue and renew certificates automatically, but browsers, phones, and operating systems will show a certificate error unless they trust your Root CA.
Every device (laptop, phone, CI runner) that accesses services secured by this CA must have the Root CA certificate installed as a trusted authority.
See Installing Root CA on Devices for step-by-step instructions per platform (Linux, macOS, Windows, Android, iOS).
| Root CA | Intermediate CA | |
|---|---|---|
| Passphrase | ✅ AES-256, required | None (by design) |
| Storage | Offline after init | Online for cert-manager |
| Compromise | Catastrophic — rebuild everything | Recoverable — rotate Intermediate |
| Rotation | Never (50 yr validity) | Every 5–10 years |
Key rules:
- Back up
certs/root-ca/root-ca.keyencrypted, offline, immediately after generation - Never commit
certs/to git (.gitignorealready covers this) chmod 400on all private keys (set automatically by the script)
| Problem | Fix |
|---|---|
| Browser shows "Not Secure" | Root CA not installed on that device — see Step 4 |
| Firefox still shows warning | Firefox uses its own store: Settings → Privacy & Security → View Certificates → Authorities → Import |
cannot execute: required file not found |
CRLF in script: sed -i 's/\r$//' cert_generator.sh |
Missing required key 'name' in [root_ca] |
CRLF in config: sed -i 's/\r$//' cert.conf |
| OpenSSL passphrase error | Passphrase must be ≥ 4 characters — script validates before calling OpenSSL |
unable to load CA private key |
Wrong passphrase or bad permissions: chmod 400 certs/root-ca/root-ca.key |
| ClusterIssuer not READY | kubectl describe clusterissuer private-ca-issuer and kubectl logs -n cert-manager -l app=cert-manager --tail=50 |
.
├── cert_generator.sh # Main script
├── cert.conf # Configuration
├── README.md
└── certs/ # Created on first run (excluded from git)
├── root-ca/
│ ├── root-ca.key # ⚠️ Root CA private key — back up offline
│ ├── root-ca.crt # Root CA certificate — install on all devices
│ ├── root-ca.cnf
│ ├── index.txt # OpenSSL CA database
│ └── serial
├── intermediate-ca/
│ ├── intermediate-ca.key # Intermediate CA private key
│ ├── intermediate-ca.crt
│ ├── intermediate-ca.csr
│ ├── ca-chain.crt # Intermediate + Root chain
│ └── index.txt
├── server/
│ └── johndoe.com/
│ ├── server.key # Server private key
│ ├── server.crt
│ └── server-fullchain.crt # Use this in nginx/Apache
└── kubernetes/
├── ca-secret.yaml # Kubernetes Secret manifest
├── ca-issuer.yaml # ClusterIssuer manifest
├── example-certificate.yaml
├── example-ingress-auto-tls.yaml
└── install-to-k8s.sh
- Initial release