Skip to content

kkemot/ca-private

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

private-ca — Private Certificate Authority for Homelabs & Kubernetes

License: MIT Shell: Bash Platform: Linux/macOS cert-manager

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

Table of Contents


Overview

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

Certificate Architecture

┌─────────────────────────────────────────┐
│         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.


Requirements

# Check required tools
which bash openssl base64
  • Linux / macOS: bash ≥ 4, openssl, base64 — usually pre-installed
  • Windows: use WSL, Git Bash, or Cygwin

Quick Start — John Doe Example

John Doe wants a TLS certificate for johndoe.com on his home server.

Step 1 — Initialize CA

./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 no to the summary, all questions restart from the beginning.

Step 2 — Archive the Root CA private key ⚠️

# 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 safe

Never lose this key. If compromised, you must regenerate the entire CA and reinstall it on all devices.

Step 3 — Generate a server certificate

./cert_generator.sh
# Select: 2. Generate Server Certificate
# Common Name: johndoe.com
# DNS names:   johndoe.com,www.johndoe.com

Or 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

Step 4 — Install Root CA on devices

For browsers to trust johndoe.com, install certs/root-ca/root-ca.crt on every device.

Phone (Android):

  1. Copy root-ca.crt to phone via USB or cloud
  2. Settings → Security → Encryption & credentials → Install a certificate → CA certificate
  3. Select the file, name it "John Doe Root CA"

Phone (iOS):

  1. AirDrop or email root-ca.crt to yourself, tap to install
  2. Settings → General → VPN & Device Management → tap the profile → Install
  3. 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-certificates

macOS:

sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain \
  certs/root-ca/root-ca.crt

Windows (PowerShell as Administrator):

certutil -addstore -f "ROOT" certs\root-ca\root-ca.crt

Firefox (uses own certificate store): Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import → select root-ca.crt → Trust to identify websites

Step 5 — Configure web server

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;
}

Menu Reference

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

CLI Reference

# 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 delete

Configuration Reference

cert.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

Installing Root CA on Devices

See Step 4 in the Quick Start for platform-specific instructions.

Android — App Development

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">

Kubernetes cert-manager Integration

Before you begin:

  1. Generate the CA first — run ./cert_generator.sh and choose option 1 (Initialize CA).
  2. The environment where you run the script must have a kubeconfig configured as default (i.e. ~/.kube/config or $KUBECONFIG pointing to your cluster), so that kubectl can communicate with the cluster without additional flags.

Prerequisites

# 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-manager

Alternatively, check cert-manager releases for the latest kubectl apply URL.

Apply CA to cluster

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

kubectl must be available and your kubeconfig must point to the target cluster before running step 2.

This creates:

  • Secret private-ca-secret in namespace cert-manager — contains Intermediate CA key + chain + Root CA cert
  • ClusterIssuer private-ca-issuer — cluster-wide, used via annotation

Use in Ingress (recommended)

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: 80

Use via Certificate resource

apiVersion: 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: 720h

Updating CA in cluster

Re-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.

Trust — installing Root CA on client devices

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).


Security Best Practices

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.key encrypted, offline, immediately after generation
  • Never commit certs/ to git (.gitignore already covers this)
  • chmod 400 on all private keys (set automatically by the script)

Troubleshooting

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

File Structure

.
├── 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

Changelog

Version 1.0.0

  • Initial release

About

Create your own Private Certificate Authority (Self-signed certs) and issue locally-trusted certificates for your homelab, Kubernetes, and self-hosted services with this simple Bash-based CLI. No cloud, no cost — just secure TLS in minutes.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages