> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dnzwithazee.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Private certificate authority

> DNZ signs certificates for internal names itself: routes, Traefik, Caddy and DNZ's own encrypted DNS get one at once, and every device that trusts its root accepts them.

Internal names (`nas.office.lan`, `*.lab.internal`, a zone DNZ serves only inside your network) can't get
a certificate from Let's Encrypt or any other public CA: none of them can check a name the internet doesn't
see. DNZ's **private certificate authority** signs them itself. Routes on such names get a certificate at
once, with no challenge, and every device you install the CA's root on trusts it.

It lives under **Proxy → Certificates → Private CA** in the dashboard, and `dnz ca` on the command line
([CLI](https://docs.dnzwithazee.com/cli/ca.md)).

> [card] [Make the CA](#making-the-ca)
>
> Your issuer name, key, validity and the names it may sign.

> [card] [Trust the root](#trusting-the-root-on-devices)
>
> A step-by-step guide per device, ending with the download address and a QR code.

> [card] [Certificates it signs](#certificates)
>
> For routes, Traefik, Caddy, DNZ's own DoT and DoH, and client certificates (mTLS).

> [card] [ACME](#acme)
>
> Traefik, Caddy, cert-manager and certbot ask the CA for certificates themselves.

> [card] [Secondaries and clusters](#secondaries-and-clusters)
>
> Shared in a cluster; carried across a take over by export and import.

## Making the CA

The CA is never made by default. A superadmin makes it on demand: **Make the private CA** on the Private
CA tab (or `dnz ca init`). Every detail is yours to set, every one is editable later from the dashboard,
and every one shows in the certificates the CA signs:

| Field | Default | |
| - | - | - |
| Common name | `<Settings → Profile label> Root CA`, else `DNZ Root CA` | What devices list the root as |
| Organization, organizational unit | The profile's label / empty | |
| Country, state or province, locality, e-mail | Empty | Country: two letters, ISO 3166 (`US`, `DE`, `ID`) |
| Key type | ECDSA P-256 | P-384, RSA 2048, 3072 or 4096 (RSA for old devices and appliances) |
| Root valid for | 10 years | 1 to 25 |
| Intermediate | On, 3 years, named after the root: `Example Root CA` gives `Example Issuing CA`, a root named otherwise `<Organization> Issuing CA`, else `DNZ Issuing CA` (an empty name follows the root) | The root signs only the intermediate; the intermediate signs certificates |
| Names it may sign | DNZ's internal zones and the private address ranges | See [Name constraints](#name-constraints) |
| Revocation list | On | Served at `/ca/<id>.crl` and named in every certificate |
| The address devices reach DNZ on | **Settings → General → Server → Device address** and DNZ's HTTP port | Where devices download the root and the revocation list |
| Defaults for new certificates | 397 days, ECDSA P-256, server, renew 30 days before the end | A subject (organization, country, …) for certificates too |

Together, the subject fields are the **issuer** of every certificate the CA signs: the root's subject
(common name, organization, unit, country, state, locality, e-mail), and with an intermediate, the
intermediate's (its own common name, the rest the root's).

### Name constraints

The names the CA may sign are written into the root (and the intermediate). Devices refuse a certificate
for any other name, so even a leaked key can't vouch for `google.com` or your bank.

* A **domain name** covers itself and every name under it: `lab.internal` covers `nas.lab.internal`.
* An **address range** covers its addresses: `10.0.0.0/8`.

The suggested list holds DNZ's internal zones (primary zones under `.internal`, `.lan`, `.local`,
`.home.arpa` and the like, or ones the internet doesn't ask DNZ for), else `internal`, `lan`, `local` and
`home.arpa`, plus `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10` and `fc00::/7`.

Empty lists let the CA sign any name; DNZ warns before making such a CA. DNZ refuses to sign a name
outside the constraints.

### Changing the CA: versions

A root can't be edited in place: devices trust that exact certificate. **Change** on the CA card opens
the same dialog, and changing the issuer, the key type, the root's validity, the intermediate or the name
constraints makes a **new version** with a new root. The dialog says so before saving (**Make version
N**).

* The new version is active at once. DNZ issues every certificate again from it in the background
  (within a minute); meanwhile the old ones stay valid and served.
* Devices that installed the old root must install the new one. The **Versions** list shows how often
  each old root was downloaded, as a reminder ("N devices may still trust only the old root").
* Once nothing an old version signed is in use, **Retire** it: its keys are deleted for good, its root is
  no longer offered, and its last revocation list stays served.

The revocation list switch, the address and the defaults for new certificates apply at once, with no new
version. **New version** with the same settings makes new keys (`dnz ca new-version`).

## Certificates

What the CA signs is listed under **Private CA → Certificates** (a page at a time, with a search) and with
the other certificates under **Proxy → Certificates**. Each certificate has:

| Field | Default |
| - | - |
| Names: host names (a wildcard like `*.lab.internal` covers one label) and IP addresses | The route's hostnames |
| Subject: common name, organization, unit, country, state, locality, e-mail | The first name; the rest from the CA's *Defaults for new certificates*, else the CA's own |
| Valid for | 397 days (browsers' limit for public certificates), never past the issuing CA's own end |
| Key type | ECDSA P-256 |
| Use | **Server** (TLS server authentication), **Client** (mTLS client authentication), or **Both** |
| Renew | 30 days before it ends (at most a third of its lifetime) |

Actions on a certificate:

* **Issue again now**: a new key and serial.
* **Revoke**: its serial goes into the revocation list, with a reason, and DNZ never serves it again. A
  route that used it gets a new one.
* **Download**: the certificate and its chain as PEM. With its private key for superadmins only; every
  such download is in the audit log.

**Issue a certificate** makes one by hand: a server certificate for a host DNZ doesn't proxy, or a
**client certificate** for a person or device (a name, an e-mail address, a validity).

### Where DNZ uses it by itself

> [accordion] DNZ Proxy routes
>
> A route on internal names gets its certificate from the CA before any public CA is asked: it is the
> first step of [Automatic certificates](https://docs.dnzwithazee.com/proxy/certificates.md#automatic-certificates). A name is
> internal when the CA may sign it and it is under `.internal`, `.lan`, `.local`, `.home.arpa`,
> `.localdomain`, `.intranet`, `.corp`, `.home`, `.private` or `.test`, or under one of the
> constraints' domain names.
>
> The route dialog's certificate step says **Private CA** for such names. For other names the CA may
> sign, it offers **Use DNZ's private CA** (the route's resolver is then `private-ca`).

> [accordion] Traefik and Caddy proxies
>
> They get the certificates of their internal routes the way they get uploaded ones (Traefik's
> `tls.certificates`, Caddy's `load_pem`): Traefik routes without a resolver and Caddy routes, with
> internal names or naming the `private-ca` resolver.

> [accordion] DNZ's own certificate (DoT, DoH, HTTPS)
>
> **Settings → General → Encrypted DNS → Certificate from DNZ** can pick **Private CA**, for internal
> names and IP addresses too, so DoT and DoH work inside a network without a public name. See
> [Encrypted DNS](https://docs.dnzwithazee.com/dns/encrypted-dns.md#from-the-private-ca).

> [accordion] Client certificates (mTLS)
>
> Issue **client** certificates for people or devices and download them as PEM. For routes that
> require client certificates, paste the root (the CA card's **Root as PEM**) into a TLS option's *CAs
> that sign client certificates* (the proxy's TLS settings). For an upstream that wants a client
> certificate, use one from the CA as the upstream's
> [client certificate](https://docs.dnzwithazee.com/proxy/upstreams.md#connection-settings).

## Trusting the root on devices

A device trusts the CA's certificates once its root is installed there. **Trust the root** on the Private
CA tab walks through it in four steps, with your address already filled in:

**1. Pick where to install it**

Windows, macOS, iPhone / iPad, Android, Linux, Chrome / Edge, Firefox, or Docker / servers. The
guide picks the system of the browser you opened it in first. Install the root once per device (or
per container image); every certificate the CA signs is then trusted there.

**2. Follow the steps for it**

Each step is a menu path to click through or one paste-and-run command, with a copy button. The
steps for every platform are below.

**3. Download the root**

The guide ends with the **download address** in large type, a copy button and a **QR code**: on a
phone on the same network, scan it with the camera. Below it are the files for that platform
(`dnz-root.crt`, `dnz-root.cer`, `dnz-root.mobileconfig`) and the root's **SHA-256 fingerprint**, with
how to show it on the device: compare the two after installing.

The address comes from **Settings → General → Server → Device address** and DNZ's HTTP port, unless
the CA has one of its own. It can be changed right there (DNZ warns on `localhost` or `127.0.0.1`,
which is the machine you are on, not where devices reach DNZ; behind a reverse proxy, use the
proxy's address), and a superadmin can **Save as the CA's address**.

**4. Check it**

The guide lists HTTPS addresses with a certificate from the CA: DNZ's own `/ca/check` when the CA
signs DNZ's certificate, and routes on internal names. Open one on the device: no warning means it
is trusted. **Test from this browser** checks it from the browser you are in.

### The steps per platform

Replace `http://192.168.1.10:8080` with the address the guide shows (the guide fills it in for you).

> [tab] Windows
>
> Windows keeps one trust store for the whole machine: Chrome, Edge and most programs use it.
>
> 1. Open PowerShell as administrator (Start → type PowerShell → **Run as administrator**) and paste
>    this. It downloads the root and adds it to *Trusted Root Certification Authorities*:
>
>    ```powershell theme={null}
>    Invoke-WebRequest -Uri "http://192.168.1.10:8080/ca/root.crt" -OutFile "$env:TEMP\dnz-root.crt"
>    certutil -addstore -f Root "$env:TEMP\dnz-root.crt"
>    ```
>
> 2. Or by hand: download `dnz-root.crt`, double-click it, then **Install Certificate…** → **Local
>    Machine** → **Place all certificates in the following store** → **Browse…** → **Trusted Root
>    Certification Authorities** → **Finish**.
>
> 3. Many PCs in a domain: import it once in a Group Policy instead: **Group Policy Management** →
>    Computer Configuration → Policies → Windows Settings → Security Settings → Public Key Policies →
>    Trusted Root Certification Authorities → **Import…**.
>
> 4. Close every browser window and open the browser again.
>
> Fingerprint: `certutil -hashfile dnz-root.cer SHA256`.

> [tab] macOS
>
> macOS trusts what the System keychain trusts: Safari, Chrome and Edge use it.
>
> 1. Paste this in Terminal (it asks for your password):
>
>    ```bash theme={null}
>    curl -fsSL -o /tmp/dnz-root.crt "http://192.168.1.10:8080/ca/root.crt"
>    sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/dnz-root.crt
>    ```
>
> 2. Or with the profile: download `dnz-root.mobileconfig`, open it, then **System Settings** →
>    **General** → **Device Management** (or **Privacy & Security → Profiles**) → the root → **Install…**.
>
> 3. Restart the browser.
>
> Fingerprint: `openssl x509 -in /tmp/dnz-root.crt -noout -fingerprint -sha256`.

> [tab] iPhone / iPad
>
> iOS asks for two things: install the profile, then turn on full trust.
>
> 1. Scan the QR code with the Camera (or open `http://192.168.1.10:8080/ca/root.mobileconfig` in
>    Safari) and tap **Allow**: it downloads the profile.
> 2. Install it: **Settings** → **Profile Downloaded** (at the top) → **Install** → **Install**.
> 3. Turn on full trust for the root: **Settings** → **General** → **About** → **Certificate Trust
>    Settings** → the root → **Continue**.
>
> Fingerprint: Settings → General → VPN & Device Management → the profile → More Details: SHA-256.

> [tab] Android
>
> Android installs it as a user CA. Chrome and the system browser trust it; other apps only when their
> makers allow user CAs (Android 7 and later).
>
> 1. Scan the QR code (or open `http://192.168.1.10:8080/ca/root.crt` in the browser) to download
>    `dnz-root.crt`.
> 2. Install it as a CA certificate (the names differ a little between phones): **Settings** →
>    **Security & privacy** → **More security settings** → **Encryption & credentials** → **Install a
>    certificate** → **CA certificate** → **Install anyway** → `dnz-root.crt`.
> 3. Older Android: Settings → Security → More → Install from storage → CA certificate.
>
> Fingerprint: Settings → Encryption & credentials → Trusted credentials → User → the root.
>
> > [!NOTE]
> >
> > An app that won't trust it ignores user CAs on purpose; only its maker (or a managed device
> > profile) can change that.

> [tab] Linux
>
> The system store covers curl, wget, apt, Python and most programs. Chrome and Firefox on Linux keep
> their own store.
>
> ```bash theme={null}
> # Debian, Ubuntu and their relatives
> sudo curl -fsSL -o /usr/local/share/ca-certificates/dnz-root.crt "http://192.168.1.10:8080/ca/root.crt"
> sudo update-ca-certificates
>
> # Fedora, RHEL, Rocky, Alma, CentOS
> sudo curl -fsSL -o /etc/pki/ca-trust/source/anchors/dnz-root.crt "http://192.168.1.10:8080/ca/root.crt"
> sudo update-ca-trust
>
> # Arch and openSUSE (p11-kit)
> curl -fsSL -o /tmp/dnz-root.crt "http://192.168.1.10:8080/ca/root.crt"
> sudo trust anchor --store /tmp/dnz-root.crt
>
> # Chrome and Chromium on Linux (needs libnss3-tools)
> curl -fsSL -o /tmp/dnz-root.crt "http://192.168.1.10:8080/ca/root.crt"
> certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "DNZ Root CA" -i /tmp/dnz-root.crt
> ```
>
> Fingerprint: `openssl x509 -in /tmp/dnz-root.crt -noout -fingerprint -sha256`.

> [tab] Chrome / Edge
>
> Chrome and Edge trust what the system trusts.
>
> 1. Install the root in the system: the Windows, macOS or Linux steps (on Linux, its Chrome line).
> 2. Or only for this browser: **Settings** → **Privacy and security** → **Security** → **Manage
>    certificates** → **Custom** (or *Installed by you*) → **Trusted certificates: Import** →
>    `dnz-root.crt`.
> 3. Restart the browser: `chrome://restart` (Edge: `edge://restart`).
>
> Fingerprint: the padlock on a site → Connection is secure → Certificate is valid → Details: the
> root's SHA-256.

> [tab] Firefox
>
> Firefox keeps a store of its own. Import the root there, or let Firefox use the system's (Windows and
> macOS).
>
> 1. Import it: download `dnz-root.crt`, then **Settings** → **Privacy & Security** → **Certificates** →
>    **View Certificates…** → **Authorities** → **Import…** → `dnz-root.crt` → tick **Trust this CA to
>    identify websites**.
> 2. Or use the system's store (after installing the root in Windows or macOS): open `about:config`
>    and set `security.enterprise_roots.enabled` to `true`.
> 3. Many computers: put this in Firefox's `policies.json` (its `distribution` folder) so it installs
>    the root by itself:
>
>    ```json theme={null}
>    {
>      "policies": {
>        "Certificates": {
>          "ImportEnterpriseRoots": true,
>          "Install": ["/usr/local/share/ca-certificates/dnz-root.crt"]
>        }
>      }
>    }
>    ```
>
> Fingerprint: Settings → Certificates → View Certificates → Authorities → the root → View.

> [tab] Docker / servers
>
> Containers and servers need the root in their own trust store, built into the image or mounted.
>
> ```dockerfile theme={null}
> # Debian or Ubuntu images (Alpine: apk add --no-cache ca-certificates first)
> ADD http://192.168.1.10:8080/ca/root.crt /usr/local/share/ca-certificates/dnz-root.crt
> RUN update-ca-certificates
> ```
>
> Kubernetes: keep it in a ConfigMap and mount it where the image looks:
>
> ```bash theme={null}
> curl -fsSL -o dnz-root.crt "http://192.168.1.10:8080/ca/root.crt"
> kubectl create configmap dnz-root-ca --from-file=dnz-root.crt
> ```
>
> ```yaml theme={null}
> volumes:
>   - name: dnz-root-ca
>     configMap:
>       name: dnz-root-ca
> # in the container:
> volumeMounts:
>   - name: dnz-root-ca
>     mountPath: /usr/local/share/ca-certificates/dnz-root.crt
>     subPath: dnz-root.crt
> ```
>
> Programs with a store of their own (Node.js, Python requests, Java):
>
> ```bash theme={null}
> export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/dnz-root.crt
> export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
> keytool -importcert -cacerts -alias dnz-root -file dnz-root.crt -storepass changeit -noprompt
> ```
>
> Fingerprint: `openssl x509 -in dnz-root.crt -noout -fingerprint -sha256`.

### Public addresses

The root is public and needs no sign-in: it is the certificate only, no key.

| Address | What |
| - | - |
| `http://<DNZ>:<port>/ca/root.crt` | The active root as PEM: Windows, Linux, Android, Firefox, servers |
| `http://<DNZ>:<port>/ca/root.cer` | The same as DER: Windows, Android, Java |
| `http://<DNZ>:<port>/ca/root.mobileconfig` | An Apple configuration profile: iPhone, iPad, Mac |
| `http://<DNZ>:<port>/ca/<id>.crl` | A version's revocation list (DER), signed by its issuing CA |
| `https://<DNZ>/ca/check` | Says "This device trusts …" when it opens without a warning |

The same files are also under `/api/ca/root.crt`, `/api/ca/root.cer` and `/api/ca/root.mobileconfig`.

## ACME

Traefik, Caddy, cert-manager, certbot and lego can ask the CA for certificates themselves over ACME
(RFC 8555), as they would ask Let's Encrypt, and renew them by themselves. **Private CA → ACME** in the
dashboard, [`dnz ca acme`](https://docs.dnzwithazee.com/cli/ca#acme) on the command line. It is **off by default**: nothing answers
under `/acme/` until a superadmin turns it on.

The directory address to give clients is `https://<DNZ>/acme/private-ca/directory`. It names the CA, not a
version, so it stays the same through new versions.

> [!WARNING]
>
> ACME is meant to run over HTTPS. Give DNZ a certificate ([Encrypted DNS](https://docs.dnzwithazee.com/dns/encrypted-dns.md#from-the-private-ca);
> the private CA can sign it) and have clients trust the root. Plain `http://` works, but some clients refuse it.

| Setting | Default | |
| - | - | - |
| On | off | Off: `/acme/` answers 404 |
| The address clients reach DNZ on | the address each request came to | Goes into every address the server hands out. Set it when clients come through a reverse proxy |
| Who may sign up | clients with a key | Or anyone who can reach DNZ |
| What new accounts may ask for | nothing | For accounts without a key, when anyone may sign up |
| Certificates are valid for | 90 days | Up to the issuing CA's own end. Clients may not ask for their own dates |

### Who gets which names

An order is only taken when every name in it is inside the CA's [name constraints](#name-constraints) **and**
on the account's **allow list**: zones or suffixes (`lab.internal` covers it and every name under it,
wildcards included).

An account gets its list from the **external key** it signed up with (external account binding): a key a
superadmin makes per client, with what that client may ask for. A key binds one account, and its secret shows
once, when it is made. If anyone may sign up, a new account gets the *What new accounts may ask for* list; left
empty, it can't get anything until a superadmin gives it a list.

> [!NOTE]
>
> The allow list is what authorizes a name in a zone DNZ holds, since DNZ answers that DNS challenge itself.
> Keep it as narrow as each client needs.

### Challenges

| Challenge | How it is checked | |
| - | - | - |
| `dns-01` | For a name in a zone DNZ holds, DNZ's own DNS answers `_acme-challenge.<name>` from the order's token while the order waits. **No record is written**, no serial moves, nothing to clean up: the client just says it is ready. For a name elsewhere, DNZ asks its own resolver, and the client must have written the record | The only one for wildcards |
| `http-01` | DNZ fetches `http://<name>/.well-known/acme-challenge/<token>`, looking the name up with its own resolver, from its own network. Redirects are followed to ports 80 and 443 only. The client must answer there | Not for wildcards |

`tls-alpn-01` is not offered. Only DNS names are ordered; addresses aren't. A pending answer sits next to any
real TXT record at the name, and costs DNS nothing while no challenge is pending.

### Set up a client

The dashboard has a guide per client with the address, the name and the key filled in and a copy button on each
block. Run it on the machine with the client: it doesn't have to be DNZ's. With `https://dns.lab.internal` as
DNZ's address, and the root downloaded first ([Public addresses](#public-addresses)):

> [tab] certbot
>
> ```bash theme={null}
> # A name in a zone DNZ holds: the hooks do nothing on purpose
> sudo REQUESTS_CA_BUNDLE=/etc/ssl/dnz/root.crt certbot certonly --non-interactive --agree-tos \
>   --server https://dns.lab.internal/acme/private-ca/directory --email you@example.com \
>   --eab-kid dnz-… --eab-hmac-key … \
>   --manual --preferred-challenges dns --manual-auth-hook /bin/true --manual-cleanup-hook /bin/true \
>   -d app.lab.internal
> ```
>
> For `http-01`, `--standalone --preferred-challenges http` in place of the `--manual` line: port 80 must
> reach this machine from DNZ's network.

> [tab] lego
>
> ```bash theme={null}
> sudo LEGO_CA_CERTIFICATES=/etc/ssl/dnz/root.crt EXEC_PATH=/bin/true lego --accept-tos \
>   --server https://dns.lab.internal/acme/private-ca/directory --email you@example.com \
>   --eab --kid dnz-… --hmac … --dns exec --dns.resolvers dns.lab.internal:53 \
>   --domains app.lab.internal run
> ```
>
> For `http-01`, `--http` in place of `--dns exec --dns.resolvers …`.

> [tab] Caddy
>
> ```text theme={null}
> {
>   email you@example.com
>   acme_ca https://dns.lab.internal/acme/private-ca/directory
>   acme_ca_root /etc/caddy/dnz-root.crt
>   acme_eab {
>     key_id dnz-…
>     mac_key …
>   }
> }
> ```
>
> Caddy then gets and renews the certificates of its sites by itself, over `http-01`: port 80 must reach it
> from DNZ's network.

> [tab] Traefik
>
> ```yaml theme={null}
> # traefik.yml, with LEGO_CA_CERTIFICATES=/etc/traefik/dnz-root.crt in its environment
> certificatesResolvers:
>   dnz:
>     acme:
>       caServer: https://dns.lab.internal/acme/private-ca/directory
>       email: you@example.com
>       storage: /etc/traefik/dnz-acme.json
>       certificatesDuration: 2160
>       httpChallenge:
>         entryPoint: web
>       eab:
>         kid: dnz-…
>         hmacEncoded: …
> ```

> [tab] cert-manager
>
> ```bash theme={null}
> kubectl -n cert-manager create secret generic dnz-acme-key --from-literal=secret='…'
> ROOT=$(curl -fsSL "http://192.168.1.10:8080/ca/root.crt" | base64 -w0)
> kubectl apply -f - <<EOF
> apiVersion: cert-manager.io/v1
> kind: ClusterIssuer
> metadata:
>   name: dnz
> spec:
>   acme:
>     server: https://dns.lab.internal/acme/private-ca/directory
>     email: you@example.com
>     caBundle: $ROOT
>     privateKeySecretRef:
>       name: dnz-acme-account
>     externalAccountBinding:
>       keyID: dnz-…
>       keySecretRef:
>         name: dnz-acme-key
>         key: secret
>     solvers:
>       - http01:
>           ingress:
>             ingressClassName: nginx
> EOF
> ```

### What it signs

Every certificate is listed under Private CA → Certificates (*For an ACME client*), and every issuance is in the
[audit log](https://docs.dnzwithazee.com/admin/audit-log.md) (`ca.acme.issue`, by `acme account <id>`). DNZ never holds its private key, so it is
neither served to proxies, nor renewed by DNZ, nor counted in the alerts about expiring certificates: the client
renews, and an old certificate simply ends. **Revoke** works as for any other certificate, here or over ACME (by
the account that ordered it, by one holding valid authorizations for all its names, or with the certificate's own
key), and lands in the revocation list.

A new version of the CA doesn't touch these certificates: they keep working until they end, and each client's next
renewal comes from the new version. An older version can be retired only after they ended. Orders that finished and
certificates that ended stay for 30 days, then go.

### In a cluster, on a secondary

* In a [cluster](https://docs.dnzwithazee.com/deploy/cluster.md) any server answers: accounts and orders live in the database. Nonces are per
  server, so a nonce from another server gets `badNonce` with a fresh one, which clients retry with.
* A DNZ secondary following a primary signs nothing: `/acme/` answers 503 there, and its settings are refused. It
  does answer the primary's pending `dns-01` challenges, **within a minute**. A client that checks all of a zone's
  name servers before it says it is ready (lego, cert-manager) may wait that long.
* After a take over, the new primary has no CA of the old one, so clients have to sign up again there.

## Security

* The CA's keys live in DNZ's database like every private key DNZ holds, so they are in its
  [backups](https://docs.dnzwithazee.com/admin/backups.md) too: encrypt the archives. They never appear in an API answer, a list, the
  proxy YAML export, the logs or to an AI assistant.
* Making, changing, exporting and importing the CA, and downloading a certificate's private key, need a
  superadmin. Issuing, renewing, revoking and downloading certificates need **Manage proxies**
  (`proxy.manage`).
* Everything is in the [audit log](https://docs.dnzwithazee.com/admin/audit-log.md): `ca.create`, `ca.change`, `ca.version`,
  `ca.retire`, `ca.export`, `ca.import`, `ca.certificate.issue`, `ca.certificate.reissue`,
  `ca.certificate.revoke` and `ca.certificate.download_key`.
* The ACME server's settings, external keys and allow lists need a superadmin and are audited too
  (`ca.acme.settings`, `ca.acme.key_create`, `ca.acme.key_delete`, `ca.acme.account_allow`,
  `ca.acme.account_deactivate`), and so is what clients do (`ca.acme.account`, `ca.acme.issue`,
  `ca.acme.revoke`, `ca.acme.key_change`).
* **Export** (superadmin, confirmed) gives the active version with its keys as one PEM file, to back it
  up or move it. **Import** makes such a file the active version. Anyone with the file can sign
  certificates every device that trusts the root accepts: keep it like a password.
* The [AI assistant tools](https://docs.dnzwithazee.com/integrations/ai-assistants.md) `private_ca` and `private_ca_acme` read the CA, its
  certificates, the root and the ACME server's accounts, keys and orders, never a key or a key's secret. An
  assistant signed in over OAuth can't download a certificate's key, export the CA, or change anything of the
  ACME server.

## Secondaries and clusters

* In a [cluster](https://docs.dnzwithazee.com/deploy/cluster.md) the servers share the CA (one database); the leader renews and moves
  certificates.
* A DNZ secondary following a primary over [replication](https://docs.dnzwithazee.com/dns/replication.md) signs nothing and refuses
  changes to the CA: its certificates are the primary's. The CA isn't copied to a secondary
  (certificates aren't DNS answers).
* After a **take over**, the new primary has no CA of the old primary's. Export the CA on the old
  primary first and import it on the new one, so devices keep trusting the same root.

> [tab] Dashboard
>
> On the old primary: **Proxy → Certificates → Private CA → Export**. On the new primary: the same tab
> → **Import**, and paste the file.

> [tab] CLI
>
> ```bash theme={null}
> dnz ca export -o ca-backup.pem --yes   # on the old primary
> dnz ca import ca-backup.pem            # on the new primary
> ```

## Metrics

| Metric | What it tells |
| - | - |
| `dnz_ca_certificates{state}` | Certificates the CA signed, DNZ's own included, by `state` (`pending`, `valid`, `failed`, `revoked`) |
| `dnz_ca_certificate_expiry_seconds{id,certificate}` | Seconds until a certificate of the CA ends (revoked ones left out) |
| `dnz_ca_root_expiry_seconds{version}` | Seconds until a version's root ends (retired ones left out) |
| `dnz_ca_intermediate_expiry_seconds{version}` | Seconds until a version's intermediate ends: no certificate it signs outlives it |
| `dnz_ca_acme_enabled`, `dnz_ca_acme_accounts{status}`, `dnz_ca_acme_orders{state}`, `dnz_ca_acme_certificates` | The [ACME server](#acme): on or off, its accounts, its orders, the certificates clients got that haven't ended |

Certificates the CA signs also count in `dnz_certificate_expiry_timestamp_seconds` (`source="ca"`) and in
the [proxy alerts](https://docs.dnzwithazee.com/integrations/notifications.md#proxy-alerts) for certificates about to expire. See
[Prometheus](https://docs.dnzwithazee.com/dashboard/prometheus.md).

## From the terminal and the API

```bash theme={null}
dnz ca show                                     # the CA, its versions, download address, fingerprint
dnz ca init --cn "Example Root CA" --org Example --country ID --constraint lab.internal
dnz ca root --format mobileconfig -o dnz-root.mobileconfig
dnz ca issue nas.lab.internal 10.0.0.5 --days 397
dnz ca issue --client --cn "Alice" --email alice@example.com
```

Every command is in [CLI → Private CA](https://docs.dnzwithazee.com/cli/ca.md), every endpoint in [Private CA API](https://docs.dnzwithazee.com/api/ca.md).

## Not yet

* OCSP (the revocation list covers revocation inside a network).
* Over ACME: addresses as names (RFC 8738), `tls-alpn-01`, renewal information (ARI).
* An offline root (only the intermediate inside DNZ).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.