A homelab private certificate authority is a small two tier PKI you run yourself: a root key that lives offline, one online intermediate that is name constrained to your home domain, and an ACME server that issues leaf certificates lasting hours or days. Every device trusts the root once, and renewal runs on its own after that.
This explainer goes one level below our walkthrough on running your own certificate authority at home, which builds an HTTP-01 setup and covers desktop and phone trust stores. Here the focus is what goes inside each certificate, how to treat the root key, how DNS-01 works against your own DNS server, and how to make containers trust the result. For protocol basics, read how ACME issues certificates and the ACME challenge types compared. Rules below come from RFC 5280, RFC 8555, version 2.3.1 of the CA/Browser Forum TLS Baseline Requirements (4 October 2026) and NIST SP 800-57 Part 1 Revision 5; commands come from each project's own documentation. Commercial certificate products appear only as categories.
Key takeaways
- Pick a private CA when the names are private. Self signed certificates make every service its own trust anchor; public certificates need public names and land in Certificate Transparency logs. A private CA gives one root and full control over names like
nas.home.arpa. - Three tiers, three different jobs. The root is a trust anchor that signs almost nothing. The intermediate carries
basicConstraintswithpathlen:0,keyUsagelimited tokeyCertSignandcRLSign, and a criticalnameConstraintsextension. Leaves carryCA:FALSE, asubjectAltNameand theserverAuthorclientAuthextended key usage. - Treat the root key like a long term signature key. NIST SP 800-57 suggests a 1 to 3 year usage period for a private signature key and notes that a root compromise forces every relying party to install a new trust anchor. Keep it offline, encrypted, backed up twice, with ceremony notes.
- DNS-01 works on a LAN if your DNS server accepts authenticated updates. RFC 2136 dynamic updates signed with a TSIG key (RFC 8945) let an ACME client publish the
_acme-challengeTXT record. Scope that key to one record name and one record type. - Short lifetimes replace revocation. Public TLS certificates drop to 200 days from 15 March 2026, 100 days from 15 March 2027 and 47 days from 15 March 2029 under ballot SC-081v3. The Baseline Requirements let CAs skip revocation for certificates valid 7 days or less. Internal leaves of 1 to 7 days give you the same property for free.
- Containers have their own trust store. Add the root at image build time; Node.js, Python Requests and Java also read their own bundle or keystore.
Why run a private CA at home instead of self-signed or public certificates?
There are three ways to put TLS on an internal service, and each one moves the trust decision to a different place.
Self-signed certificates: every service is its own root
A self signed certificate is signed by its own key. A client trusts it only if you add that exact certificate to its trust store, or if the user clicks through a warning. Ten services means ten trust anchors to distribute, ten expiry dates to track, and no central revocation.
The click through habit is worse. A warning on your own NAS looks exactly like a warning caused by someone intercepting your Wi-Fi. Train your household to accept those and you have removed the one signal TLS was meant to give. Self signed certificates are fine for a throwaway test, and nothing more.
Public certificates: real trust, public names, public logs
A public CA can only validate names in the public DNS. If you own example.net, DNS-01 gets you a certificate for nas.lab.example.net without opening a port, and every device already trusts it. The costs: names must sit under a public domain, so home.arpa (reserved for home networks by RFC 8375) is out; every certificate is logged in Certificate Transparency, which turns your hostnames into a searchable inventory; and lifetimes are capped at 200 days from 15 March 2026, so you need automation anyway.
A private CA: one root, your rules
A private CA gives you one trust anchor to install, then any number of certificates for internal names with lifetimes you choose: client certificates for mutual TLS between a backup agent and its server, IP address certificates for a switch page, wildcards if you want them. The price is that you are now a CA. You must protect the root key, distribute the root everywhere, keep issuance running and plan rotation. The rest of this guide covers those four jobs at home scale.
What does the hierarchy look like, and what goes into each certificate?
A homelab PKI that holds up has three layers:
- Root CA. A self signed certificate whose key is generated and kept offline. It signs the intermediate, a certificate revocation list (CRL) for the intermediate, and nothing else.
- Intermediate (issuing) CA. Signed by the root, its key lives on the online CA server. It signs every leaf certificate, usually through ACME.
- Leaf (end entity) certificates. Issued to services, reverse proxies and clients. They live hours or days.
The split limits blast radius. If the CA server is compromised, you revoke and replace the intermediate and keep the root already installed on every device. With the root key on that server, recovery means reinstalling a new root everywhere.
Certificate profile by tier
| Field (RFC 5280 section) | Root CA | Intermediate CA | Leaf |
|---|---|---|---|
basicConstraints (4.2.1.9) | critical, CA:TRUE, no path length | critical, CA:TRUE, pathlen:0 | CA:FALSE (or absent) |
keyUsage (4.2.1.3) | critical, keyCertSign, cRLSign | critical, keyCertSign, cRLSign | critical, digitalSignature (plus keyEncipherment for RSA keys used with RSA key transport) |
extendedKeyUsage (4.2.1.12) | absent | optional, see below | serverAuth, clientAuth or both |
nameConstraints (4.2.1.10) | absent (not reliably enforced on a trust anchor) | critical, permitted subtrees for your domain and subnet | not allowed |
subjectAltName (4.2.1.6) | absent | absent | the DNS names and IP addresses the service answers on |
subjectKeyIdentifier / authorityKeyIdentifier (4.2.1.2, 4.2.1.1) | SKI | SKI and AKI | SKI and AKI |
crlDistributionPoints (4.2.1.13) | absent | URL of the root's CRL | usually absent for short lived leaves |
| Suggested validity | 10 to 20 years | 1 to 3 years | 1 hour to 7 days |
| Key location | offline, encrypted or on a hardware token | CA server disk, readable only by the CA service | the service that uses it |
Basic constraints and path length
RFC 5280 Section 4.2.1.9 says conforming CAs must include basicConstraints in every CA certificate whose public key is used to verify certificate signatures, and must mark it critical. The cA boolean says whether the subject may act as a CA. pathLenConstraint limits how many non self issued intermediate certificates may follow this one in a path. A value of zero on your intermediate means it may issue leaves but cannot create another CA below itself, so a stolen intermediate key cannot mint a sub CA that escapes your monitoring.
On leaves, CA:FALSE is the important part. OpenSSL's openssl ca manual warns that if copy_extensions is set to copyall and a certificate request contains basicConstraints with CA:TRUE, signing it hands the requester a valid CA certificate. Use copy_extensions = none and define every extension in the CA's own configuration.
Key usage and extended key usage
keyUsage restricts what a key may do: keyCertSign and cRLSign for CA keys, digitalSignature for an ECDSA TLS leaf. RFC 5280 says conforming CAs should mark it critical.
extendedKeyUsage names purposes such as serverAuth and clientAuth. RFC 5280 says it will in general appear only in end entity certificates. Some verifiers also treat an EKU on an intermediate as a limit on what it may issue, so adding serverAuth, clientAuth to a homelab intermediate is a cheap extra fence, but do not count on every client enforcing it.
Name constraints, the most valuable extension you can add
nameConstraints (RFC 5280 Section 4.2.1.10) is what turns a private CA from "can impersonate any website to every device that trusts it" into "can only vouch for my home domain." The rules that matter:
- It must be used only in a CA certificate, conforming CAs must mark it critical, and it must not be an empty sequence.
- It holds permitted and excluded subtrees. Any name matching an excluded subtree is invalid whatever the permitted list says.
- For DNS names, any name formed by adding zero or more labels to the left of the constraint matches.
home.arpapermitsnas.home.arpaanda.b.home.arpa, notmyhome.arpa. - For IP addresses, the constraint is an address and a mask, so
192.168.10.0/255.255.255.0permits one /24. - Name constraints are not applied to self issued certificates unless they are the final certificate in the path. A self signed root is a trust anchor, and path validation starts from it rather than checking it like the certificates below it. Put the constraint on the intermediate.
RFC 5280 also says restrictions apply only when that name form is present. List a DNS subtree and no IP subtree, and IP address names are not constrained at all. Our post on name constraints for a private CA covers verifier behaviour in more depth.
Subject Alternative Name on leaves
Leaf identity goes in subjectAltName. RFC 5280 Section 4.2.1.6 defines dNSName and iPAddress entries, and name constraints are checked against them. Put every hostname and IP address the service answers on in the SAN, and do not rely on the subject common name. ACME servers build the SAN from the identifiers in the order, so this happens automatically once ACME is in place.
Reading a certificate to check its profile
Every certificate you create should be inspected before use:
openssl x509 -in int.crt -noout -text | sed -n '/X509v3 extensions/,/Signature Algorithm/p'
For the intermediate you should see CA:TRUE, pathlen:0 under Basic Constraints marked critical, Certificate Sign, CRL Sign under Key Usage, and a critical Name Constraints block listing your permitted subtrees. If any of those are missing, do not install the chain anywhere.
How do you protect the root key?
The root key is the one secret in a homelab PKI whose loss cannot be fixed quickly. NIST SP 800-57 Part 1 Revision 5 (May 2020), the US government's general key management recommendation, puts it plainly in its discussion of compromise recovery: recovering from the compromise of a root CA's private signature key requires every entity using the infrastructure to obtain and install a new trust anchor certificate, which usually involves expensive physical procedures, and elaborate precautions to avoid compromise may be justified. In a homelab "every entity" is every phone, laptop, TV box, container image and family member's tablet you ever set up.
Cryptoperiods from SP 800-57
SP 800-57 Table 1 suggests cryptoperiods by key type. A private signature key has a suggested originator usage period of 1 to 3 years. A public signature verification key has a recipient usage period of several years, depending on key size. The document also says longer periods may be warranted depending on the environment, as long as the risk is considered.
Here is how that maps to a two tier homelab PKI:
| Key | SP 800-57 key type | Suggested period | Homelab setting |
|---|---|---|---|
| Intermediate private key | Private signature key | 1 to 3 years originator usage | 2 year certificate, replace the key each time |
| Root private key | Private signature key, used rarely and offline | 1 to 3 years originator usage; longer needs a risk decision | Sign intermediates for a few years, then plan a root rollover |
| Root public key in trust stores | Public signature verification key | Several years, depends on key size | 10 to 20 year root certificate so verification outlives the signing period |
| Leaf private keys | Private authentication key in TLS | 1 to 2 years | Replaced at every renewal, so hours or days |
A root certificate's validity is about how long clients may verify with it; the key's signing life can be shorter. A 20 year root with a written plan to introduce a successor after five years or so fits both.
Where the root key lives
Pick one of three storage approaches and write down which you chose.
- An offline machine. An old laptop with its network hardware removed, or a live USB boot with networking off. Generate and encrypt the key there and never connect it to your LAN. CSRs come in and certificates go out on a USB stick.
- A hardware token. A smart card or USB key that supports PKCS#11 can hold the key so it never exists as a file;
openssl caaccepts a key URI instead of a file name in-keyfile. Many tokens refuse to export keys, which makes the token a single point of failure unless you plan backup with the token's own documentation. - Encrypted files on removable media. The minimum: an encrypted key on two USB sticks in two places, decrypted only on a machine that is offline at the time.
Backups and recovery tests
Back up the root key and the CA database (index.txt, serial, crlnumber and issued certificates) together, because a key restored without its database reuses serial numbers and forgets what you revoked. Keep the passphrase apart from the media. Once a year, restore from the second copy on the offline machine, check the key matches the root certificate, and generate a fresh CRL.
# Run on the offline machine: confirm the restored key matches the root certificate
openssl pkey -in private/root.key -pubout | openssl sha256
openssl x509 -in root.crt -noout -pubkey | openssl sha256
The two digests must be identical.
Ceremony notes
A homelab key ceremony is a text file you append to every time the root key is used:
| Field | Example |
|---|---|
| Date and time | 2026-10-25 14:10 local |
| Purpose | Sign intermediate "Home Lab Issuing CA 2026", issue root CRL number 1001 |
| Hardware | ThinkPad with Wi-Fi card removed, live USB image version and checksum |
| Software | openssl version output |
| Inputs | SHA-256 of int.csr as copied from the CA server |
| Commands | The exact commands run |
| Outputs | SHA-256 fingerprint of int.crt, CRL number, next update date |
| Backups updated | Which media, where stored |
| Next action due | Intermediate expiry date minus 60 days; CRL next update minus 30 days |
How does an internal ACME server issue certificates?
ACME (RFC 8555, March 2019) does not care whether the CA is public. A client fetches a directory, registers an account key, places an order for one or more identifiers, proves control of each identifier with a challenge, sends a CSR to the order's finalize URL, and downloads the certificate. A private ACME server signs with your intermediate, and any RFC 8555 client can use it once it is given the directory URL and trusts your root. The live posts linked above walk through the message flow; this section is about what changes inside a LAN.
HTTP-01 and TLS-ALPN-01 inside a LAN
HTTP-01 has the ACME server fetch a token over port 80; TLS-ALPN-01 (RFC 8737) has it complete a special handshake on port 443. Both need the ACME server to resolve the name through your internal resolver and reach the host directly, which rules out isolated VLANs, hosts on other ports, and wildcards.
DNS-01 with a DNS server you run
RFC 8555 Section 8.4 defines DNS-01. The client computes the key authorization (the challenge token joined to the base64url encoded thumbprint of the account key), takes its SHA-256 digest, base64url encodes the result, and publishes it as a TXT record at the validation name formed by prepending _acme-challenge to the identifier. For nas.home.arpa that is _acme-challenge.nas.home.arpa. The server then queries that name and compares.
On a LAN you need an authoritative DNS server for your internal zone and a way for clients to add and remove that record. RFC 2136 (April 1997), DNS UPDATE, defines the add and delete message but leaves authentication to other specifications, so updates are signed with TSIG, a shared secret HMAC scheme now specified in RFC 8945 (November 2020).
Security then rests on two things. First, what each TSIG key may change: a key that can update any record in home.arpa can redirect any service, so use BIND's update-policy to allow only TXT at one name. Second, which resolver the ACME server queries: it must reach your authoritative server, never a public resolver. RFC 8555 Section 7.1.3 represents a wildcard order as an authorization for the base name with a wildcard flag, and in practice wildcards are validated through DNS-01.
Restricting who can get certificates
Anything that can reach the ACME directory can create an account. Narrow that with an issuance policy on the server that allows only your domains and subnets, with name constraints on the intermediate as a backstop if that policy has a bug, and, where the server supports it, External Account Binding (RFC 8555 Section 7.3.4), which requires a MAC made with a key you hand out out of band.
RFC 9773 (June 2025) adds ACME Renewal Information: the server suggests a renewal window per certificate, so it can ask every client to renew early after you replace an intermediate. Check that both your client and server support it before relying on it.
Which open source ACME CAs exist?
We describe categories, not products, and list one project only where we use its documentation for the walkthrough.
| Category | What it is | Fits a homelab when |
|---|---|---|
| Web server with a built in CA and ACME server | A web server that runs an internal CA and exposes an ACME endpoint. Caddy's acme_server is one; its documented options include lifetime (default 12 hours), resolvers, challenges (http-01, tls-alpn-01, dns-01) and allow or deny policy. | You want one small binary |
| Standalone online CA server | A CA daemon that speaks ACME and often other enrollment methods, with its own policy engine and database. | You want mTLS for many services or several enrollment methods |
| Secrets manager with a PKI engine | A secrets server whose PKI backend issues certificates through its API, and in some versions through ACME. | You already run one; see our homelab secrets server guide and confirm ACME support in its docs |
| Public CA software released as open source | Many services, a relational database, hardware key storage. | Almost never at home |
| Test only ACME servers | Built to test ACME clients, often without persistence. | Client development only |
Why short-lived certificates?
Because revocation is the weakest part of any PKI, and short lifetimes make it matter less.
A leaked key stays usable until its certificate expires. Revocation is meant to close that gap, but clients often skip the check, cache answers or fail open, and at home few clients check a private CA at all. A leaf that lives 24 hours stops working a day after a leak whatever the client does.
Public certificate lifetimes under ballot SC-081v3
The CA/Browser Forum's Server Certificate Working Group adopted ballot SC-081v3, "Introduce Schedule of Reducing Validity and Data Reuse Periods." The published results record 25 certificate issuers voting yes, none voting no and 5 abstaining, and all 4 certificate consumers voting yes. The schedule now sits in Section 6.3.2 of the Baseline Requirements (version 2.3.1, 4 October 2026):
| Certificate issued | Must not exceed | Should not exceed | Domain and IP validation reuse |
|---|---|---|---|
| Before 15 March 2026 | 398 days | 397 days | 398 days |
| 15 March 2026 to 14 March 2027 | 200 days | 199 days | 200 days |
| 15 March 2027 to 14 March 2029 | 100 days | 99 days | 100 days |
| From 15 March 2029 | 47 days | 46 days | 10 days |
They also define a Short-lived Subscriber Certificate: from 15 March 2026, one with a validity period of 7 days (604,800 seconds) or less (10 days before that date). Section 4.9.1.1 says the CA "MAY support revocation" of these, and the usual 24 hour and 5 day revocation deadlines exclude them. In other words, the public PKI itself now treats a lifetime of a week as a substitute for revocation.
What lifetimes should a homelab use?
Your private CA is not bound by the Baseline Requirements, so the choice is yours. Two worked examples show how the numbers interact.
Worked example 1: a 48 hour leaf with Certbot. Certbot's documentation (from version 4.0.0) says a certificate is due when less than a third of its lifetime remains, or half for certificates of 10 days or less. A 48 hour certificate is due at 24 hours. With certbot renew running every 12 hours, the first attempt lands between 24 and 36 hours, so the ACME server can be unreachable for about 12 hours before a service fails. A 7 day leaf on the same rule is due at 3.5 days and tolerates an outage of about 3 days.
Worked example 2: leaf against intermediate in Caddy. Caddy's documentation warns that the leaf lifetime must stay below renewal_window_ratio times the intermediate lifetime. Suppose the ratio were 0.2 and the intermediate lasted 730 days: the ceiling is 146 days, far above a 7 day leaf. The binding limit at home is manual: re-sign the intermediate with the offline root well before expiry, because the ACME server cannot renew its own intermediate without the root key.
A calm default for a homelab: 2 to 7 day leaves for services, 24 hours for anything that renews itself through a reverse proxy, a 2 year intermediate renewed 60 days early, and a 20 year root.
How do you distribute the root and rotate it?
A private CA works only where its root is trusted, and every platform has its own store.
Hosts, browsers and phones
On Debian and Ubuntu, copy the root as a .crt file into /usr/local/share/ca-certificates/ and run update-ca-certificates, which rebuilds /etc/ssl/certs/ca-certificates.crt. Windows, macOS and the Fedora family have their own store tools, and Firefox can keep its own store. On Android, the default network security configuration for apps targeting API level 24 or later trusts only system CAs, so a user installed root often works in the browser and not in apps. On iOS and iPadOS, Apple says a manually installed root needs "Enable full trust for root certificates" switched on under Certificate Trust Settings. Our home CA walkthrough has the commands and menus for each platform. The place people forget is containers.
Containers
A container image carries its own filesystem, so it carries its own trust store. Installing the root on the Docker host changes nothing inside an image built from a public base. Worse, several language runtimes ignore the operating system bundle entirely.
| Runtime | Where it looks for trusted roots | How to add your root |
|---|---|---|
| Debian or Ubuntu based image (OpenSSL, curl, most C programs) | /etc/ssl/certs/ca-certificates.crt | Copy the .crt to /usr/local/share/ca-certificates/ and run update-ca-certificates at build time |
| Go programs | System locations, overridable with SSL_CERT_FILE and SSL_CERT_DIR | Use the image's system bundle after the step above, or point SSL_CERT_FILE at a bundle that includes your root |
| Node.js | Its own bundled root list by default | NODE_EXTRA_CA_CERTS=/path/to/root.crt adds roots to the bundled list |
| Python Requests (and tools built on it, including Certbot) | Its own bundle, not the operating system store | REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt after adding your root to the system bundle |
| Java | The JDK's cacerts keystore | keytool -importcert into cacerts at build time |
Add the root once, at build time, in a shared base image. Point variables that replace a bundle at one holding both public roots and yours, or the container can no longer reach public services.
Revoking an intermediate
Revocation at home is mostly about the intermediate. On the offline machine, run openssl ca -revoke on the intermediate's certificate with a reason, generate a new CRL with openssl ca -gencrl, and publish it at the URL in the intermediate's crlDistributionPoints. Many clients will never fetch it, so the dependable part of recovery is creating a new intermediate and letting short lived leaves move onto it within a day or two.
Rotating the root
A root rollover works best with overlap:
- Create the new root on the offline machine, with a different subject name (for example including the year) so the two are easy to tell apart.
- Distribute the new root to every trust store while the old one is still installed. Both are trusted for a while.
- Sign a new intermediate with the new root and load it into the ACME server. Leaves renew onto the new chain within one lifetime.
- Keep the old root installed until no certificate chains to it, then remove it everywhere and destroy its key with a ceremony note.
The openssl ca manual has a detail that helps here: the unique_subject option defaults to yes, which requires every valid certificate in the database to have a unique subject. The manual recommends no to make CA certificate rollover easier, so a new intermediate with the same subject can be issued before the old one expires.
Which approach should you choose?
| Self-signed per service | Private CA, manual issuance | Private CA with ACME | Public CA with DNS-01 | |
|---|---|---|---|---|
| Trust setup | Every certificate installed on every client, or warnings clicked | One root installed on every client and container | One root installed on every client and container | None, already trusted |
| Names it can cover | Anything | Anything you choose, limited by name constraints | Anything your ACME policy and name constraints allow | Only names under a public domain you control |
| Hostname privacy | Private | Private | Private | Logged publicly in Certificate Transparency |
| Automation | None | Scripts you write | Standard ACME clients and reverse proxies | Standard ACME clients |
| Typical leaf lifetime | Years, because renewal is manual | Months to years | Hours to days | Up to 200 days today, 47 days from 15 March 2029 |
| Revocation | Remove the certificate from each client | CRL you maintain | Mostly unnecessary because leaves expire fast | Public CA's CRL and OCSP |
| Blast radius if a key leaks | One service | Root or intermediate key can impersonate any permitted name to every client | Same, reduced by name constraints and short leaves | One certificate, until it expires or is revoked |
Most homelabs end up with a mix: a public CA with DNS-01 for the few services visitors reach from the internet, and a private CA with ACME for everything internal.
Step by step: offline root, constrained intermediate, ACME over DNS-01, automatic renewal
This walkthrough uses OpenSSL for the root and intermediate, BIND as the internal authoritative DNS server, Caddy's ACME server as the issuing CA, and Certbot with its RFC 2136 plugin as the ACME client in front of an nginx reverse proxy. The example network is 192.168.10.0/24, the internal zone is home.arpa, the CA server is ca.home.arpa and the DNS server is 192.168.10.53. Substitute your own names, and check every option against the documentation for the versions you install.
Step 1: prepare the offline root directory
Use a machine with networking disabled. If you are going to format a USB stick to carry files, identify it first.
Warning: formatting a drive destroys its contents. Before any command that writes a filesystem, run
lsblk -o NAME,SIZE,MODEL,MOUNTPOINT, unplug and replug the stick, run it again, and confirm which device appeared. Use that exact path and nothing else.
lsblk -o NAME,SIZE,MODEL,MOUNTPOINT
# Confirm the device, for example /dev/sdb, before formatting it.
Create the CA directory and its database files. The openssl ca manual says the database file is mandatory and may start empty, the serial file must contain a valid hex serial number, and CRL numbers are added only if the crlnumber file exists.
umask 077
mkdir -p ~/rootca/{private,db,newcerts,crl}
cd ~/rootca
touch db/index.txt
openssl rand -hex 16 > db/serial
echo 1000 > db/crlnumber
Write ~/rootca/root.cnf:
[ ca ]
default_ca = root_ca
[ root_ca ]
dir = /home/admin/rootca
database = $dir/db/index.txt
serial = $dir/db/serial
crlnumber = $dir/db/crlnumber
new_certs_dir = $dir/newcerts
certificate = $dir/root.crt
private_key = $dir/private/root.key
default_md = sha384
default_days = 730
default_crl_days = 365
unique_subject = no
copy_extensions = none
email_in_dn = no
policy = policy_home
[ policy_home ]
organizationName = optional
commonName = supplied
[ req ]
distinguished_name = req_dn
prompt = no
[ req_dn ]
O = Home Lab
CN = Home Lab Root CA 2026
[ v3_root ]
basicConstraints = critical, CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
[ v3_intermediate ]
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, keyCertSign, cRLSign
extendedKeyUsage = serverAuth, clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
crlDistributionPoints = URI:http://ca.home.arpa/crl/root.crl
nameConstraints = critical, @home_constraints
[ home_constraints ]
permitted;DNS.0 = home.arpa
permitted;IP.0 = 192.168.10.0/255.255.255.0
[ crl_ext ]
authorityKeyIdentifier = keyid:always
The settings that matter: copy_extensions = none so nothing in a CSR can grant itself CA powers, unique_subject = no so rollover is easier, and the v3_intermediate section with pathlen:0 and critical name constraints. The x509v3_config manual documents the permitted; and excluded; prefixes and the address and mask form for IP constraints.
Step 2: create the root key and self signed certificate
cd ~/rootca
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 \
-aes-256-cbc -out private/root.key
openssl req -config root.cnf -new -key private/root.key -out root.csr
openssl ca -config root.cnf -selfsign -in root.csr \
-extensions v3_root -days 7300 -out root.crt
genpkey writes an encrypted P-384 key and prompts for a passphrase. openssl ca -selfsign signs the request with the key that created it, which the manual notes puts the root into the CA database and uses the same serial counter as everything the root signs later. That keeps one record of every certificate the root has ever issued, itself included. The -days 7300 gives about 20 years. For the reasoning behind curve choice, see ECDSA or RSA for TLS certificates.
Record the fingerprint in your ceremony notes:
openssl x509 -in root.crt -noout -subject -dates -fingerprint -sha256
Step 3: create the intermediate key on the CA server
On ca.home.arpa, create the intermediate key and request. The key stays on this machine.
sudo install -d -m 700 /etc/homeca
cd /etc/homeca
sudo openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out int.key
sudo openssl req -new -key int.key \
-subj "/O=Home Lab/CN=Home Lab Issuing CA 2026" -out int.csr
sha256sum int.csr
The intermediate key is unencrypted because the ACME server reads it unattended; protect it with file permissions, disk encryption and a network segment untrusted devices cannot reach. Copy int.csr to the USB stick and note its digest.
Step 4: sign the intermediate and issue the first CRL
Back on the offline machine, compare the digest of the CSR you copied, then sign:
cd ~/rootca
sha256sum /media/usb/int.csr
openssl req -in /media/usb/int.csr -noout -text | head -20
openssl ca -config root.cnf -extensions v3_intermediate \
-days 730 -in /media/usb/int.csr -out int.crt
openssl ca -config root.cnf -gencrl -crlexts crl_ext -out crl/root.crl
cp int.crt root.crt crl/root.crl /media/usb/
openssl ca shows the subject and asks for confirmation before signing. Read it. The CRL is valid for default_crl_days (365 days here), so add a reminder to regenerate it within a year. Append the outputs and fingerprints to the ceremony notes, update both backups with the new database files, and shut the machine down.
Step 5: prove the chain and the name constraints
On any machine, check the chain and run a negative test that shows the constraint working. The negative test signs a throwaway certificate for a name outside your domain with the intermediate key, so run it on the CA server and delete the output afterwards.
openssl verify -CAfile root.crt -untrusted int.crt int.crt
# Negative test: a leaf for a name outside home.arpa must fail verification
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout /tmp/out.key -subj "/CN=outside.example" \
-addext "subjectAltName=DNS:outside.example" -out /tmp/out.csr
openssl x509 -req -in /tmp/out.csr -CA int.crt -CAkey int.key \
-days 1 -copy_extensions copy -out /tmp/out.crt
openssl verify -CAfile root.crt -untrusted int.crt /tmp/out.crt
shred -u /tmp/out.key /tmp/out.csr /tmp/out.crt
The last verify must fail with a name constraints error. If it succeeds, the constraint is not in the intermediate; stop and fix it.
Step 6: configure BIND for authenticated updates to challenge records
On the DNS server, generate a TSIG key for each ACME client and allow it to change only the TXT record at its own challenge name. BIND's tsig-keygen prints a key statement ready to include.
sudo tsig-keygen -a hmac-sha256 acme-nas | sudo tee /etc/bind/acme-nas.key
sudo chown root:bind /etc/bind/acme-nas.key
sudo chmod 640 /etc/bind/acme-nas.key
In named.conf.local:
include "/etc/bind/acme-nas.key";
zone "home.arpa" {
type primary;
file "/var/lib/bind/db.home.arpa";
update-policy {
grant acme-nas name _acme-challenge.nas.home.arpa. TXT;
};
};
The name rule type limits the key to exactly that owner name, and the trailing TXT limits the record type. Check and reload:
sudo named-checkconf
sudo rndc reload
A zone with update-policy is dynamic, so BIND keeps a journal; use rndc freeze and rndc thaw around any manual edit of the zone file.
Step 7: run the ACME server with DNS-01 validation
Install Caddy from its official packages on ca.home.arpa. Copy root.crt and int.crt into /etc/homeca beside int.key, make the key readable by the caddy user only, and write /etc/caddy/Caddyfile:
{
pki {
ca home {
name "Home Lab CA"
root {
format pem_file
cert /etc/homeca/root.crt
key /etc/homeca/int.key
}
intermediate {
format pem_file
cert /etc/homeca/int.crt
key /etc/homeca/int.key
}
}
}
}
ca.home.arpa {
tls {
issuer internal {
ca home
}
}
acme_server {
ca home
lifetime 48h
challenges dns-01
resolvers 192.168.10.53:53
allow {
domains home.arpa *.home.arpa
}
}
handle_path /crl/* {
root * /srv/crl
file_server
}
}
Caddy's global options documentation describes this offline root pattern: load the intermediate certificate and key, supply only the root certificate for chain building, and because the Caddyfile still expects a key under root, point it at a readable PEM file such as the intermediate key; Caddy does not sign with it. challenges dns-01 turns off the other challenges, and resolvers sends validation queries to your BIND server. Copy root.crl into /srv/crl/, then:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl enable --now caddy
curl --cacert /etc/homeca/root.crt https://ca.home.arpa/acme/home/directory
The last command should print the ACME directory as JSON.
Step 8: request and renew with Certbot and the RFC 2136 plugin
On the reverse proxy host, install the root into the system store, install Certbot and its dns-rfc2136 plugin from your distribution or the project's documented method, and create the credentials file. The plugin documentation lists these keys and warns that the TSIG secret can be used to add, update or delete DNS records, so protect it.
sudo cp root.crt /usr/local/share/ca-certificates/home-root.crt
sudo update-ca-certificates
sudo install -m 600 /dev/null /etc/letsencrypt/rfc2136.ini
sudo tee /etc/letsencrypt/rfc2136.ini >/dev/null <<'EOF'
dns_rfc2136_server = 192.168.10.53
dns_rfc2136_port = 53
dns_rfc2136_name = acme-nas.
dns_rfc2136_secret = PASTE_THE_SECRET_FROM_acme-nas.key
dns_rfc2136_algorithm = HMAC-SHA256
EOF
Certbot uses the Requests library, which does not read the operating system trust store. Its documentation says to set REQUESTS_CA_BUNDLE globally, not only on the command line, or scheduled renewals will fail. Point it at the system bundle, which now contains your root and the public roots:
sudo systemctl edit certbot.service
# Add:
# [Service]
# Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Request the first certificate:
sudo REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt certbot certonly \
--server https://ca.home.arpa/acme/home/directory \
--dns-rfc2136 --dns-rfc2136-credentials /etc/letsencrypt/rfc2136.ini \
--dns-rfc2136-propagation-seconds 10 \
--key-type ecdsa -m admin@home.arpa --agree-tos \
-d nas.home.arpa
The plugin waits 60 seconds by default; 10 is plenty when the ACME server queries your authoritative server directly. Renewals reuse the same server and plugin. To reload nginx only after a successful renewal, add a script to the deploy hooks directory:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx >/dev/null <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx
Point nginx at /etc/letsencrypt/live/nas.home.arpa/fullchain.pem and privkey.pem. Check that renewal is scheduled with systemctl list-timers, and dry run it:
sudo certbot renew --dry-run
Step 9: make your containers trust the root
In a shared base image:
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY home-root.crt /usr/local/share/ca-certificates/home-root.crt
RUN update-ca-certificates
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/home-root.crt
NODE_EXTRA_CA_CERTS adds to Node's bundled list rather than replacing it, so pointing it at your root alone is correct. REQUESTS_CA_BUNDLE and SSL_CERT_FILE replace the bundle, so they point at the merged system file. For a Java image, import the root into the JDK keystore at build time with keytool -importcert -cacerts -alias homelab-root -file home-root.crt -noprompt, supplying the keystore password your JDK uses.
Test from inside a container:
docker run --rm my-base:latest \
sh -c 'apt-get update >/dev/null 2>&1; curl -sS -o /dev/null -w "%{http_code}\n" https://nas.home.arpa/'
Step 10: monitor expiry for the pieces that do not renew themselves
Leaves renew on their own. The intermediate and the root CRL do not. A daily check on the CA server catches both:
openssl x509 -in /etc/homeca/int.crt -noout -checkend $((60*86400)) \
|| echo "Intermediate expires within 60 days: schedule a root ceremony"
openssl crl -in /srv/crl/root.crl -noout -nextupdate
-checkend exits non zero if the certificate expires within the given number of seconds. Feed both results into whatever alerting you already run.
Common mistakes
- Keeping the root key on the online CA server. It turns any compromise of that server into a reinstall on every device. The ACME server only needs the intermediate key and the root certificate.
- No name constraints on the intermediate. Without them, anyone who gets the intermediate key can issue a certificate for your bank's domain that all of your devices accept. Constraints on the root alone are not reliably enforced, because the root is a trust anchor.
- Constraining DNS names but not IP addresses. Constraints apply per name type. If your leaves never carry IP SANs, add a narrow IP permitted subtree anyway, so a stolen key cannot mint IP certificates for other networks.
- Ten year leaf certificates. They mean renewal is never tested, keys are never rotated, and a leaked key works for a decade. Use days, not years.
copy_extensions = copyallin a CA config. The OpenSSL manual spells out the risk: a request carryingCA:TRUEbecomes a CA certificate if nobody notices.- A TSIG key with rights over the whole zone. A key that can change any record in
home.arpacan redirect every service. Grant it one name and one record type. - Setting
REQUESTS_CA_BUNDLEonly on the command line. The first request works, and the scheduled renewal fails silently, because the timer runs without that variable. - Forgetting to trust the root inside containers. The host trusts it, the container does not, and the application logs a verification error that looks like a network fault.
- Forgetting the intermediate's expiry. Leaves renew themselves for two years, then every service fails on the same day because the intermediate expired. Put the date in ceremony notes and in monitoring.
- Never testing the root backup. The day you need it is the wrong day to learn the passphrase was wrong.
FAQ
Is a homelab certificate authority safe to install on my devices?
It is as safe as its keys. An unconstrained CA key can vouch for any website to your devices. Keep the root offline and name constrain the intermediate, and a CA server compromise is limited to your own names.
Should I put name constraints on the root or the intermediate?
The intermediate. RFC 5280 does not apply name constraints to self issued certificates unless they end the path, and a root is the starting trust anchor. A critical constraint on the intermediate is enforced by every conforming verifier.
Can I use ACME DNS-01 without a public domain?
Yes, against your own ACME server. Run an authoritative DNS server for your internal zone, let clients publish _acme-challenge TXT records through TSIG signed RFC 2136 updates, and point the ACME server's resolver at it.
How long should homelab certificates last?
Root certificate 10 to 20 years, intermediate 1 to 3 years (matching NIST SP 800-57's suggested 1 to 3 year usage period for a private signature key), and leaves between a few hours and 7 days. Seven days matches what the CA/Browser Forum now defines as a short lived certificate for which revocation is optional.
What is CA/Browser Forum ballot SC-081?
The ballot that cut the maximum lifetime of publicly trusted TLS certificates to 200 days from 15 March 2026, 100 days from 15 March 2027 and 47 days from 15 March 2029. It binds public CAs, not yours.
Do I need OCSP or a CRL for a home CA?
A root CRL covering the intermediate costs one command a year and is worth having. OCSP for leaves rarely is, because short lived leaves expire before revocation would help.
Why does my container say "unable to get local issuer certificate" when the host trusts my CA?
The container has its own trust store from its base image. Add your root at build time with update-ca-certificates, and set NODE_EXTRA_CA_CERTS or REQUESTS_CA_BUNDLE for runtimes with their own bundle.
How do I rotate my home root CA?
Create a new root offline, install it beside the old one everywhere, move the ACME server to a new intermediate under it, wait for leaves to renew, then remove the old root.
Where Safeguard fits
Safeguard does not run certificate authorities, manage certificates or trust stores, protect CA keys, operate DNS, or monitor certificate expiry, and nothing in this guide depends on it. Its fit is indirect and sits on the software side. A homelab CA is a stack of open source software: OpenSSL, a DNS server, an ACME server and its Go dependencies, an ACME client and its Python dependencies, and the container images you just rebuilt to trust the root. Safeguard can scan a repository, a container image or an SBOM file (for example safeguard scan --dir . in the folder holding your compose files and Dockerfiles, or safeguard scan --image <name> for the base image that now carries your root), match components against vulnerability databases, show which projects contain a given component and version, and, with your approval, open fix pull requests. Keeping the software that holds your intermediate key and TSIG secrets current is part of protecting them.
Fix it without waiting for upstream
A CA server, a DNS server and every container that trusts your root are rebuilt from open source packages and images, and each rebuild pulls whatever versions resolve that day. The section below describes how Safeguard helps keep those components on fixed versions, including when upstream has not shipped a fix yet; fork and patch is early access.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
These demos show Safeguard working on software projects, not on certificates or keys. They apply to the repositories, Dockerfiles and container images that make up your CA server, DNS server and reverse proxies.
The first demo is a real Safeguard CLI session. After install, safeguard opens the interactive terminal, you ask it to fix the lodash CVE in a repository, and it reads the finding and advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. The same ask, approve and test loop applies to a small internal web app you now serve behind your private CA.
Click through it at your own pace, or open it full screen.
A homelab CA lives on a network nobody outside can reach. The local runner demo shows how to set up the runner on your own machine, approve it with its device code, and then scan from the terminal. A runner on the host that builds your shared base image can scan that image after you add the root, without opening anything to the internet.
Click through it at your own pace, or open it full screen.
The last demo is a real Android session: you send work to a computer running the Safeguard CLI and approve each command on the phone with Allow once, Allow for turn or Deny. Approving one command at a time is the same habit as a root key ceremony: nothing privileged happens without a deliberate yes.
Click through it at your own pace, or open it full screen.
To try it on your own repositories, download Safeguard and follow the CLI documentation.
Quick check
Your intermediate has a permitted DNS subtree of home.arpa and no IP constraint. Can a stolen intermediate key mint a certificate for 10.0.0.5 that your devices accept?
Yes. Name constraints apply per name type, so with no IP subtree listed, IP address names are unconstrained. Add a permitted IP subtree for your lab subnet to close that gap.
A leaf certificate lives 48 hours and the client renews when half its lifetime remains. How long can the ACME server be down before services start failing?
Roughly 24 hours, minus however long the renewal timer waits between runs. Renewal becomes due at 24 hours, so an outage that starts then has the remaining 24 hours of validity, less one timer interval, before the certificate expires.
Sources
- RFC 5280: Internet X.509 Public Key Infrastructure Certificate and CRL Profile
- RFC 8555: Automatic Certificate Management Environment (ACME)
- RFC 9773: ACME Renewal Information (ARI) Extension
- RFC 2136: Dynamic Updates in the Domain Name System (DNS UPDATE)
- RFC 8945: Secret Key Transaction Authentication for DNS (TSIG)
- RFC 8375: Special-Use Domain 'home.arpa.'
- CA/Browser Forum: TLS Baseline Requirements
- CA/Browser Forum TLS Baseline Requirements source, version 2.3.1
- CA/Browser Forum: Ballot SC081v3, Introduce Schedule of Reducing Validity and Data Reuse Periods
- NIST SP 800-57 Part 1 Rev. 5: Recommendation for Key Management, Part 1: General
- OpenSSL 3.0 documentation: openssl-ca
- OpenSSL 3.0 documentation: x509v3_config
- Caddy documentation: acme_server directive
- Caddy documentation: global options and PKI options
- OpenSSL 3.0 documentation: openssl-x509
- Oracle Java SE 21: keytool
- Android Developers: Network security configuration
- Apple Support: Trust manually installed certificate profiles
- Certbot documentation: User Guide
- certbot-dns-rfc2136 plugin documentation
- BIND 9 Administrator Reference Manual: configuration reference
- Debian manual page: update-ca-certificates(8)
- Node.js documentation: command line options and environment variables
- Go standard library: crypto/x509
- Requests documentation: advanced usage
Last reviewed: 25 October 2026. Tool options and defaults change between releases; check the documentation for the versions you run, and confirm the device path before any command that formats a drive.