Safeguard
Supply Chain Security•

Run Your Own Certificate Authority at Home: ACME and Trust Stores

Keep an offline root, sign a name constrained intermediate, issue short lived certificates over ACME on your LAN, then install the root on every device.

Safeguard Security Team
45 min read

To run your own certificate authority at home, create a root on an offline machine, use it to sign an intermediate name constrained to your internal domain, and load that intermediate into an ACME server on your LAN. Reverse proxies then renew short lived certificates automatically, and you install the root once on every device.

This guide goes one level deeper than our posts on how ACME issues certificates, the three ACME challenge types and name constraints on a private CA. It pairs with running your own DNS resolver at home, because a private CA is only as useful as the internal names it certifies. Every protocol rule below comes from the IETF RFCs, every lifetime rule from the CA/Browser Forum's TLS Baseline Requirements (version 2.3.1, dated 4 October 2026), and every command from the OpenSSL, Caddy, Debian, Fedora, Mozilla, Microsoft, Apple and Android documentation. Commercial certificate products and services appear only as categories.

Key takeaways

  • A private CA makes sense for names that public CAs cannot or should not certify: home.arpa (reserved for home networks by RFC 8375), .internal (reserved by ICANN for private use in July 2024), bare hostnames, and mutual TLS between your own services.
  • If you own a public domain and are happy for hostnames to appear in public Certificate Transparency logs, a public CA with the DNS-01 challenge is less work, because every device already trusts it.
  • Build two tiers: a root whose key lives offline and encrypted, and an online intermediate with pathlen:0 and a nameConstraints extension that limits it to your internal names. Put the constraints on the intermediate, because RFC 5280 treats a root as plain trust anchor input.
  • Run ACME (RFC 8555) against the intermediate. On a LAN, HTTP-01 is the simplest challenge, TLS-ALPN-01 works when only port 443 is open, and DNS-01 is the only route to wildcards and to hosts that are not reachable from the CA.
  • Public TLS certificates are getting shorter by rule: 200 days maximum from 15 March 2026, 100 days from 15 March 2027 and 47 days from 15 March 2029 (CA/Browser Forum ballot SC-081v3). Internal automation that already renews every few hours or days is ready for that world.
  • Every platform keeps its own trust list. Linux, Windows and macOS have system stores, Firefox has its own, Android apps targeting API level 24 or later ignore user installed CAs unless the app opts in, and iOS needs a manual "full trust" switch after a profile is installed by hand.

Why run a private CA in a homelab?

Three situations push a homelab toward its own CA.

Internal names with no public DNS. Services such as nas.home.arpa, grafana.lab.internal or printer do not exist on the public internet. A publicly trusted CA has to validate control of a name through the public DNS or a public HTTP endpoint, so it cannot issue for names it cannot see. RFC 8375 (May 2018) set aside home.arpa. for residential home networks, and on 29 July 2024 the ICANN Board resolved to reserve .INTERNAL permanently from delegation in the DNS root zone "for its use in private-use applications". Both are good choices for a lab precisely because no public CA will ever issue for them, which also means no one else can obtain a trusted certificate for your internal names through a public CA.

Mutual TLS between your own services. When a backup agent talks to a backup server, or an MQTT client talks to a broker, you may want each side to present a certificate. Client certificates for your own machines are not something the public Web PKI is designed to provide, and you want to decide yourself which identities exist. A private CA lets you issue clientAuth certificates to exactly the machines you choose and revoke them when a machine leaves.

Privacy of hostnames. Publicly trusted certificates are logged in Certificate Transparency logs, which anyone can search. If your hostnames reveal what you run (vaultwarden.example.net, jellyfin.example.net), issuing them from a public CA publishes that inventory. A private CA issues certificates nobody else sees.

When is a public certificate with DNS-01 the better choice?

If you own a real domain, a public CA with the DNS-01 challenge is often the better answer, and it is worth being honest about that before building anything. DNS-01, defined in Section 8.4 of RFC 8555, proves control by publishing a TXT record at _acme-challenge.<your name>. The CA never needs to connect to your server, so a host that is only reachable on your LAN can still get a publicly trusted certificate, as long as your DNS provider has an API your ACME client can drive. You can serve nas.lab.example.net only on your internal resolver and still get a certificate every browser, phone, television and smart speaker already trusts.

The trade-offs: hostnames land in public CT logs (a wildcard certificate such as *.lab.example.net limits what you reveal), you need a DNS API token sitting on a machine in your lab (scope it to one zone if your provider allows it), and you depend on an external service being reachable when renewal runs. The decision table later in this post compares the options side by side.

A rough rule: if every client is a browser or a phone you do not want to configure, use a public CA with DNS-01. If you have internal only names, need client certificates, or run devices and containers you can configure, a private CA is worth it.

How do you build a sensible CA hierarchy?

A homelab CA should have two tiers, the same shape every public CA uses, for the same reason: the most valuable key should almost never be used.

