Certificate hierarchy
homepki builds exactly three tiers: a self-signed root CA per domain, intermediate CAs signed by the root, and server or client leaves signed by an intermediate.
The three tiers
| Tier | Command | Signed by | Path length | Default validity |
|---|---|---|---|---|
| Root CA | homepki root-ca -d runlocal.dev | itself | 1 | 2190 days |
| Intermediate CA | homepki intermediate-ca -d runlocal.dev -n bu1 | the root | 0 | 2190 days |
| Server leaf | homepki server-cert -d runlocal.dev -i bu1 -s gw | the intermediate | not a CA | 365 days |
| Client leaf | homepki client-cert -d runlocal.dev -i bu1 -c cli | the intermediate | not a CA | 365 days |
The root has path length 1, so it can sign intermediates only. Intermediates have path length 0, so they can sign leaves only: you cannot nest a second intermediate under one. Each tier requires the tier above it and stops with an error when it is missing.
CA certificates carry keyCertSign and cRLSign. Server leaves carry serverAuth, client leaves clientAuth, and both carry digitalSignature; an RSA server leaf also carries keyEncipherment. Serial numbers are random 128-bit values, and NotBefore is set 5 minutes in the past so a new certificate is already valid in a container or VM whose clock runs slightly behind.
Derived subjects
Subject names are not configurable. They are derived from the domain, the intermediate name and the leaf name:
| Tier | O | OU | CN |
|---|---|---|---|
| Root CA | runlocal-dev | runlocal.dev | |
| Intermediate CA | runlocal-dev | bu1 | bu1.runlocal.dev |
| Leaf | runlocal-dev | bu1 | gw.bu1.runlocal.dev |
The organization is the domain with dots replaced by dashes, which is also the directory name. The leaf CN <leaf>.<intermediate>.<domain> is always the leaf's first DNS Subject Alternative Name. Add the names clients actually use with --san; most TLS clients ignore the CN and match SANs only. See Subject Alternative Names.
Several intermediates under one root
Create one intermediate per tenant, team or environment under a single root:
$ homepki intermediate-ca -d runlocal.dev -n bu1
$ homepki intermediate-ca -d runlocal.dev -n bu2
$ homepki intermediate-ca list -d runlocal.dev
Leaves under one intermediate are independent of leaves under another. The chain file of bu1 also contains the root, so a server that uses it to verify client certificates accepts a bu2 client certificate when the client sends the bu2 intermediate with it. To keep tenants apart, have the server check the issuer or the OU of the client certificate, trust only the intermediate certificate (bu1-intermediate-ca.crt) where the server allows a non-root trust anchor, or use one root per tenant. Re-issuing one intermediate only orphans its own leaves.
Tiers can use different key types: an ECDSA leaf under an RSA intermediate is fine.
Chain files
intermediate-ca writes <name>-intermediate-ca-chain.crt: the intermediate followed by the root, in PEM. Use it as:
- the CA bundle handed to clients that must trust a server certificate;
- the trust store a server uses to verify client certificates;
- the chain a server presents after its leaf, so clients that trust only the root can build the path.
There is no full-chain file that includes a leaf. Concatenate the leaf and the intermediate when a server needs one: see nginx.
File layout
Everything lives under the working directory: --workdir, then $HOMEPKI_WORKDIR, then ~/.homepki.
~/.homepki/
└── runlocal-dev/ # domain, dots replaced by dashes
├── ca/ # the root CA
│ ├── runlocal-dev-root-ca.crt
│ ├── runlocal-dev-root-ca.crl # after revoke or crl
│ └── private/runlocal-dev-root-ca.key
└── bu1/ # an intermediate CA
├── bu1-intermediate-ca.crt
├── bu1-intermediate-ca-chain.crt # intermediate + root
├── bu1-intermediate-ca.crl # after revoke or crl
├── bu1-crl-chain.crl # intermediate CRL + root CRL
├── private/bu1-intermediate-ca.key
├── server-tls/
│ ├── kong-gateway.crt
│ ├── kong-gateway.key
│ └── kong-gateway.p12 # with --pkcs12
└── client-tls/
├── my-client.crt
├── my-client.key
└── my-client.p12 # with --pkcs12
Certificates and CRLs are PEM. Private keys are unencrypted PKCS#8 PEM with mode 0600; private/ directories are 0700; .p12 files are 0600. Leaf directories are created on first use, so server-cert list before any server certificate exists prints an empty list and exits 0.
Working directories created before 0.7.0 also hold OpenSSL .conf and .csr files, db/ directories and numbered .pem copies. homepki ignores them, and --force on a tier deletes that tier's leftovers.
Overwrite protection
A generate command refuses to replace existing material. It exits 1, prints the files in the way, and changes nothing. --force replaces them:
Command with --force | Effect |
|---|---|
root-ca -d X | New root key and certificate; deletes the root CRL. Every intermediate and leaf under it stops verifying. |
intermediate-ca -d X -n Y | New intermediate key, certificate and chain file; deletes its CRLs. Every leaf under it stops verifying. |
server-cert / client-cert | New key and certificate; rewrites the .p12 with --pkcs12, deletes it otherwise. |
sign | Overwrites the certificate at the output path. |
--force on a CA tier orphans everything below it. The tier you replaced still reports ✓ OK in list; only the tier below flips to ✗ INVALID: x509: certificate signed by unknown authority. After forcing a CA, check the lowest tier.
There is no certificate database, so the same subject can be issued any number of times. In scripts, prefer a fresh working directory or a new name over --force.