# Vlastní certifikační autorita

Můžeš si certifikáty vystavovat sám. Funguje to, je to zadarmo a na první pohled je to elegantní řešení pro vnitřní služby, které nemají veřejnou doménu.

**Většinou to ale dělat nemáš.** Tahle stránka nejdřív vysvětlí proč, a teprve pak — když víš, do čeho jdeš — jak na to pořádně.

Jak certifikáty fungují je v [TLS a HTTPS](TLS-a-HTTPS), veřejné certifikáty v [certifikátech a Let's Encrypt](Certifikaty-a-Lets-Encrypt).

## Proč to nedělat

Vystavit certifikát je pět minut. Ta práce není v tom.

**Musíš kořenový certifikát dostat na každé zařízení.** Na notebook, na telefon, na tablet, na televizi, na tiskárnu. A pak na notebook návštěvy. A pak na nový telefon. Tahle práce nikdy neskončí a nedá se zautomatizovat pro zařízení, která nespravuješ.

**Spousta zařízení ti to neumožní vůbec.** Chytré televize, IP kamery, herní konzole, IoT krabičky. Buď nemají kam certifikát nahrát, nebo mají vlastní úložiště důvěry, ke kterému se nedostaneš.

**Android tvoji CA aplikacím nevnutí.** Od Androidu 7 aplikace ve výchozím nastavení nedůvěřují certifikátům, které nainstaloval uživatel. Prohlížeč ano, aplikace ne. Znamená to, že ti přes vlastní CA přestane fungovat mobilní klient Nextcloudu, Home Assistantu i Jellyfinu, a nezmůžeš s tím nic — je to úmyslné opatření proti odposlechu.

**Kořenový klíč je univerzální paklíč.** Kdo ho ukradne, může vystavit důvěryhodný certifikát na `seznam.cz`, `google.com` i na tvou banku — a všechna zařízení, kde máš CA nainstalovanou, mu uvěří. Přestaneš chránit provoz a začneš vyrábět jednotný bod selhání, který je zajímavější než cokoliv jiného v síti. Dá se to zmenšit [omezením jmen](#omezení-jmen-nameconstraints), ale ne odstranit.

**Obnova je tvůj problém.** U Let's Encrypt řeší 90denní platnost časovač, o kterém nevíš. U vlastní CA je obnova ruční práce, nebo si musíš postavit vlastní ACME server — a pak jsi na složitosti tam, kde jsi nechtěl být.

**Nic z toho nepotřebuješ.** Doména stojí dvě stovky ročně a s [DNS-01 ověřením](Certifikaty-a-Lets-Encrypt) dostaneš plnohodnotný certifikát i pro `nas.example.cz`, které ukazuje na `192.168.1.10` a z internetu není dostupné vůbec. Wildcard `*.example.cz` pokryje všechny vnitřní služby jedním certifikátem, obnovuje se sám a věří mu úplně všechno, včetně té televize.

| | Veřejná doména + DNS-01 | Vlastní CA |
|---|---|---|
| Cena | ~200 Kč / rok za doménu | zdarma |
| Instalace na zařízení | žádná | na každé zařízení ručně |
| Funguje v aplikacích na Androidu | ano | ne |
| Funguje na televizi a v IoT | ano | většinou ne |
| Obnova | automatická | tvoje starost |
| Riziko při úniku klíče | jen tvoje doména | všechny weby na všech zařízeních |
| Vnitřní jména ve veřejných logech | ano (CT) | ne |

**Doporučení: kup si doménu a použij DNS-01.** Zbytek téhle stránky je pro případy, kdy to opravdu nejde.

## Kdy to naopak smysl dává

Tohle nejsou výmluvy, to jsou legitimní důvody:

**Síť bez internetu.** Průmyslová síť, laboratoř, oddělený segment. ACME nemá jak fungovat, protože se Let's Encrypt nemá kam dovolat.

**Klientské certifikáty (mTLS).** Když se má klient serveru prokázat certifikátem, žádná veřejná CA ti ho nevystaví. Vlastní CA je tady jediná cesta a je to její nejsprávnější použití.

**Certifikát pro holou IP adresu.** Veřejné CA nevystaví certifikát pro `192.168.1.1` ani pro `10.0.0.5`. Když musíš mít HTTPS na adrese bez jména, jsi odkázaný na sebe.

**Vnitřní jména, která nikam nesmí.** Každý certifikát od veřejné CA se zapíše do veřejných logů certifikační transparentnosti. `ucetnictvi.firma.cz` je tím pádem veřejná informace. U vlastní CA ne.

**Vývoj na localhostu.** Na to je [mkcert](#mkcert-pro-vývoj) a je to naprosto v pořádku.

**Šifrování mezi službami uvnitř.** Když posíláš provoz mezi servery přes nedůvěryhodnou síť a obě strany spravuješ, vlastní CA je správný nástroj — protože obě strany spravuješ.

Všimni si, co mají společného: buď spravuješ všechna zapojená zařízení, nebo se bavíme o strojích, ne o lidech s telefony.

## Jak to funguje

Rekapitulace z [TLS a HTTPS](TLS-a-HTTPS), protože bez tohohle nedávají další příkazy smysl.

```
kořenová CA (root)          ← tenhle certifikát rozvezeš na zařízení
    └── mezilehlá CA (intermediate)
            └── certifikát pro nas.doma   ← tenhle nasadíš na server
```

Zařízení věří kořenovému certifikátu, protože jsi ho tam vlastníma rukama nainstaloval. Všemu, co je pod ním, věří odvozeně.

**Server posílá svůj certifikát a mezilehlé, kořenový ne.** Ten už klient má. Když mezilehlý zapomeneš, dostaneš klasickou chybu „funguje v prohlížeči, nefunguje v `curl`".

**Proč vůbec mezilehlá CA?** Aby kořenový klíč mohl ležet offline. Denní práci dělá mezilehlá; kdyby ti unikla, odvoláš ji a vystavíš novou pod stejným kořenem — a nemusíš znovu obcházet všechna zařízení. To je celý smysl toho mezikroku. Pokud kořenový klíč necháš ležet na stejném serveru vedle mezilehlého, můžeš krok vynechat a nic tím neztratíš, protože nic nechrání.

## Kterou cestou

| Nástroj | Pro co | Náročnost |
|---|---|---|
| [Caddy `tls internal`](#caddy-tls-internal) | služby za jednou proxy | jeden řádek |
| [mkcert](#mkcert-pro-vývoj) | vývoj na vlastním stroji | jeden příkaz |
| [step-ca](#step-ca-vlastní-ca-s-acme) | skutečná vnitřní CA s automatickou obnovou | půl dne |
| [openssl ručně](#ručně-přes-openssl) | pochopení, mTLS, jednorázové věci | hodina a pak už navždy ručně |

Ruční openssl je poslední volba, ne první. Je tu hlavně proto, abys věděl, co ostatní nástroje dělají za tebe.

## Caddy `tls internal`

Když už [Caddy](Caddy) používáš, máš vlastní CA hotovou:

```
router.doma {
    tls internal
    reverse_proxy 192.168.1.1
}
```

Caddy si při startu vyrobí kořenovou a mezilehlou CA a vystavuje pod ní certifikáty. Kořenový certifikát najdeš v datovém svazku:

```bash
# na hostiteli
/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt

# v Dockeru
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt .
```

Na stroji, kde Caddy běží, ho do systémového úložiště přidá jedním příkazem:

```bash
caddy trust      # a caddy untrust ho zase odebere
```

Na ostatní zařízení ho musíš dostat [ručně](#instalace-na-koncová-zařízení).

Certifikáty jsou záměrně krátkodobé — **koncový platí 12 hodin, mezilehlý 7 dní**, kořenový zhruba 10 let. Zní to šíleně, ale Caddy je obnovuje sám a ty o tom nevíš. Je to ta správná úvaha: krátká platnost místo odvolávání.

**Kde to končí:** tahle CA obslouží jen to, co teče přes tenhle jeden Caddy. Nemá ACME rozhraní, takže si u ní jiný stroj o certifikát požádat nemůže. Pro certifikát na poštovní server, do databáze nebo na druhý hypervizor ti je k ničemu — a přesně tam začíná [step-ca](#step-ca-vlastní-ca-s-acme).

**Datový svazek musí být trvalý.** Když ho ztratíš, Caddy vyrobí novou CA a ty musíš znovu obejít všechna zařízení. Viz [Caddy](Caddy).

## mkcert pro vývoj

Nástroj na jednu jedinou věc: certifikáty pro `localhost` a vývojová jména na tvém vlastním počítači.

```bash
apt install mkcert libnss3-tools    # Ubuntu 24.04, balíček je v universe
mkcert -install                     # vyrobí CA a přidá ji do úložišť důvěry
mkcert localhost 127.0.0.1 ::1 app.test
```

`libnss3-tools` doinstaluj vždycky. Balíček `mkcert` na něm nezávisí, ale bez něj neumí certifikát vložit do Firefoxu ani do Chrome, protože ty [mají vlastní úložiště](#firefox).

Vypadnou dva soubory `localhost+3.pem` a `localhost+3-key.pem` a prohlížeč je bez řečí přijme.

```bash
mkcert -CAROOT              # kde leží kořenový klíč
mkcert -uninstall           # odebere CA z úložišť důvěry
```

**Nepoužívej ho jako CA pro celou síť.** mkcert nemá odvolávání, nemá evidenci vystavených certifikátů, nemá obnovu a jeho kořenový klíč leží nezašifrovaný v domovském adresáři. Kopírovat na další stroje se smí **jen `rootCA.pem`**, nikdy `rootCA-key.pem` — na to upozorňuje i jeho vlastní dokumentace. Jakmile máš víc než jeden stroj, chceš step-ca.

## step-ca: vlastní CA s ACME

Když už vlastní CA opravdu potřebuješ, tohle je jediná varianta, kterou se dá dlouhodobě provozovat. `step-ca` od Smallstepu je plnohodnotná certifikační autorita, která umí **ACME** — tedy stejný protokol, jakým mluví Let's Encrypt. Certifikáty se pak obnovují samy a ty se o ně nestaráš.

To je zásadní rozdíl proti ručnímu openssl: neřešíš obnovu.

### Instalace na Ubuntu 24.04

Smallstep má vlastní APT repozitář, takže se to aktualizuje s systémem. Pozor na `Suites: debs` — vypadá to jako překlep, ale je to správně:

```bash
curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg \
  -o /etc/apt/keyrings/smallstep.asc

cat > /etc/apt/sources.list.d/smallstep.sources <<'EOF'
Types: deb
URIs: https://packages.smallstep.com/stable/debian
Suites: debs
Components: main
Signed-By: /etc/apt/keyrings/smallstep.asc
EOF

apt update && apt install step-cli step-ca
```

**Balíček nainstaluje jen binárku.** Nepřidá systemd jednotku, uživatele ani `/etc/step-ca` — to všechno je ruční práce podle dokumentace Smallstepu. Počítej s tím, návody, které tvrdí opak, jsou zastaralé.

### Založení CA

```bash
step ca init \
  --name "Doma CA" \
  --dns ca.doma \
  --address :8443 \
  --provisioner martin@doma \
  --deployment-type standalone \
  --acme
```

Zeptá se na heslo ke kořenovému klíči — vymysli dlouhé a ulož do správce hesel. Konfigurace a klíče skončí v `~/.step` (tedy `$(step path)`), ne v `/etc/step-ca`; přesun do systémového umístění je další ruční krok.

`--acme` rovnou založí ACME poskytovatele. Poslouchání na `:8443` místo `:443` ti ušetří řešení oprávnění k privilegovaným portům.

Příkaz vypíše **otisk kořenového certifikátu**. Zapiš si ho, budeš ho potřebovat na každém klientovi.

### Delší platnost certifikátů

Výchozí platnost je **24 hodin** (minimum 5 minut, maximum 24 hodin). To je záměr — místo odvolávání se spoléhá na to, že certifikát rychle vyprší. Pro službu, která si o certifikát umí říct sama, to nech být. Když potřebuješ delší:

```bash
step ca provisioner update acme --x509-default-dur=720h
systemctl restart step-ca
```

### Napojení klientů

[Caddy](Caddy) se na vlastní CA přepne v globálních volbách:

```
{
    acme_ca https://ca.doma:8443/acme/acme/directory
    acme_ca_root /etc/ssl/certs/doma-root.crt
}

nas.doma {
    reverse_proxy 127.0.0.1:5000
}
```

Certbot potřebuje kromě `--server` ještě `REQUESTS_CA_BUNDLE`, protože je v Pythonu a systémovému úložišti nevěří:

```bash
REQUESTS_CA_BUNDLE=/etc/ssl/certs/doma-root.crt \
certbot certonly --standalone \
  --server https://ca.doma:8443/acme/acme/directory \
  -d nas.doma
```

A stroj, kterému chceš jen předat důvěru:

```bash
step ca bootstrap --ca-url https://ca.doma:8443 --fingerprint <otisk> --install
```

**Tohle je ta cesta, kterou jdi,** pokud vlastní CA opravdu potřebuješ. Zbytek stránky je ruční varianta.

## Ručně přes openssl

Pro pochopení, pro klientské certifikáty a pro jednorázové věci. Všechny příkazy jsou ověřené na **Ubuntu 24.04 s OpenSSL 3.0.13**.

Používám eliptické klíče (P-256). Jsou menší, rychlejší a dnes je podporuje všechno. RSA 4096 potřebuješ jen pro prehistorická zařízení — pak vyměň `openssl ecparam ...` za `openssl genrsa -out root.key 4096`.

### Kořenová CA

```bash
#!/bin/bash
# vytvor-ca.sh — jednorázově, kořenový klíč pak ulož mimo server
set -euo pipefail

mkdir -p ca && cd ca
umask 077

openssl ecparam -name prime256v1 -genkey -noout -out root.key

openssl req -x509 -new -key root.key -sha256 -days 3650 -out root.crt \
  -subj "/CN=Doma Root CA/O=Doma" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash" \
  -addext "nameConstraints=critical,permitted;DNS:doma,permitted;IP:192.168.1.0/255.255.255.0"

echo "Hotovo. Otisk kořenového certifikátu:"
openssl x509 -in root.crt -noout -fingerprint -sha256
```

### Omezení jmen (nameConstraints)

Ten poslední `-addext` je nejdůležitější řádek celého skriptu a skoro nikdo ho nepoužívá.

**Omezuje, na co smí tvoje CA vystavovat certifikáty.** Bez něj je kořenový klíč paklíč ke všemu — kdo ho získá, vystaví si důvěryhodný certifikát na jakoukoliv doménu na světě. S ním umí vystavit jen na `doma` a na tvůj rozsah IP adres.

Že to funguje, si ověříš:

```bash
$ openssl verify -CAfile root.crt podvrzeny-google.crt
error 47 at 0 depth lookup: permitted subtree violation
```

Zápis má čtyři pasti a všechny stojí za přečtení, protože tři z nich vedou k falešnému pocitu bezpečí.

**Doména se píše bez tečky na začátku.** `permitted;DNS:doma` pokrývá `doma`, `nas.doma` i `a.b.doma`. Varianta `permitted;DNS:.doma`, kterou najdeš ve většině návodů, znamená **jen podřízená jména** — samotné `doma` pak neprojde. Zároveň se porovnává po celých částech jména, takže `zladoma` se dovnitř nevejde.

**Krátká jména přestanou fungovat.** `DNS:nas` bez domény je mimo `permitted;DNS:doma` a certifikát neprojde ověřením. S omezením jmen musíš všude používat plná jména. V domácí síti to zabolí hned první den.

**Omezení platí zvlášť pro každý druh jména.** Tohle je největší past. Když omezíš jen DNS, IP adresy a e-maily zůstanou úplně volné:

```bash
# kořen omezený jen na permitted;DNS:doma
DNS:nas.doma             -> OK      (čekáš)
IP:8.8.8.8               -> OK      (nečekáš!)
email:kdokoli@evil.com   -> OK      (nečekáš!)
```

Proto jsou v skriptu povolené typy dva. Když v síti používáš víc rozsahů nebo IPv6, musíš je vyjmenovat všechny. A `excluded;email:.` ti nepomůže — zapíše se do certifikátu, ale nevynucuje nic; zamknout se druh jména dá jedině tím, že pro něj přidáš úzké `permitted`.

**IP rozsah se píše plnou maskou.** `192.168.1.0/255.255.255.0`, nikoliv `/24` — zápis s prefixem openssl odmítne s `bad ip address`.

Omezení vynucují OpenSSL, prohlížeče i Windows. Starší a vestavěná zařízení ho můžou ignorovat, takže to není neprůstřelné — ale zdarma to výrazně zmenší škodu při úniku klíče.

### Mezilehlá CA

```bash
#!/bin/bash
# vytvor-mezilehlou.sh
set -euo pipefail
cd ca
umask 077

openssl ecparam -name prime256v1 -genkey -noout -out int.key
openssl req -new -key int.key -out int.csr -subj "/CN=Doma Issuing CA/O=Doma"

cat > int.ext <<'EOF'
basicConstraints=critical,CA:TRUE,pathlen:0
keyUsage=critical,keyCertSign,cRLSign
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always
EOF

openssl x509 -req -in int.csr -CA root.crt -CAkey root.key \
  -out int.crt -days 1825 -sha256 -extfile int.ext

rm -f int.csr int.ext
```

`pathlen:0` znamená „pode mnou už žádná další CA být nesmí". Vystavuje jen koncové certifikáty.

**Teď kořenový klíč ze serveru odnes.** Na šifrovaný flash disk do šuplíku, kopii jinam. Na serveru zůstane `root.crt` (veřejný, ten se rozváží) a `int.key` s `int.crt`, kterými se denně podepisuje.

### Vystavení certifikátu pro službu

```bash
#!/bin/bash
# vystav.sh nas.doma 192.168.1.10 nas.jinak.doma
#
# První parametr je hlavní jméno, další jsou doplňkové jmenné a IP záznamy.
set -euo pipefail

[ $# -ge 1 ] || { echo "pouziti: $0 <jmeno> [dalsi-jmeno|IP ...]"; exit 1; }

CN="$1"; shift
DNY=395
cd "$(dirname "$0")/ca"
umask 077

# subjectAltName: hlavní jméno musí být i tady, samotné CN dnes nikdo nečte
SAN="DNS:${CN}"
for x in "$@"; do
  if [[ "$x" =~ ^[0-9.]+$ || "$x" == *:* ]]; then
    SAN="${SAN},IP:${x}"
  else
    SAN="${SAN},DNS:${x}"
  fi
done

openssl ecparam -name prime256v1 -genkey -noout -out "${CN}.key"
openssl req -new -key "${CN}.key" -out "${CN}.csr" -subj "/CN=${CN}"

cat > "${CN}.ext" <<EOF
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=${SAN}
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
EOF

openssl x509 -req -in "${CN}.csr" -CA int.crt -CAkey int.key \
  -out "${CN}.crt" -days "${DNY}" -sha256 -extfile "${CN}.ext"

# fullchain: certifikát + mezilehlá. Kořenová sem NEPATŘÍ.
cat "${CN}.crt" int.crt > "${CN}.fullchain.crt"

rm -f "${CN}.csr" "${CN}.ext"

openssl verify -CAfile root.crt -untrusted int.crt "${CN}.crt"
echo "Nasaď: ${CN}.fullchain.crt + ${CN}.key"
```

Použití:

```bash
./vystav.sh nas.doma 192.168.1.10
./vystav.sh proxmox.doma 192.168.1.5
```

Čtyři věci, na kterých se to nejčastěji láme:

**`openssl x509 -req` nepřebírá z CSR vůbec nic.** Nejde jen o `subjectAltName` — bez `-extfile` vypadne certifikát úplně bez rozšíření. Vygeneruješ hezký CSR se všemi jmény, podepíšeš ho a dostaneš certifikát, kterému žádný prohlížeč neuvěří.

**Nespoléhej na `-copy_extensions copy`.** OpenSSL 3 umí rozšíření z CSR převzít, jenže převezme i to, co si tam žadatel napsal — včetně `basicConstraints=CA:TRUE`. Kdo ti pošle takový CSR, dostane od tebe podepsanou plnohodnotnou certifikační autoritu a může si pod tvým kořenem vystavit certifikát na cokoliv. Ověřeno prakticky: takhle vzniklý certifikát projde `openssl verify` i jako `www.google.com`. U vlastního CSR to nevadí, u cizího je to zadní vrátka. **Drž se `-extfile`.**

**Common Name se dnes neuznává.** Prohlížeče se dívají výhradně na `subjectAltName`. Certifikát jen s `CN=nas.doma` je neplatný, i když vypadá správně — proto skript hlavní jméno vkládá do SAN taky.

**Nasazuj `fullchain`, ne samotný `.crt`.** Stejná past jako u Let's Encrypt, viz [certifikáty](Certifikaty-a-Lets-Encrypt).

Sériová čísla řešit nemusíš: když `-CAcreateserial` ani `-CAserial` neuvedeš, OpenSSL 3 vygeneruje náhodné dvacetibajtové číslo a žádný soubor nezaloží — dokumentace to označuje za doporučený postup. Jen pozor, že pokud ti ve složce zůstane starý soubor `int.srl`, openssl ho tiše použije a přepne se na řadu za sebou jdoucích čísel.

### Klientský certifikát pro mTLS

Jediný rozdíl je `extendedKeyUsage`:

```bash
cat > klient.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature
extendedKeyUsage=clientAuth
subjectAltName=email:martin@doma
EOF

openssl ecparam -name prime256v1 -genkey -noout -out martin.key
openssl req -new -key martin.key -out martin.csr -subj "/CN=martin"
openssl x509 -req -in martin.csr -CA int.crt -CAkey int.key \
  -out martin.crt -days 365 -sha256 -extfile klient.ext

# do prohlížeče a do telefonu se nosí PKCS#12
openssl pkcs12 -export -out martin.p12 \
  -inkey martin.key -in martin.crt -certfile int.crt
```

`.p12` se importuje do prohlížeče, do telefonu i do Windows. Vyžádá si heslo — nastav ho, prázdné heslo některé systémy odmítnou.

Ověřování klientů pak zapneš na proxy, v [nginxu](Nginx):

```nginx
ssl_client_certificate /etc/ssl/doma/ca-chain.crt;
ssl_verify_client on;
ssl_verify_depth 2;
```

Tohle je použití, kde je vlastní CA jednoznačně správná volba — veřejná CA ti klientský certifikát nevystaví.

## Instalace na koncová zařízení

Rozváží se **jen kořenový certifikát** (`root.crt`). Nikdy klíč.

Zkontroluj si, že posíláš správný soubor:

```bash
openssl x509 -in root.crt -noout -subject -dates -fingerprint -sha256
```

Dobrá zpráva na úvod: na Ubuntu a Debianu jedna instalace pokryje OpenSSL, `curl`, `wget`, Go, systémový Python i Javu. Zvlášť pak musíš řešit už jen Firefox, Chrome, Node a Python v `pip`/venv.

### Linux (Debian, Ubuntu)

```bash
cp root.crt /usr/local/share/ca-certificates/doma-root.crt
update-ca-certificates
```

**Přípona musí být `.crt`** a v souboru smí být jeden certifikát v PEM. Soubor s příponou `.pem` se tiše přeskočí — `update-ca-certificates` vypíše „0 added, 0 removed" a ty budeš hodinu hledat, proč to nefunguje.

Odebrání:

```bash
rm /usr/local/share/ca-certificates/doma-root.crt
update-ca-certificates --fresh
```

Bez `--fresh` se certifikát ze svazku sice odstraní, ale v `/etc/ssl/certs` zůstanou viset mrtvé symlinky. A **nepoužívej `--default`** — ten sice `--fresh` zahrnuje, ale zároveň vrátí zpátky všechny distribuční CA včetně těch, které sis v `/etc/ca-certificates.conf` vypnul.

### Linux (RHEL, Fedora, AlmaLinux)

```bash
cp root.crt /etc/pki/ca-trust/source/anchors/doma-root.crt
update-ca-trust
```

Tady na příponě nezáleží a přijímá se PEM i DER.

### Alpine a kontejnery

V Dockeru se na vlastní CA naráží pořád — kontejner má vlastní úložiště důvěry a o tom hostitelském neví.

```dockerfile
# Debian/Ubuntu základ
COPY doma-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

# Alpine základ — balíček je potřeba doinstalovat
COPY doma-root.crt /usr/local/share/ca-certificates/
RUN apk add --no-cache ca-certificates && update-ca-certificates
```

To `apk add` u Alpine vynechat nejde. Odlehčené obrazy mají jen hotový svazek certifikátů, ne nástroj `update-ca-certificates`, takže bez něj build spadne. Alpine si naopak nevšímá přípony souboru — pravidlo s `.crt` platí jen pro Debian.

Bez přestavby obrazu se dá certifikát podstrčit i za běhu, ale musíš rovnou přepsat celý svazek:

```yaml
volumes:
  - ./doma-bundle.crt:/etc/ssl/certs/ca-certificates.crt:ro
```

`doma-bundle.crt` je spojení systémového svazku a tvého kořenového certifikátu. Připojit jen samotnou CA do `/usr/local/share/ca-certificates/` nestačí, protože v běžícím kontejneru už nikdo `update-ca-certificates` nespustí.

### Windows

V příkazovém řádku jako správce:

```
certutil -addstore -f Root doma-root.crt
```

Bez práv správce jde certifikát přidat jen pro sebe:

```
certutil -user -addstore -f Root doma-root.crt
```

Odebrání je `certutil -delstore Root "Doma Root CA"`, kontrola v `certmgr.msc` → Důvěryhodné kořenové certifikační autority. V doméně se to rozváží přes zásady skupiny (GPO), ne ručně.

**Pozor na jméno `certutil`.** Ve Windows a v Linuxu jsou to dva úplně různé programy, které si nerozumí přepínače. Ten linuxový je [níž u Chrome](#chrome-a-chromium-na-linuxu).

### macOS

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

`-d` znamená pro celý systém (a vyžaduje práva správce), `-r trustRoot` nastaví plnou důvěru. Bez `-d` skončí certifikát jen v uživatelské klíčence.

### iOS a iPadOS

Tady je dvoukrokový postup, na kterém uvízne každý.

1. Pošli si `root.crt` mailem nebo přes AirDrop a otevři ho. Telefon nabídne stažení profilu.
2. **Nastavení → Obecné → VPN a správa zařízení** → nainstaluj profil.
3. **A teď to podstatné:** Nastavení → Obecné → Informace → **Nastavení důvěryhodnosti certifikátů** → přepnout přepínač u tvé CA.

Bez třetího kroku je certifikát nainstalovaný a **nedůvěryhodný**. Nikde se to nepíše a chová se to, jako bys nic neudělal.

### Android

Nastavení → Zabezpečení → Šifrování a přihlašovací údaje → Instalovat certifikát → **certifikát CA**. Cesta se liší podle výrobce. Telefon tě varuje, že tvoje síť může být monitorovaná — což je přesně pravda.

**A pak zjistíš, že to aplikacím nepomohlo.** Od Androidu 7 aplikace ve výchozím nastavení uživatelské CA ignorují; věří jen systémovým. Chrome a ostatní prohlížeče tvoji CA použijí, mobilní klient Nextcloudu nebo Home Assistantu ne.

Obejít se to nedá. Aplikace by musela sama mít v `network_security_config` napsáno, že uživatelským CA věří, což dělá málokdo. A od Androidu 14 se systémové úložiště přesunulo do neměnného modulu aktualizovaného přes Google Play, takže ani telefon s root přístupem už certifikát jednoduše nepřidá.

**Je to ten nejsilnější argument proti vlastní CA v domácnosti** a je dobré na něj narazit teď, ne až po dvou večerech práce.

### Firefox

Firefox má **vlastní úložiště důvěry** a systémové ignoruje. Certifikát nainstalovaný do systému v něm prostě nefunguje.

Ruční cesta, která funguje vždycky: Nastavení → Soukromí a zabezpečení → Certifikáty → **Zobrazit certifikáty** → záložka Autority → **Importovat** → zaškrtnout „Důvěřovat tomuto CA pro identifikaci webových stránek".

Na víc strojů se to dá rozvézt zásadou v `/etc/firefox/policies/policies.json`:

```json
{
  "policies": {
    "Certificates": {
      "Install": ["/usr/local/share/ca-certificates/doma-root.crt"]
    }
  }
}
```

**Nehledej `security.enterprise_roots.enabled`.** Ta volba, kterou doporučuje polovina internetu, v linuxových sestaveních Firefoxu neexistuje — Mozilla ji odmítla doplnit. Funguje na Windows a na macOS, na Linuxu ne. Na Linuxu použij `Certificates.Install` výše.

Na Ubuntu 24.04 je Firefox snap, ale cesta `/etc/firefox/policies/policies.json` platí i pro něj. Ověř si to:

```bash
snap connections firefox | grep etc-firefox
```

A po restartu prohlížeče zkontroluj `about:policies`, jestli se zásada opravdu načetla.

### Chrome a Chromium na Linuxu

Taky nepoužívají systémové úložiště, ale NSS databázi uživatele:

```bash
apt install libnss3-tools
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "Doma Root CA" -i doma-root.crt
certutil -d sql:$HOME/.pki/nssdb -L        # kontrola
```

Tohle `certutil` je z balíčku `libnss3-tools` a s tím windowsovým nemá společného nic než jméno.

### Programy s vlastním úložištěm

Klasický zdroj „vždyť to mám nainstalované, proč to nefunguje":

| Prostředí | Co udělat |
|---|---|
| Node.js | `NODE_EXTRA_CA_CERTS=/cesta/doma-root.crt` |
| Python v pip/venv/Dockeru | `REQUESTS_CA_BUNDLE=…` (requests), `SSL_CERT_FILE=…` (httpx, urllib) |
| Python z balíčku distribuce | nic, systémové úložiště stačí |
| curl | nic; jednorázově `--cacert`, nebo `CURL_CA_BUNDLE` |
| Java z distribuce | nic, `ca-certificates-java` importuje automaticky |
| Java z tarballu nebo v kontejneru | `keytool -importcert -cacerts -alias doma -file doma-root.crt` (heslo `changeit`) |
| Git | `git config --global http.sslCAInfo /cesta/doma-root.crt` |
| Go | nic, používá systémové úložiště |

Python je tu zrádný, protože se dvě obvyklé situace chovají opačně: `python3-requests` z balíčku distribuce je upravený tak, aby četl systémový svazek, kdežto `certifi` nainstalované přes `pip` si nese vlastní seznam a systém úplně ignoruje. Ve venv a v kontejnerech tedy proměnnou nastavit musíš.

Nikdy to neřeš vypnutím ověřování (`curl -k`, `verify=False`, `insecureSkipVerify`). Tím zahazuješ přesně tu vlastnost, kvůli které jsi celou CA stavěl.

## Platnost a obnova

Kolem maximální platnosti koluje spousta nesmyslů, tak si to srovnejme.

**Apple.** Známé omezení na 398 dní platí **jen pro certifikáty od autorit, které Apple sám předinstaloval**. Na tvoji vlastní CA nedopadá. Zato **dopadá starší pravidlo 825 dní**, které se na ručně přidané kořeny vztahuje — a to je tvůj skutečný strop. Desetiletý certifikát na iPhonu ani na Macu fungovat nebude.

**Chrome.** Na certifikáty z lokálně přidaných kořenů nevynucuje žádnou maximální platnost. Ani chystané zkracování veřejných certifikátů na 47 dní se privátních autorit netýká — pravidla CA/Browser Fora se na vnitřní PKI výslovně nevztahují.

**Rozumné nastavení:** kořenová CA 10 let, mezilehlá 5 let, koncové certifikáty kolem roku. Pod 825 dní se vejdeš s rezervou a jednou ročně si aspoň ověříš, že postup ještě funguje.

Krátká platnost má ještě jeden důvod: **odvolávání v praxi nefunguje.** CRL a OCSP si musíš sám provozovat a klienti je stejně často neověřují. Jediná spolehlivá obrana proti ukradenému certifikátu je, že brzy vyprší. Proto step-ca vystavuje ve výchozím stavu na 24 hodin a Caddy na 12.

Připomínku na obnovu si nastav hned, protože za rok si nevzpomeneš:

```bash
# co všechno mám a do kdy to platí
for f in ca/*.crt; do
  printf '%-28s %s\n' "$f" "$(openssl x509 -in "$f" -noout -enddate)"
done
```

A po každé výměně certifikátu nezapomeň službu znovu načíst — `systemctl reload nginx` nebo `docker compose restart`.

## Ochrana kořenového klíče

Zopakuju to, protože je to ta část, kterou lidé odbydou:

**Kořenový klíč nepatří na server vystavený do internetu.** Šifrovaný flash disk, druhá kopie jinde. Když ho někdo získá, může se vydávat za jakýkoliv web, který má tvoje [omezení jmen](#omezení-jmen-nameconstraints) povolený — a bez něj za jakýkoliv web vůbec.

```bash
chmod 600 ca/*.key
```

Klíč můžeš zašifrovat heslem (`openssl ec -aes256 -in root.key -out root.key.enc`), ale u mezilehlé CA to znamená zadávat heslo při každém podpisu. Proto se to řeší tak, jak je popsáno výše: kořen zašifrovaný a offline, mezilehlá dostupná.

A hlavně — **až tě to přestane bavit, CA odinstaluj.** Opuštěná CA, jejíž klíč leží zapomenutý na starém disku, je horší než žádná. Kořenový certifikát ze zařízení odeber stejnou cestou, jakou jsi ho tam dal.

## Diagnostika

```bash
# co je v certifikátu
openssl x509 -in nas.doma.crt -text -noout

# platí řetěz?
openssl verify -CAfile root.crt -untrusted int.crt nas.doma.crt

# co doopravdy posílá server
openssl s_client -connect nas.doma:443 -servername nas.doma -CAfile root.crt < /dev/null

# vidí systém moji CA?
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt root.crt
```

| Problém | Příčina |
|---|---|
| `unable to get local issuer certificate` | klient nemá kořenový certifikát, nebo server neposílá mezilehlý |
| `self-signed certificate in certificate chain` | nasadil jsi `cert.crt` místo `fullchain.crt` |
| Funguje v prohlížeči, ne v `curl` | totéž — chybí mezilehlý certifikát |
| `NET::ERR_CERT_COMMON_NAME_INVALID` | chybí `subjectAltName`, nepoužil jsi `-extfile` |
| `permitted subtree violation` | jméno je mimo `nameConstraints` kořenové CA, nebo jsi použil krátké jméno bez domény |
| `update-ca-certificates` hlásí „0 added" | soubor nemá příponu `.crt` |
| Funguje na počítači, ne v mobilní aplikaci | Android — uživatelské CA aplikace neuznávají |
| Na iPhonu pořád varování | neproběhlo zapnutí v Nastavení důvěryhodnosti certifikátů |
| Funguje v Chrome, ne ve Firefoxu | Firefox má vlastní úložiště důvěry |
| Funguje na hostiteli, ne v kontejneru | kontejner má vlastní úložiště důvěry |
| Na iPhonu neplatný, jinde v pořádku | platnost delší než 825 dní |

## Shrnutí

Vlastní certifikační autorita je funkční nástroj s úzkým využitím: klientské certifikáty, sítě bez internetu, stroje které spravuješ všechny. Pro „chci mít doma všude zelený zámeček" je to špatný nástroj — zaplatíš za něj opakovanou ruční prací na každém zařízení, nefunkčními mobilními aplikacemi a klíčem, který musíš navždy hlídat.

Doména za dvě stovky ročně a [DNS-01](Certifikaty-a-Lets-Encrypt) ten problém řeší lépe, levněji a jednou provždy.

A jestli přesto jdeš do vlastní CA: použij [step-ca](#step-ca-vlastní-ca-s-acme), nastav [omezení jmen](#omezení-jmen-nameconstraints) a kořenový klíč nenechávej na serveru.