TierWhat it doesWhere the key livesSuggested lifetimeKey extensions
Root CASigns intermediates and their CRLs, nothing elseOffline: encrypted file on removable media, or a hardware token10 yearsbasicConstraints = critical, CA:true; keyUsage = critical, keyCertSign, cRLSign
Intermediate (issuing) CASigns leaf certificates through ACMEOn the CA server, readable only by the CA process1 to 2 years, renewed by handbasicConstraints = critical, CA:true, pathlen:0; nameConstraints = critical, ...; keyUsage = critical, keyCertSign, cRLSign
Leaf certificatesIdentify a service or a clientOn each serviceHours to daysbasicConstraints = CA:false; extendedKeyUsage = serverAuth (and clientAuth for mTLS); SAN with DNS names or IPs

Basic constraints and path length

RFC 5280 Section 4.2.1.9 defines basicConstraints. The cA boolean says whether the certified key may sign certificates, and pathLenConstraint limits how many non self issued intermediates may follow. A pathLenConstraint of zero means the intermediate can sign leaf certificates but no further CAs. That is exactly what you want for an issuing CA: if its key leaks, the attacker cannot mint a new sub CA with fresh constraints. RFC 5280 also says the field is meaningful only when cA is true, and conforming CAs mark the extension critical in CA certificates.

Name constraints, and why they belong on the intermediate

The name constraints extension (RFC 5280 Section 4.2.1.10) "indicates a name space within which all subject names in subsequent certificates in a certification path MUST be located". You list permitted subtrees (for example the DNS name home.arpa and the IP range 192.168.0.0/16) and excluded subtrees. For DNS names, a constraint matches the name itself and any name formed by adding labels to the left, so a permitted home.arpa covers nas.home.arpa and a.b.home.arpa but not home.arpa.evil.example. The RFC requires conforming CAs to mark the extension critical, forbids an empty extension, and says a client that sees a critical name constraint on a name form it meets must either process it or reject the certificate.

Two details decide where the extension goes.

First, RFC 5280 says name constraints "are not applied to self-issued certificates (unless the certificate is the final certificate in the path)". Second, in RFC 5280's path validation algorithm (Section 6.1.1) the trust anchor is an input made of a name and a public key; a self signed root certificate is just one convenient container for that input. Whether a verifier also enforces extensions inside the root certificate depends on the implementation. The intermediate, on the other hand, is an ordinary certificate in the path, and its constraints are processed by every conforming verifier. So put nameConstraints on the intermediate. You can add them to the root as well, but do not rely on that.

Why bother at all in a homelab? Because the root you install on your laptop and phone is trusted for every name on the internet unless something says otherwise. With a constrained intermediate and no other intermediates in existence, the worst case of a stolen issuing key is a forged certificate for your own internal names, not for your bank. Our name constraints post covers the client support picture in more depth.

Key types and lifetimes

Elliptic curve keys keep certificates and handshakes small. A sensible homelab pairing is ECDSA P-384 for the root and ECDSA P-256 for the intermediate and leaves; RSA 3072 or 4096 is a reasonable choice if you have old clients that do not handle ECDSA. NIST SP 800-57 Part 1 Revision 5 (May 2020) is the reference for choosing key sizes and for the idea of a cryptoperiod, the span of time a key is authorised for use. Its core advice applies to a lab as much as to an enterprise: keys that protect many other keys deserve the strongest protection and the shortest practical exposure, and every key should have a planned end of life.

Practical lifetimes:

  • Root: 10 years. Rotating a root means touching every device, so make it long, and protect it accordingly.
  • Intermediate: 1 or 2 years. You need the root to renew it, so the lifetime sets how often you bring the root out of storage. Put the expiry in your calendar 60 days early.
  • Leaves: as short as your automation can handle. Caddy's ACME server defaults to 12 hours. A few days is a good compromise for clients that renew less aggressively.

How does ACME work against a private CA?

ACME, the Automatic Certificate Management Environment (RFC 8555, March 2019), is the protocol behind automatic certificate issuance. Nothing in it is specific to public CAs, so any ACME client can talk to a private ACME server if it is told the directory URL and trusts the server's TLS certificate.

The flow is the same on a LAN as on the internet:

  1. Directory. The client fetches the directory object, a JSON document listing the URLs for newNonce, newAccount, newOrder, revokeCert and keyChange.
  2. Account. The client generates an account key pair and registers it with newAccount. Every later request is a JWS signed with that key and carries a fresh anti replay nonce from the server's Replay-Nonce header; a stale nonce gets a badNonce error and a new nonce to retry with.
  3. Order. The client submits newOrder with the identifiers it wants, such as dns: nas.home.arpa.
  4. Authorization and challenge. For each identifier the server returns an authorization with one or more challenges. The client picks one, provisions the response, and tells the server to check.
  5. Finalize. Once all authorizations are valid, the client sends a CSR to the order's finalize URL.
  6. Certificate. The server issues the certificate and the client downloads the chain.

The proof in each challenge is the key authorization: the challenge token joined with a dot to the base64url encoded thumbprint of the account key. That binds the proof to the account that asked for it.

