Building and Running Your Own Local Certificate Authority¶
A practical guide to OpenSSL/PKI fundamentals, standing up a private CA for internal use, and understanding what it would take to scale one into a publicly trusted, commercial CA.
Part 1 — OpenSSL Command Reference¶
1.1 Format conversion (PFX/PKCS12 ↔ PEM)¶
# Extract cert only from a .pfx
openssl pkcs12 -in [ hostname ] -clcerts -nokeys -out [ hostname ]
# Extract key only (still password-protected) from a .pfx
openssl pkcs12 -in [ hostname ] -nocerts -out [ hostname ]
# Strip the passphrase from the extracted key
# WARNING: the resulting key file is plaintext — lock permissions immediately
openssl rsa -in [ hostname ] -out [ hostname ]
chmod 0400 [ hostname ]
# Pull cert + chain + key out of a combined PFX into one file for inspection
openssl pkcs12 -in [ hostname ] -out [ hostname ] -nodes
# then split the resulting file by hand into:
# -----BEGIN PRIVATE KEY-----...END----- -> [ hostname ]
# -----BEGIN CERTIFICATE-----...END----- (x1) -> [ hostname ] (leaf — no "friendlyName: ... Root/Intermediate" comment above it)
# -----BEGIN CERTIFICATE-----...END----- (x2+)-> [ hostname ] (comment above each block shows the issuer name)
# Combine cert + key into a single PEM (needed by nginx, HAProxy, Dovecot, etc.)
cat [ hostname ] [ hostname ] > [ hostname ]
chown root:root [ hostname ] && chmod 0400 [ hostname ]
1.2 Generating keys, CSRs, and self-signed certs¶
# --- Modern, recommended: RSA, one-step self-signed cert with SAN inline (no config file needed) ---
openssl req -x509 -newkey rsa:4096 -noenc -days 365 \
-keyout [ hostname ] -out [ hostname ] \
-subj "/C=US/ST=State/L=City/O=Organization/CN=[ hostname ]" \
-addext "subjectAltName=DNS:[ hostname ],DNS:[ hostname ]"
# --- Preferred where you control both ends: ECDSA (faster, smaller, modern) ---
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -noenc -days 365 \
-keyout [ hostname ] -out [ hostname ] \
-subj "/C=US/ST=State/L=City/O=Organization/CN=[ hostname ]" \
-addext "subjectAltName=DNS:[ hostname ]"
# --- CSR only (to be signed by your own CA or a public CA) ---
mkdir -p ~/[ hostname ]
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ~/[ hostname ]/[ hostname ]
openssl req -new -sha256 -key ~/[ hostname ]/[ hostname ] \
-out ~/[ hostname ]/[ hostname ] \
-addext "subjectAltName=DNS:[ hostname ],DNS:[ hostname ]"
openssl req -noout -text -in ~/[ hostname ]/[ hostname ] # verify before submitting
Note on
-noencvs-nodes: OpenSSL 3.x introduced-noencas the preferred spelling;-nodes("no DES") still works as a deprecated alias. Either is fine on 3.x, but prefer-noencgoing forward.
1.3 Inspecting certificates¶
# Full local cert dump
openssl x509 -in [ hostname ] -text -noout
# Just the fields you usually want
openssl x509 -in [ hostname ] -noout -subject -issuer -dates
# Fingerprint (for matching a browser-presented cert to a server file)
openssl x509 -in [ hostname ] -noout -fingerprint -sha256
# Remote server — full handshake dump
openssl s_client -connect [ hostname ]:443 -servername [ hostname ]
# Remote server — just expiry, issuer, or subject (fast, scriptable)
echo | openssl s_client -servername [ hostname ] -connect [ hostname ]:443 2>/dev/null | openssl x509 -noout -dates
echo | openssl s_client -servername [ hostname ] -connect [ hostname ]:443 2>/dev/null | openssl x509 -noout -issuer
echo | openssl s_client -servername [ hostname ] -connect [ hostname ]:443 2>/dev/null | openssl x509 -noout -subject
# Show the full chain a server presents (useful for diagnosing "why doesn't this client trust me")
openssl s_client -showcerts -connect localhost:443
# Verify a leaf against a specific CA file (not the system trust store)
openssl verify -CAfile [ hostname ] -purpose sslserver [ hostname ]
# Confirm a cert / key / CSR all share the same public key
openssl x509 -noout -modulus -in [ hostname ] | openssl md5
openssl rsa -noout -modulus -in [ hostname ] | openssl md5
openssl req -noout -modulus -in [ hostname ] | openssl md5
# for EC keys, modulus doesn't apply — compare instead with:
openssl pkey -pubout -in [ hostname ] | openssl md5
openssl x509 -pubkey -noout -in [ hostname ] | openssl md5
1.4 Encrypt/decrypt files (symmetric, not TLS-related but same toolkit)¶
# Prefer AES-256 for anything new — the old 3DES-based cipher is legacy
openssl enc -aes-256-cbc -pbkdf2 -salt -in [ hostname ] -out [ hostname ]
openssl enc -aes-256-cbc -pbkdf2 -d -salt -in [ hostname ] -out [ hostname ]
Part 2 — A Few Things Worth Knowing Before You Start¶
- SSLv2/SSLv3 test flags are gone.
openssl s_client -ssl2and-ssl3were removed entirely from OpenSSL 1.1.0+. If you find old documentation referencing them, treat it as historical only — it won't run on a current system. - SAN, not CN, is what browsers check. Since Chrome 58 (2017), the Common Name field is ignored for hostname validation — only Subject Alternative Name entries count. A cert issued without a SAN will often still work fine against
curl/openssl s_client(which are more lenient) while silently failing in every modern browser. Always set SAN explicitly. - RSA vs ECDSA vs Ed25519. RSA-2048/4096 remains universally compatible. ECDSA (P-256/P-384) and Ed25519 are faster to generate and verify and produce much smaller certificates and handshakes, at equivalent security margins — a good default when you control both the CA and every client that needs to trust it.
- A CRL you generate but never serve does nothing. Generating a CRL with
openssl ca -gencrlis only half the job — something needs to fetch it (a web server hosting the file, referenced via the cert's CRL Distribution Point extension, or a VPN daemon'scrl-verifydirective). An unpublished CRL protects nothing. chown [ hostname ]vschown root:root. GNUchownaccepts.as a group separator, but it isn't portable to BSD/macOSchown, which expects:. Use:for portability across platforms.
Part 3 — Two-Tier CA Architecture¶
A single "root CA signs everything directly" setup is the easiest to build and the easiest to lose control of. A better pattern, used at every scale from homelab to enterprise, separates a rarely-used root from a day-to-day issuing (intermediate) CA:
Root CA (offline, air-gapped, used only to sign intermediates)
│
└── Intermediate/Issuing CA (online, used for routine cert issuance)
├── server certs
├── client certs
└── ...
Why bother with two tiers? If the online issuing CA is ever compromised, you revoke and reissue just the intermediate. Every device that trusts the root stays trusted — you only need to push a new intermediate chain. With a single-tier setup, compromise of the one host doing your signing means rebuilding trust from scratch on every device that has your root CA cert installed.
3.1 Choosing your tooling¶
| Approach | Good for | Downsides |
|---|---|---|
Raw openssl ca + .cnf config files |
Full control, zero dependencies, most educational | Manual bookkeeping ([ hostname ], serial files), easy to mismatch SAN/extensions between certs, no automated renewal |
| EasyRSA | A maintained wrapper around the same OpenSSL CA machinery; widely used alongside OpenVPN | Still fundamentally manual issuance |
step-ca (Smallstep) or a similar ACME-capable CA |
Speaks the same ACME protocol as Let's Encrypt, so certbot, Caddy, Traefik, or Kubernetes cert-manager can request certs from your own CA automatically; supports short-lived certs and automated renewal |
An additional service to run and secure; a bit more setup up front |
For anything beyond a handful of manually-managed certs, an ACME-capable private CA is worth the initial setup cost — it removes the recurring manual-renewal workload entirely and lets you issue much shorter-lived certificates, which meaningfully reduces the blast radius of a leaked key. For learning the underlying mechanics, or for a small number of long-lived certs, raw OpenSSL is perfectly workable and is what's demonstrated below.
3.2 Building the root CA¶
Generate this once, ideally on a machine with no network access (a live-boot USB, a VM you use once and discard, or a machine you keep powered off between uses):
mkdir -p ~/local-ca/root/{private,certs,newcerts,crl}
cd ~/local-ca/root
touch [ hostname ] && echo 1000 > serial
openssl genpkey -algorithm ED25519 -out private/[ hostname ] # or RSA 4096 / EC P-384 for broader client compatibility
chmod 0400 private/[ hostname ]
openssl req -x509 -new -key private/[ hostname ] -sha256 -days 7300 \
-out certs/[ hostname ] \
-subj "/C=US/ST=State/O=Example Root CA/CN=Example Root CA" \
-addext "basicConstraints=critical,CA:true,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
# 7300 days = 20 years — a root should long-outlive its intermediates.
# Store private/[ hostname ] offline after this and never let it touch a networked host again.
3.3 Building the intermediate CA¶
This lives on your issuing host (or is loaded into step-ca):
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out [ hostname ]
openssl req -new -key [ hostname ] -out [ hostname ] \
-subj "/C=US/ST=State/O=Example Root CA/CN=Example Issuing CA 01"
# Sign the intermediate with the OFFLINE root — do this step on the offline machine,
# then transfer only the signed certificate (never the root key) back to the online host
openssl x509 -req -in [ hostname ] -CA certs/[ hostname ] -CAkey private/[ hostname ] \
-CAcreateserial -days 1825 -out [ hostname ] -sha256 \
-extfile <(printf "basicConstraints=critical,CA:true,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign")
cat [ hostname ] certs/[ hostname ] > [ hostname ] # full chain for clients/services to trust
[ hostname ] plus [ hostname ]/[ hostname ] become the signing identity for day-to-day issuance. Only certs/[ hostname ] — the public certificate, never the key — needs to be distributed to trust stores on client devices.
3.4 Issuing a leaf certificate from the intermediate¶
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out [ hostname ]
openssl req -new -key [ hostname ] -out [ hostname ] \
-subj "/C=US/ST=State/O=Example Org/CN=[ hostname ]" \
-addext "subjectAltName=DNS:[ hostname ]"
openssl x509 -req -in [ hostname ] -CA [ hostname ] -CAkey [ hostname ] \
-CAcreateserial -days 397 -out [ hostname ] -sha256 \
-extfile <(printf "basicConstraints=CA:false\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=DNS:[ hostname ]")
cat [ hostname ] [ hostname ] > [ hostname ]
chown root:root [ hostname ] && chmod 0400 [ hostname ]
(397 days mirrors the current industry-standard maximum lifetime for publicly trusted server certificates — a reasonable default even for a private CA, since it forces a renewal habit before it becomes urgent.)
3.5 Distributing trust to clients¶
- Linux: drop the root cert into
/usr/local/share/ca-certificates/(Debian/Ubuntu) or/etc/pki/ca-trust/source/anchors/(RHEL/Fedora), then runupdate-ca-certificatesorupdate-ca-trust. - Firewalls/routers with their own trust store (e.g. OPNsense, pfSense): most have a dedicated Certificate Authorities import section under their system settings.
- Browsers: Chrome/Edge use the OS certificate store on Linux and Windows; Firefox maintains its own store (Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import).
- Mobile: iOS requires the profile to be installed and separately enabled under Settings → General → About → Certificate Trust Settings — the second step is easy to miss.
- Fleet/config management: if this needs to reach more than a couple of devices, distribute the root cert through whatever configuration management tooling you already use rather than installing it by hand on each device.
3.6 Revocation: CRL and OCSP¶
# Generate/update the CRL after any revocation
openssl ca -config [ hostname ] -gencrl -out crl/[ hostname ]
# Serve it somewhere stable, e.g. via nginx:
# location /crl/ { alias /path/to/crl/; }
# and reference it in every issued cert via the CA config's CRL Distribution Point extension:
# crlDistributionPoints = URI:http://[ hostname ]/crl/[ hostname ]
Short-lived certificates (hours to days, as an ACME-based CA can issue) make CRL/OCSP largely unnecessary in practice, since a compromised cert simply expires soon rather than needing active revocation. This is the same reasoning behind the broader industry's move toward shorter certificate lifetimes generally.
3.7 Key security hygiene for the root¶
- Generate the root key on a machine with no network access, encrypt it at rest, and never let it touch a host that also runs live services.
- For shared or organizational use, consider a passphrase-protected key combined with secret-sharing (e.g. Shamir's Secret Sharing via a tool like
ssss) so no single person can reconstitute root access alone. - Back up the encrypted root key to at least two separate physical locations. Losing it means every device that has your root cert installed needs a new root pushed out before old certificates stop validating.
Part 4 — Scaling to a Commercial / Publicly Trusted CA¶
Everything above produces a private CA — trusted only on devices where the root has been manually installed. Becoming a publicly trusted CA, where an unmodified browser or OS trusts certs you issue out of the box, is a fundamentally different undertaking. It's worth understanding the distinction clearly before treating it as a natural next step.
4.1 Two different things people mean by "commercial CA"¶
A. Selling PKI-as-a-service using a private root. Running internal CAs for other organizations — device identity, internal mTLS, code-signing for a company that controls its own build fleet. This is entirely achievable with the tooling above: you're operating a CA (via step-ca or similar) as a managed service, and clients install your root on their devices. This is a viable business model on its own and doesn't require public trust at all.
B. Becoming a publicly trusted root CA (in the same category as DigiCert, Sectigo, Let's Encrypt) — this is an enormous undertaking, described below, and realistically not something an individual or small operation bootstraps from scratch in the current landscape.
4.2 What public trust (option B) actually requires¶
- CA/Browser Forum Baseline Requirements ([ hostname ]) — the industry-defined rules covering key ceremony procedures, certificate lifecycle, revocation response times, minimum algorithm/key-size standards, and domain/organization validation methods. Every publicly trusted CA must comply.
- WebTrust for CAs (or ETSI EN 319 411) audit — an annual third-party audit against the Baseline Requirements, performed by one of a small number of accredited firms. This typically costs tens of thousands of dollars annually and requires months of preparation for a first audit.
- Root program inclusion — Microsoft, Apple, Google, and Mozilla each run separate application processes to add a root to their trust stores. These typically take over a year, involve public scrutiny of operational practices, and can be paused or rejected for policy-compliance reasons at any point.
- Certificate Transparency — Chrome will not trust any certificate that isn't logged in a public CT log. This means either operating your own CT log infrastructure or arranging inclusion with existing log operators.
- HSM-backed key ceremonies — root and intermediate keys must be generated and stored in FIPS 140-2/3 Level 3 (or Common Criteria EAL4+) certified hardware, with the generation ceremony itself scripted, recorded, and witnessed by independent auditors — specifically so no single individual, including the CA operator, ever has raw access to the root key.
- Liability and insurance — publicly trusted CAs carry substantial liability insurance, since a single misissued certificate for a major domain becomes an internet-wide incident; browsers can and do distrust an entire root over one serious violation.
- 24/7 operational requirements — the Baseline Requirements mandate revocation response within hours for certain classes of problem report, which means real operational staffing, not best-effort uptime.
Given all of this, most new "CA" businesses today operate as resellers or delegated third parties under an existing trusted root rather than standing up a new one from scratch. Bootstrapping an independent publicly trusted root is generally not viable without either an existing large user base that justifies the audit/HSM cost, or partnership with an established CA.
4.3 A realistic middle path¶
- Build and productize the private CA offering first — internal mTLS, device identity, or code-signing PKI for organizations that don't want to run it themselves. This is achievable with the architecture above and is commercially viable at small scale.
- Consider becoming a Delegated Third Party under an existing publicly trusted CA if there's demand for publicly-trusted certificates — some CAs allow partners to perform domain validation and issuance under the parent CA's already-audited infrastructure and root, avoiding the HSM/audit/root-program burden while still operating the customer-facing side of the business.
- Only pursue an independent public root once volume justifies it — the WebTrust audit, HSM ceremony costs, and ongoing root-program relationship management make sense once there's an established paying customer base asking for it, not as a starting point.
Part 5 — Service Hardening Reference¶
Apache/nginx TLS config¶
# Require TLS 1.2 minimum, prefer 1.3 where supported; disable compression (mitigates the CRIME attack)
SSLHonorCipherOrder On
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite HIGH:!aNULL:!MD5
SSLCompression off
HSTS¶
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
(One year is the current standard max-age. Add preload only once you're confident every subdomain will always be served over HTTPS — removal from browsers' preload lists is a slow process.)
OpenVPN¶
cipher AES-256-GCM # AEAD cipher, preferred over AES-256-CBC
auth SHA256
tls-version-min 1.2
tls-cipher TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384
crl-verify path-to/[ hostname ]
remote-cert-tls server
Dovecot¶
ssl = yes
ssl_cert = </etc/ssl/private/[ hostname ]
ssl_key = </etc/ssl/private/[ hostname ]
ssl_min_protocol = TLSv1.2
(ssl_cert and ssl_key must point at the certificate and the private key respectively — a surprisingly common copy-paste error is pointing both at the same file.)