RFC 8555 also defines External Account Binding (Section 7.3.4). The server can require that newAccount carry an externalAccountBinding field, a MAC computed with a key the CA operator gave you out of band. On a home network where any device can reach the ACME endpoint, external account binding or an IP allow list is how you stop a random device on the guest Wi-Fi from creating an account, although it still has to pass a challenge for each name.

Two later RFCs matter for a lab. RFC 8738 (February 2020) adds IP address identifiers, so a client can request a certificate for 192.168.1.10 and prove control with HTTP-01 or TLS-ALPN-01 (not DNS-01). RFC 9773 (June 2025) defines ACME Renewal Information (ARI): the server publishes a renewalInfo URL in its directory, and clients ask it for a suggested renewal window instead of guessing. ARI lets a CA spread renewals over time or ask clients to renew early after an incident. Check whether your ACME server and client support it before relying on it.

Which challenge types make sense on a LAN?

ChallengeWhat the CA checksPortWildcardsFits a LAN when
HTTP-01 (RFC 8555 Section 8.3)A file at http://<name>/.well-known/acme-challenge/<token> containing the key authorization80NoThe ACME server can resolve the name through your internal DNS and reach port 80 on the host
TLS-ALPN-01 (RFC 8737)A self signed certificate with a special extension, served during a TLS handshake that negotiates the acme-tls/1 protocol443NoOnly 443 is open, and the client controls the TLS listener (reverse proxies usually do)
DNS-01 (RFC 8555 Section 8.4)A TXT record at _acme-challenge.<name>None on the hostYesYou want wildcards, or the host is not reachable from the CA, and your internal DNS server accepts dynamic updates

Two things to remember. First, the private ACME server performs validation using whatever DNS it is configured with; if it uses a public resolver it will never find nas.home.arpa. Point it at your internal resolver. Second, RFC 8555 Section 7.1.3 handles wildcards by returning an authorization for the base name with a wildcard field set to true; in practice, public ACME CAs issue wildcards only through DNS-01, because HTTP-01 and TLS-ALPN-01 prove control of a single host rather than a whole namespace. Caddy's ACME server issues wildcards only if you opt in with allow_wildcard_names.

On a home network the security of HTTP-01 and TLS-ALPN-01 rests on your internal DNS and your LAN. Anyone who can answer DNS for your internal zone, or sit between the CA and the host, can pass the challenge. That is acceptable for a lab if your DNS is under your control and your Wi-Fi is not shared, and it is another reason to name constrain the intermediate.

Why are short lived certificates becoming normal?

Because the rules for public certificates now require it, and because short lifetimes reduce the damage when something goes wrong.

The CA/Browser Forum's Server Certificate Working Group passed ballot SC-081v3, "Introduce Schedule of Reducing Validity and Data Reuse Periods", in a vote that ran from 4 to 11 April 2025. According to the published results, 25 certificate issuers voted yes, none voted no and 5 abstained, and all 4 certificate consumers (the major browser and operating system root programs) voted yes. The intellectual property review period ended on 13 May 2025. The ballot now lives in Section 6.3.2 of the TLS Baseline Requirements.

Certificate issuedMaximum validityDomain and IP validation data reuse
Before 15 March 2026398 days398 days
15 March 2026 to 14 March 2027200 days200 days
15 March 2027 to 14 March 2029100 days100 days
From 15 March 202947 days10 days

The Baseline Requirements also say that a day is 86,400 seconds and that any extra time, including fractional or leap seconds, counts as another day, so CAs "SHOULD NOT" issue for the maximum by default. Separately, the requirements define a "Short-lived Subscriber Certificate" as one valid for 7 days or less if issued on or after 15 March 2026 (10 days before that), and CAs may choose not to support revocation for those.

The ballot's own rationale is worth reading. It argues that a certificate is a snapshot of facts at the moment of issuance, so the longer it lives, the more likely it describes a world that no longer exists: a domain that changed hands, a key someone else now holds. It also notes that revocation checking does not protect relying parties well at internet scale, and that shorter lifetimes push the ecosystem toward reliable automated renewal.

What does that mean for internal automation?

The Baseline Requirements apply to publicly trusted certificates. Your private CA is not bound by them, and nothing stops you issuing a 10 year leaf certificate for your NAS. You should not, for the same reasons the browsers gave:

  • Revocation barely works in a homelab. Many clients never check revocation for private CAs, and you are unlikely to run a highly available CRL or OCSP service. A 24 hour certificate that leaks is useless a day later, without any revocation infrastructure.
  • Renewal that runs every day is renewal that works. A certificate that renews once a year fails silently the one time you have forgotten how it was set up. One that renews twice a day is tested twice a day.
  • The same automation covers public certificates. If you also use public certificates for some services, they will need renewal at least every 100 days from March 2027 and every 47 days from March 2029. ACME clients that already handle internal certificates every few hours will not notice.

Short lifetimes do change the shape of the CA. The ACME server becomes a dependency: if it is down for longer than the remaining lifetime of your leaves, services start failing. Size lifetimes so that a weekend outage is survivable (leaves of 2 to 7 days with renewal at a third of lifetime remaining is a calm setting for a homelab), and monitor the CA like any other service.

How do you get devices to trust your root?

Installing the root is where most homelab CAs stall, because there is no single trust store. Install the root certificate only, never the intermediate, and never any private key.

PlatformWhere the trust livesHow to add your rootNotes
Debian, Ubuntu/etc/ssl/certs, built by update-ca-certificatesCopy to /usr/local/share/ca-certificates/ with a .crt extension, run sudo update-ca-certificatesFiles without .crt are ignored
Fedora, RHEL familyp11-kit trust, extracted by update-ca-trustCopy to /etc/pki/ca-trust/source/anchors/, run sudo update-ca-trust extractPEM or DER accepted
Archp11-kit trustCopy to /etc/ca-certificates/trust-source/anchors/, run sudo update-ca-trust extractSame tool, different path
WindowsLocal Machine "Trusted Root Certification Authorities"certutil -addstore -f Root root.crt or Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root (elevated)Group Policy for many machines
macOSSystem keychain trust settingssudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain root.crtOr Keychain Access, then set "Always Trust"
Firefox (all desktop OSes)Its own NSS storeEnterprise policy Certificates.Install, or ImportEnterpriseRoots on Windows and macOSImportEnterpriseRoots sets security.enterprise_roots.enabled
AndroidSystem store plus a separate user storeSettings, then install a CA certificate (menu path varies)Apps targeting API 24 or later ignore user CAs unless they opt in
iOS, iPadOSProfilesInstall a profile containing the root, then enable it under Certificate Trust SettingsManual installs need the extra trust switch
Containers, language runtimesImage or runtime specificCopy into the image's trust store at build time, or set the runtime's CA bundleSee our runtime trust post

Linux trust stores

On Debian and Ubuntu, update-ca-certificates rebuilds /etc/ssl/certs and the single file bundle ca-certificates.crt. Its manual page says certificates "must have a .crt extension in order to be included", and that every .crt file found below /usr/local/share/ca-certificates is "implicitly trusted". Before it exits it runs the hooks in /etc/ca-certificates/update.d, passing each one the certificates added and removed, which is how other trust stores on the system can be kept in step.

On Fedora and the Red Hat family, the shared system trust is managed by p11-kit. The update-ca-trust manual gives the quick path: put a PEM or DER file in /etc/pki/ca-trust/source/anchors/ and run update-ca-trust extract. Arch uses the same tool with /etc/ca-certificates/trust-source/anchors/. Both also have a blocklist directory, which is how you distrust a certificate system wide if you ever need to.

Windows and macOS certificate stores

Windows keeps per machine and per user stores. certutil -addstore adds a certificate to a named store; the store name for trusted roots is Root. In PowerShell, Microsoft's documented example for importing into the machine's root store is Import-Certificate with -CertStoreLocation Cert:\LocalMachine\Root. Both need an elevated prompt for the machine store.

On macOS, the security command's add-trusted-cert verb adds a certificate with trust settings. The -d flag writes to the admin trust domain, -r trustRoot marks it as a trusted root, and -k chooses the keychain. You can do the same in Keychain Access by importing into the System keychain and setting "When using this certificate" to "Always Trust".

Firefox's own store and the enterprise roots setting

Firefox does not use the operating system's trust store for TLS by default; it ships its own. You have two documented routes. The Certificates | ImportEnterpriseRoots policy tells Firefox to trust certificates "added to the operating system certificate store by a user or administrator". It controls the security.enterprise_roots.enabled preference, and Mozilla's policy documentation says it works only on Windows and macOS. On Linux, use the Certificates | Install policy, which installs a certificate into Firefox's store with the trust string CT,CT,. Given a bare filename, Firefox looks in /usr/lib/mozilla/certificates, /usr/lib64/mozilla/certificates and ~/.mozilla/certificates; since Firefox 65 a full path also works. You can also import the root by hand in Firefox's certificate settings and tick the box to trust it for websites.

Android user CAs and network security configuration

Android keeps a system store, which you cannot change without root access, and a user store you can add to from Settings. The catch is in Android's network security configuration documentation. For apps targeting Android 7.0 (API level 24) and higher, the default base-config trusts only <certificates src="system" />. Only apps targeting Android 6.0 (API level 23) and lower trust user installed CAs by default. So installing your root on a phone helps apps that target old API levels and apps that opt in; most modern apps will still reject your certificates.

If you write the app, opt in explicitly in res/xml/network_security_config.xml. Narrowest is best: trust your CA file only for your internal domain.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">home.arpa</domain>
    <trust-anchors>
      <certificates src="@raw/home_root_ca" />
    </trust-anchors>
  </domain-config>
</network-security-config>

Bundling the root as a raw resource is safer than trusting the whole user store, because it does not trust whatever else a user (or malware with user level access) adds. Android also supports debug-overrides, which applies only when the app is debuggable; Google notes that app stores do not accept debuggable apps, so debug only CAs cannot leak into a release.

For phones whose apps you do not control, this is the strongest argument for public certificates with DNS-01 on the services you open from your phone.

iOS and iPadOS profiles

On iPhone and iPad, a root arrives inside a configuration profile. Apple's support article is direct: when you install a profile "sent to you via email or downloaded from a website", you must turn on trust manually under Settings, General, About, Certificate Trust Settings, in the section "Enable full trust for root certificates". Apple recommends Apple Configurator or MDM for organisations, because certificate payloads installed that way are trusted for TLS automatically. For a homelab, the manual route is fine: serve root.crt from an internal web page, open it in Safari, install the downloaded profile from Settings, then flip the trust switch.

How do you protect the CA keys?

A CA is a key with paperwork around it. The paperwork can be light in a homelab; the key handling cannot.

Root key: offline, encrypted, duplicated. Generate the root on a machine that is not connected to a network, ideally booted from a live USB image. Encrypt the key with a strong passphrase as it is generated (OpenSSL's genpkey takes a cipher option such as -aes-256-cbc). Store the encrypted key on two removable drives kept in different places, and store the passphrase in your password manager and on paper. The root key leaves storage only to sign a new intermediate or a CRL.

Hardware tokens. A smart card or USB security key that supports PIV or PKCS#11 can hold the root or intermediate key so it never exists as a file. OpenSSL 3 can use such keys through a provider or engine, and Caddy's documentation notes that the root key can be omitted when it lives on hardware. The trade-off is backup: a key generated inside a token usually cannot be exported, so plan for loss by keeping a second token or an encrypted offline copy generated outside the token and imported into both.

Intermediate key: online but small blast radius. The intermediate key has to be readable by the ACME server. Run the server as its own user, keep the key file mode 0600, keep the CA server off the guest network, and back up its data directory. Thanks to pathlen:0 and name constraints, a stolen intermediate key can forge certificates only for your internal names, and only until you rotate it and the leaves expire.

Backups. Back up the root certificate and encrypted root key (offline), the intermediate certificate and key (encrypted backup of the CA server), the OpenSSL serial file and index if you use openssl ca, and the ACME server's database. Test that you can sign with the backup.

NIST SP 800-57 frames the whole thing as key lifecycle: generation, distribution, storage, use, backup, archive, and destruction, with a cryptoperiod for each key. Write down, even in a text file, when each key was created, where it lives, who knows its passphrase, and when it should be retired.

How do you revoke or rotate?

A leaf leaks. With 12 hour to 7 day leaves, the practical answer is to remove the key from the service, issue a new certificate, and wait out the old one. ACME also has a revokeCert endpoint, but revocation only helps if clients check, and most clients do not check CRLs or OCSP for a private CA.

The intermediate leaks or the CA server is compromised. Rebuild the CA server, generate a new intermediate key, sign it with the root, and restart the ACME server. Old leaves expire on their own. If you published a CRL from the root, add the old intermediate's serial to it; if you did not, the old intermediate stays technically valid until it expires, which is the main reason to keep intermediate lifetimes to a year or two and constraints tight.

Routine intermediate renewal. Generate a fresh key and CSR on the CA server, carry the CSR to the offline root, sign, carry back the certificate, and reload. Do it 30 to 60 days before expiry so leaves issued under the old intermediate expire well before it does.

The root leaks or expires. Create a new root, sign a new intermediate, push the new root to every device, and only then switch the ACME server. Remove the old root from devices afterwards. This is the expensive operation, which is why the root key gets the most care.

Step by step: build a homelab CA with OpenSSL and an ACME server

This walkthrough uses OpenSSL 3 for the root and intermediate, and Caddy's built in ACME server and PKI app as the issuing CA. Caddy is an open source web server; its acme_server directive turns a Caddy instance into an RFC 8555 server that issues certificates "for any other ACME-compatible software". A second Caddy instance (or any ACME client) acts as the reverse proxy that gets certificates automatically.

Names used below: internal zone home.arpa, LAN 192.168.1.0/24, CA server ca.home.arpa at 192.168.1.5, reverse proxy proxy.home.arpa at 192.168.1.10 serving nas.home.arpa and grafana.home.arpa. Make sure your internal DNS resolves all of these before you start.

Step 1: prepare the offline root machine

Boot a laptop from a live Linux USB image, disconnect it from the network, and insert a USB drive for the output.

Warning: the next block writes a filesystem to a USB drive and destroys everything on it. Check the device path first.

# Identify the USB drive. Confirm size and model before going further.
lsblk -o NAME,SIZE,MODEL,TRAN,MOUNTPOINTS

# Replace /dev/sdX with the device you confirmed above.
DEV=/dev/sdX
sudo cryptsetup luksFormat "$DEV"
sudo cryptsetup open "$DEV" caroot
sudo mkfs.ext4 -L caroot /dev/mapper/caroot
sudo mkdir -p /mnt/caroot && sudo mount /dev/mapper/caroot /mnt/caroot
sudo chown "$USER" /mnt/caroot
mkdir -p /mnt/caroot/root && cd /mnt/caroot/root

The LUKS layer encrypts the whole drive; the key encryption in the next step protects the key file even if it is copied elsewhere.

Step 2: create the root key and certificate

# ECDSA P-384 root key, encrypted with AES-256 as it is written.
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 \
  -aes-256-cbc -out root.key
chmod 400 root.key

Create root.cnf:

[req]
distinguished_name = dn
prompt             = no
x509_extensions    = v3_root

[dn]
O  = Home Lab
CN = Home Lab Root CA 2026

[v3_root]
basicConstraints     = critical, CA:true
keyUsage             = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash

Self sign it for 10 years:

openssl req -x509 -new -key root.key -config root.cnf \
  -sha384 -days 3650 -out root.crt

openssl x509 -in root.crt -noout -subject -dates -ext basicConstraints,keyUsage

Copy root.crt (the certificate only) to a second USB stick for distribution. It is public; the key is not.

Step 3: create the intermediate key and CSR on the CA server

On ca.home.arpa, as root:

sudo install -d -m 0750 -o root -g caddy /etc/homeca
cd /etc/homeca
sudo openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out int.key
sudo chown root:caddy int.key && sudo chmod 0640 int.key
sudo openssl req -new -key int.key \
  -subj "/O=Home Lab/CN=Home Lab Issuing CA 2026" -out int.csr

The intermediate key is deliberately unencrypted, because the ACME server must use it unattended. File permissions and the host's own security protect it. Carry int.csr to the offline machine on the distribution stick.

Step 4: sign the intermediate with name constraints

On the offline machine, create int.ext. The constraints go in their own section, the long form OpenSSL allows for multi valued extensions, which keeps a list of several names readable:

[v3_int]
basicConstraints       = critical, CA:true, pathlen:0
keyUsage               = critical, keyCertSign, cRLSign
extendedKeyUsage       = serverAuth, clientAuth
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always
nameConstraints        = critical, @nc

[nc]
permitted;DNS.0 = home.arpa
permitted;DNS.1 = internal
permitted;IP.0  = 192.168.1.0/255.255.255.0
permitted;IP.1  = 10.0.0.0/255.0.0.0

The OpenSSL x509v3_config manual describes the syntax: each name starts with permitted or excluded and a semicolon, then follows subjectAltName syntax, and IP entries take an address and a subnet mask separated by a slash. The extendedKeyUsage on a CA certificate is not defined by RFC 5280 as a constraint, but many verifiers treat it as one; it is a cheap extra fence.

Sign for two years:

openssl x509 -req -in int.csr -CA root.crt -CAkey root.key \
  -CAcreateserial -days 730 -sha384 \
  -extfile int.ext -extensions v3_int -out int.crt

openssl x509 -in int.crt -noout -text | grep -A8 "X509v3 Name Constraints"
openssl verify -CAfile root.crt int.crt

-CAcreateserial creates root.srl next to the root certificate; keep it with the root so serial numbers never repeat. Copy int.crt back to the CA server, unmount, close the LUKS volume (sudo cryptsetup close caroot) and put the drive away.

Any constraint mistake is permanent for this intermediate, so read the Name Constraints output before you leave the offline machine.

Step 5: run the ACME server

Install Caddy from its official packages on ca.home.arpa, copy root.crt and int.crt into /etc/homeca, 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 http-01 tls-alpn-01
		resolvers 192.168.1.1:53
		allow {
			domains home.arpa *.home.arpa
		}
	}
}

What each piece does, according to Caddy's documentation:

  • The pki global option defines a CA with the ID home. Supplying the intermediate certificate and key makes Caddy sign with your intermediate. Caddy's docs describe exactly this offline root pattern: load the intermediate pair, 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 that root key when an intermediate key pair is configured.
  • issuer internal { ca home } gives the ACME endpoint itself a certificate from the same CA.
  • acme_server serves the ACME API under /acme/home/directory. The default lifetime is 12 hours; 48 hours here is a calmer homelab setting. Caddy warns that the leaf lifetime must stay below renewal_window_ratio times the intermediate lifetime, so a leaf never outlives the intermediate that signed it. With a two year intermediate that is easy to satisfy, but you must renew the intermediate by hand before it gets close to expiry, because Caddy cannot re-sign it without the root key.
  • challenges limits validation to HTTP-01 and TLS-ALPN-01, resolvers points validation at your internal DNS, and allow restricts the names this server will issue for, a policy check on top of the certificate's name constraints.

Validate the file before starting (the ACME database can be opened by only one process, so validate while Caddy is stopped, or use caddy reload against the running instance), then start the service:

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 JSON. If it fails with a certificate error, the chain or the name constraints are wrong, which is better found now.

Step 6: point a reverse proxy at the ACME server

On proxy.home.arpa, install the root into the system store first (Step 7 shows how), then copy root.crt to /etc/caddy/home-root.crt and write its Caddyfile:

{
	acme_ca https://ca.home.arpa/acme/home/directory
	acme_ca_root /etc/caddy/home-root.crt
	email admin@home.arpa
}

nas.home.arpa {
	reverse_proxy 192.168.1.20:5000
}

grafana.home.arpa {
	reverse_proxy 127.0.0.1:3000
}

acme_ca sets the ACME directory for every site and acme_ca_root tells Caddy which root to trust when talking to it, if that root is not in the system store. When Caddy starts it registers an account, places an order for each name, answers the HTTP-01 or TLS-ALPN-01 challenge itself, and renews automatically. Check the result from any machine that trusts the root:

openssl s_client -connect nas.home.arpa:443 -servername nas.home.arpa \
  -CAfile root.crt </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject -dates

The notAfter date should be about 48 hours after notBefore.

Any other RFC 8555 client works the same way: give it the directory URL, make it trust root.crt, and choose a challenge your network allows. For a client certificate used in mTLS, request a certificate whose name the issuing policy allows and configure the server side to require client certificates signed by your intermediate.

Step 7: install the root on each operating system

Debian and Ubuntu:

sudo cp root.crt /usr/local/share/ca-certificates/home-lab-root.crt
sudo update-ca-certificates

Fedora, RHEL, CentOS Stream, Rocky and Alma:

sudo cp root.crt /etc/pki/ca-trust/source/anchors/home-lab-root.crt
sudo update-ca-trust extract

Arch:

sudo cp root.crt /etc/ca-certificates/trust-source/anchors/home-lab-root.crt
sudo update-ca-trust extract

Windows, from an elevated PowerShell prompt:

Import-Certificate -FilePath .\root.crt -CertStoreLocation Cert:\LocalMachine\Root
# or, from an elevated Command Prompt:
certutil -addstore -f Root root.crt

macOS:

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

Firefox on Linux, with a system wide policies.json (on most distributions in /etc/firefox/policies/):

{
  "policies": {
    "Certificates": {
      "Install": ["/usr/local/share/ca-certificates/home-lab-root.crt"]
    }
  }
}

Firefox on Windows and macOS can instead use "ImportEnterpriseRoots": true in the same Certificates object, so it picks up the root you just added to the OS store.

Containers built from Debian based images:

COPY home-lab-root.crt /usr/local/share/ca-certificates/home-lab-root.crt
RUN update-ca-certificates

Android and iOS: serve root.crt from an internal web page, download it on the device, install it from Settings, and on iOS enable it under Certificate Trust Settings. On Android, remember that most apps will still ignore it.

Finally, verify trust from each device with a browser or curl https://nas.home.arpa, and check that a name outside your constraints fails. An easy test is to issue a certificate from your intermediate by hand for a public name on the CA server (for example with openssl x509 -req and the intermediate key) and confirm that clients reject it with a name constraints error. Delete the test certificate afterwards.

Step 8: monitor it

Add three checks to whatever monitoring you already run: the ACME directory responds; the leaf on each service has more than half its lifetime remaining; and the intermediate has more than 60 days left. A simple script can check the last two with openssl x509 -checkend:

# Exit status 1 if the intermediate expires within 60 days.
openssl x509 -in /etc/homeca/int.crt -noout -checkend $((60*24*3600))

Public CA, private CA, self signed or mesh VPN: which should you use?

ApproachTrust setup on clientsRenewalExposureEffort
Public CA with DNS-01None; every device already trusts itAutomatic through ACME; lifetimes falling to 200, 100 and then 47 daysHostnames in public CT logs; DNS API token in your lab; needs a public domainLow after the DNS API is wired up
Private CA with ACMEInstall your root on every device; Android apps mostly ignore itAutomatic, as often as every few hoursNames stay private; offline root and constrained intermediate limit damageMedium: one afternoon plus intermediate renewal every year or two
Self signed per serviceTrust each certificate separately, or click through warningsManualTrains people to ignore warnings; no central controlLow at first, high over time
Mesh VPN built in certificatesDepends on the product: some issue publicly trusted certificates for a provider assigned domain, some use their own CA inside the VPN clientHandled by the productNames may be logged publicly or visible to the provider; tied to one productLow, but only covers devices on the VPN

There is no single winner. Many homelabs end up with two: a public CA with DNS-01 for the handful of services opened from phones and smart TVs, and a private CA for internal only names, service to service traffic and mTLS.

Common mistakes

  • Using the root to sign leaf certificates. Then the root key has to be online, and the first compromise means touching every device. Always sign leaves with an intermediate.
  • Name constraints only on the root. RFC 5280 treats the root as trust anchor input; enforcement of its extensions varies. Put the constraints on the intermediate.
  • Forgetting pathlen:0. Without it, a stolen intermediate key can create a new sub CA without your constraints in some verifiers. With it, any further CA certificate fails path validation.
  • Installing the intermediate as a trust anchor. Trust the root only. Servers send the intermediate in the chain; Caddy does that for you.
  • Wrong file extension on Debian. update-ca-certificates ignores files that do not end in .crt.
  • Assuming Firefox and Android follow the OS. Firefox has its own store unless the enterprise roots policy is on, and Android apps targeting API 24 or later ignore user CAs.
  • Validation through a public resolver. The ACME server must use your internal DNS, or it cannot see home.arpa names.
  • Leaf lifetime longer than the intermediate's remaining life. Caddy's docs warn that the chain breaks when the intermediate expires even if the leaf is still valid. Renew the intermediate early.
  • No backup of the root key, or one copy only. A lost root key means a full rebuild and a trip to every device.
  • Leaving the ACME endpoint open to the guest network. Use an allow policy, external account binding or a firewall rule so only your own machines can request certificates.
  • Very long leaf lifetimes "because revocation does not matter at home". That is backwards: because revocation does not work at home, short lifetimes are your revocation.

FAQ

Is it safe to install my own root CA on my devices?

It is as safe as the root key. Anyone with that key can create certificates your devices trust. Keep the key offline and encrypted, sign only a name constrained intermediate with it, and the realistic worst case shrinks to forged certificates for your own internal names.

Can I get a public certificate for home.arpa or .internal?

No. Public CAs validate names through the public DNS, and both are reserved for private use: home.arpa by RFC 8375 and .internal by the ICANN Board's 29 July 2024 resolution. That is why a private CA is the right tool for those names.

What is the difference between a root CA and an intermediate CA?

The root is self signed and is the trust anchor you install on devices. The intermediate is signed by the root and does the daily signing. Keeping the root offline means a compromise of the online CA can be fixed by replacing the intermediate, without touching every device.

How long should internal certificates last?

As short as your automation handles reliably. Caddy's ACME server defaults to 12 hours; 2 to 7 days is a comfortable homelab setting. Public certificates are limited to 200 days from 15 March 2026 and will be limited to 47 days from 15 March 2029.

What is ballot SC-081?

A CA/Browser Forum ballot, adopted in April 2025, that cuts the maximum validity of publicly trusted TLS certificates in steps from 398 days to 200 days (15 March 2026), 100 days (15 March 2027) and 47 days (15 March 2029), and shortens how long domain validation can be reused, down to 10 days from 2029.

Do I need OCSP or a CRL for a homelab CA?

Usually not for leaves, if they live for hours or days. Publishing a CRL from the root is still worthwhile so you can revoke an intermediate. Most clients do not check revocation for private CAs, so short lifetimes do the real work.

Which ACME challenge should I use on my home network?

HTTP-01 if the ACME server can reach port 80 on the host through your internal DNS, TLS-ALPN-01 if only 443 is open, and DNS-01 for wildcards or hosts the CA cannot reach. Point the ACME server at your internal resolver in every case.

Why does my Android app reject my private CA certificate?

Since Android 7.0, apps targeting API level 24 or higher trust only system CAs by default. Installing your root in Settings adds it to the user store, which those apps ignore unless their network security configuration includes your CA.

How do I make Firefox trust my home CA?

On Windows and macOS, enable the ImportEnterpriseRoots policy (or the security.enterprise_roots.enabled preference) after adding the root to the OS store. On Linux, use the Certificates | Install policy or import the root in Firefox's certificate manager and trust it for websites.

Can I use name constraints with IP addresses?

Yes. RFC 5280 supports iPAddress constraints, and OpenSSL writes them as an address and subnet mask, for example permitted;IP:192.168.1.0/255.255.255.0. RFC 8738 adds IP identifiers to ACME, validated with HTTP-01 or TLS-ALPN-01.

Where Safeguard fits

Safeguard does not run certificate authorities, manage certificates or trust stores, protect CA keys, or monitor certificate expiry, and nothing in this guide depends on it. Its fit here is indirect and sits on the software side of a homelab. The CA you just built is open source software (OpenSSL, Caddy and its Go dependencies), usually running next to reverse proxies and container images full of other open source components. Safeguard can scan a repository, a container image or an SBOM file (for example safeguard scan --dir . in the folder that holds your compose files and Caddyfiles, or safeguard scan --image <name> for an image you run), 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 current is part of protecting that key.

A CA server is also a machine you will rebuild one day, and a rebuild pulls packages and images again at whatever versions resolve that day. The section below explains how Safeguard helps you keep those open source components on fixed versions, including when upstream has not shipped a fix yet (fork and patch is early access).

Fix it without waiting for upstream

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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull 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, compose files and container images that make up your CA 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 each command, 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 fits a small internal 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 same host as your reverse proxy can scan the images and repositories that live there without opening anything to the internet.

Click through it at your own pace, or open it full screen.

If you work with Claude, the last demo shows how to connect Claude to the Safeguard MCP server with OAuth and choose which tools Claude can use. Choosing tools one by one is the same least privilege habit as constraining your intermediate to the names it actually needs.

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

You put nameConstraints for home.arpa on your self signed root but not on the intermediate. Is your laptop protected if the intermediate key is stolen?

Not reliably. RFC 5280 treats the root as trust anchor input, and name constraints are not applied to self issued certificates, so enforcement of the root's extensions depends on the verifier. Put the constraints and pathlen:0 on the intermediate, where every conforming verifier processes them.

A public certificate is issued on 1 April 2027. What is the longest validity it can have under the TLS Baseline Requirements?

100 days. Certificates issued on or after 15 March 2027 and before 15 March 2029 must not exceed 100 days (and should not exceed 99). From 15 March 2029 the limit is 47 days.

Sources

Last reviewed: 24 October 2026. Tool options and platform menus change between releases; check the documentation for the versions you run, and confirm the device path before any command that formats a drive.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.