← back to /blog← powrót do /blog
Własne CA w domowym labie: step-ca na QNAP-ie i koniec z klikaniem "i tak przejdź"
Każdy, kto ma w domu cokolwiek na HTTPS-ie (Vaultwardena, Proxmoxa, NAS-a, Home Assistanta), zna ten moment: przeglądarka krzyczy na czerwono, a Ty po raz setny klikasz “Zaawansowane → Przejdź dalej (niebezpieczne)”. Po miesiącu przestajesz czytać te ostrzeżenia, a to jest dokładnie ten nawyk, którego nie chcesz mieć.
Let’s Encrypt tu nie pomoże: nie wystawi certu na *.cygal.lan, bo tej domeny nie
ma w publicznym DNS-ie i nie da się jej zwalidować. Zostają dwa wyjścia: wieczne
self-signed albo własne CA. Postawiłem własne i od tamtej pory w domu jest
zielona kłódka wszędzie tam, gdzie powinna być.
Poniżej dokładnie to, co u mnie stoi, w formie do przeklejenia.
Co stoi u mnie
- QNAP „Selene” (
192.168.100.210), a na nim Container Station (czyli zwykły Docker z ładnym UI). - Kontener
smallstep/step-cana porcie9000, czyli całe CA, nazwaneCygal-Home-CA. - Kontener
caddy:2w dwóch rolach: front HTTPS dla Vaultwardena (port8443) i serwer statyczny ze stroną self-service do instalacji root CA (port8088). - Domena wewnętrzna:
*.cygal.lan.
Całość to dwa kontenery i jeden plik konfiguracyjny. Naprawdę nie trzeba do tego klastra ani dedykowanej maszyny.
Krok 1: step-ca w kontenerze
W Container Station (albo zwykłym docker compose, bez różnicy) wrzucasz:
services:
step-ca:
image: smallstep/step-ca:latest
container_name: step-ca
restart: unless-stopped
ports:
- "9000:9000"
volumes:
- /share/CACHEDEV1_DATA/step-ca:/home/step
environment:
DOCKER_STEPCA_INIT_NAME: "Cygal-Home-CA"
DOCKER_STEPCA_INIT_DNS_NAMES: "selene.cygal.lan,192.168.100.210,localhost"
DOCKER_STEPCA_INIT_PROVISIONER_NAME: "admin"
Dwie rzeczy, które warto zrobić dobrze od razu:
DOCKER_STEPCA_INIT_DNS_NAMES wpisz komplet. To są nazwy, pod którymi CA samo
siebie wystawia. Jeśli dziś chodzisz po IP, a jutro dodasz DNS, to bez tej nazwy na
liście dostaniesz błąd TLS przy każdym step i będziesz musiał przeinicjować CA.
U mnie są trzy: FQDN, IP i localhost.
Hasło. Przy pierwszym starcie kontener wygeneruje hasło do klucza CA i wypisze je w logach. Wyciągnij je i schowaj w menedżerze haseł (u mnie: w tym samym Vaultwardenie, który potem tym CA zabezpieczam; tak, wiem):
docker logs step-ca 2>&1 | grep -i password
Sprawdzenie, że żyje:
docker exec step-ca step ca health --ca-url https://localhost:9000 \
--root /home/step/certs/root_ca.crt
Teraz najważniejsza liczba w całej zabawie: fingerprint roota. To on zastępuje zaufanie do kanału: certyfikat możesz rozdawać choćby po zwykłym HTTP, byle odbiorca porównał fingerprint.
docker exec step-ca step certificate fingerprint /home/step/certs/root_ca.crt
Zapisz go sobie, będzie potrzebny w każdym kolejnym kroku.
Krok 2: pierwszy cert dla usługi
Domyślnie step-ca stawia provisioner typu JWK (u mnie nazwany admin), czyli „żeby dostać
cert, podaj hasło”. Wystawienie certu dla Vaultwardena:
docker exec -it step-ca step ca certificate \
"vault.cygal.lan" \
/home/step/certs/vw.crt /home/step/secrets/vw.key \
--provisioner admin \
--not-after 8760h
8760h to rok. Wrócę do tego niżej, bo to jest właśnie to miejsce, gdzie mój
setup ma dziś dług.
Cert plus klucz kopiujesz tam, gdzie ich potrzebuje usługa. U mnie do katalogu
montowanego do Caddy’ego jako /certs.
Krok 3: Caddy jako front HTTPS
Vaultwarden siedzi na zwykłym HTTP w kontenerze, a HTTPS-em zajmuje się Caddy.
Mój Caddyfile, w całości:
{
admin off
auto_https off
}
# Strona self-service do instalacji root CA (HTTP, bo nowe urządzenia jeszcze
# nie ufają CA; integralność chroni weryfikacja fingerprintu w skryptach).
# Publikowane na hoście jako :8088.
:80 {
root * /srv
file_server
}
# Front-end HTTPS dla Vaultwardena.
# Statyczny cert wystawiony przez step-ca (Cygal-Home-CA), ważny 1 rok.
# Publikowane na hoście jako :8443.
:443 {
tls /certs/vw.crt /certs/vw.key
encode gzip
reverse_proxy 192.168.100.210:32787 {
header_up X-Real-IP {remote_host}
}
}
Jedna linijka wymaga komentarza: auto_https off. Caddy domyślnie jest
sprytny i sam próbuje załatwić certy z Let’s Encrypt. W sieci .lan to się nie
uda i skończy się pętlą błędów w logach. Skoro cert dostarczam ręcznie,
wyłączam automat.
Krok 4: rozdanie root CA po całym domu
To jest ta część, którą najczęściej się olewa, a bez niej całe CA nie ma sensu: każde urządzenie musi mieć Twój root w trust storze. Laptop, telefon żony, służbowy Mac, Raspberry Pi, konsola.
Zrobiłem sobie prostą stronę self-service: statyczny HTML serwowany przez
Caddy’ego na :8088, z rootem, skryptami instalacyjnymi i fingerprintem na
wierzchu. Wchodzisz z nowego urządzenia, klikasz swoją platformę, wklejasz jedną
komendę. Struktura katalogu /srv:
/srv
├── index.html
├── root_ca.crt
├── install-linux.sh
├── install-macos.sh
└── install-windows.ps1
Skrypt dla Linuxa. Sedno sprawy to weryfikacja fingerprintu, bo dzięki niej transport po HTTP jest w porządku:
#!/usr/bin/env bash
set -euo pipefail
CERT_NAME="cygal-home-ca-root"
EXPECTED_FP="<TU_WKLEJ_SWÓJ_FINGERPRINT>"
CERT_URL="${CERT_URL:-http://192.168.100.210:8088/root_ca.crt}"
[ "$(id -u)" -eq 0 ] || { echo "Uruchom przez sudo" >&2; exit 1; }
TMP="$(mktemp)"; trap 'rm -f "$TMP"' EXIT
curl -fsSL "$CERT_URL" -o "$TMP"
FP="$(openssl x509 -in "$TMP" -noout -fingerprint -sha256 \
| sed 's/.*=//; s/://g' | tr 'A-F' 'a-f')"
if [ "$FP" != "$EXPECTED_FP" ]; then
echo "BŁĄD: fingerprint się nie zgadza!" >&2
exit 1
fi
echo "Fingerprint OK: $FP"
if [ -d /usr/local/share/ca-certificates ] && command -v update-ca-certificates >/dev/null; then
install -m 0644 "$TMP" "/usr/local/share/ca-certificates/${CERT_NAME}.crt"
update-ca-certificates
elif [ -d /etc/pki/ca-trust/source/anchors ] && command -v update-ca-trust >/dev/null; then
install -m 0644 "$TMP" "/etc/pki/ca-trust/source/anchors/${CERT_NAME}.crt"
update-ca-trust extract
else
echo "BŁĄD: nieznana dystrybucja." >&2
exit 1
fi
echo "OK: root CA zainstalowany."
Użycie z dowolnego domowego kompa:
curl -fsSL http://192.168.100.210:8088/install-linux.sh | sudo bash
macOS (System Keychain, poprosi o hasło admina):
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain root_ca.crt
Windows, PowerShell jako Administrator:
Import-Certificate -FilePath .\root_ca.crt `
-CertStoreLocation Cert:\LocalMachine\Root
Jeśli na maszynie masz step CLI, całość załatwia jedna komenda, która pobiera root,
sprawdza fingerprint i instaluje do systemu:
step ca bootstrap \
--ca-url https://192.168.100.210:9000 \
--fingerprint <TU_WKLEJ_SWÓJ_FINGERPRINT> \
--install
Trzy pułapki, na które i tak wpadniesz
- Firefox ma własny trust store i systemowy go nie obchodzi. Albo
about:config→security.enterprise_roots.enabled→true, albo ręczny import w Ustawieniach. Chrome, Edge i Safari biorą z systemu. - iOS ma dwa kroki. Instalacja profilu to dopiero połowa: potem Ustawienia → Ogólne → Informacje → Zaufanie certyfikatów i tam trzeba ręcznie przełączyć suwak. Bez tego cert jest zainstalowany i… ignorowany.
- Androidowe „user certificates” są ignorowane przez aplikacje (tylko przeglądarka je uzna), o ile aplikacja nie deklaruje inaczej. Na telefonie nastaw się na przeglądarkę.
Krok 5: automatyzacja odnawiania (to mój następny krok)
Uczciwie: u mnie tego jeszcze nie ma. Mam certy roczne i wystawiane ręcznie, czyli klasyczny tykający zegar. Za jedenaście miesięcy zapomnę, że to w ogóle istnieje, i Vaultwarden padnie w najmniej wygodnym momencie. Tak to zamierzam naprawić i tak to poleciłbym zrobić od razu, jeśli zaczynasz od zera.
Klucz to ACME. step-ca umie być własnym Let’s Encryptem. Dodajesz provisioner:
docker exec -it step-ca step ca provisioner add acme --type ACME
docker restart step-ca
(Przy stawianiu od zera wystarczy DOCKER_STEPCA_INIT_ACME: "true" w compose.)
I nagle Caddy potrafi wszystko sam, bez ręcznych plików .crt:
{
acme_ca https://192.168.100.210:9000/acme/acme/directory
acme_ca_root /certs/root_ca.crt
}
vault.cygal.lan {
reverse_proxy 192.168.100.210:32787
}
Caddy sam wystawi cert, sam go odnowi, sam przeładuje konfigurację. Zniknie cały krok 2 i połowa moich pilnowanek.
Krok 6: certy dla samych maszyn (i jak się same odnawiają)
Certy dla usług za Caddym to jedno, ale prędzej czy później chcesz cert na samej maszynie: do mTLS między usługami, do Prometheusa, do panelu Proxmoxa, do syslogu po TLS. Tu Caddy niczego nie załatwi; host musi umieć wystawić i odnowić cert sam.
Najpierw step CLI z oficjalnego repo (Debian/Ubuntu):
sudo apt-get update && sudo apt-get install -y --no-install-recommends curl gpg ca-certificates
sudo curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg \
-o /etc/apt/keyrings/smallstep.asc
cat <<'EOF' | sudo tee /etc/apt/sources.list.d/smallstep.sources
Types: deb
URIs: https://packages.smallstep.com/stable/debian
Suites: debs
Components: main
Signed-By: /etc/apt/keyrings/smallstep.asc
EOF
sudo apt-get update && sudo apt-get install -y step-cli
Potem bootstrap do CA, czyli to samo polecenie co w kroku 4: pobiera root, weryfikuje fingerprint i instaluje go do systemu:
sudo step ca bootstrap \
--ca-url https://192.168.100.210:9000 \
--fingerprint <TU_WKLEJ_SWÓJ_FINGERPRINT> \
--install
I wystawiasz cert dla hosta:
sudo mkdir -p /etc/step/certs
sudo step ca certificate "$(hostname -f)" \
"/etc/step/certs/$(hostname -s).crt" \
"/etc/step/certs/$(hostname -s).key" \
--provisioner admin
Teraz rzecz, przez którą to w ogóle nadaje się do automatu: odnowienie nie
wymaga hasła do provisionera. step ca renew uwierzytelnia się dotychczasowym
certyfikatem (mTLS), więc na maszynie nie musi leżeć żaden sekret CA. Dzięki
temu automatyczny odnawiacz jest bezpieczny nawet na laptopie, który wyjeżdża
z domu.
Smallstep ma na to gotowy wzorzec z szablonami unitów. Plik
/etc/systemd/system/cert-renewer@.service:
[Unit]
Description=Certificate renewer for %I
After=network-online.target
Documentation=https://smallstep.com/docs/step-ca/renewal
StartLimitIntervalSec=0
[Service]
Type=oneshot
User=root
Environment=STEPPATH=/etc/step-ca \
CERT_LOCATION=/etc/step/certs/%i.crt \
KEY_LOCATION=/etc/step/certs/%i.key
; Odnawiaj tylko wtedy, gdy faktycznie trzeba.
ExecCondition=/usr/bin/step certificate needs-renewal ${CERT_LOCATION}
ExecStart=/usr/bin/step ca renew --force ${CERT_LOCATION} ${KEY_LOCATION}
; Przeładuj usługę, która z tego certu korzysta (jeśli istnieje).
ExecStartPost=/usr/bin/env sh -c "! systemctl --quiet is-active %i.service || systemctl try-reload-or-restart %i"
[Install]
WantedBy=multi-user.target
I timer /etc/systemd/system/cert-renewer@.timer, który sprawdza co 15 minut, czy już
czas; o to, żeby nie odnawiać za wcześnie, dba ExecCondition wyżej:
[Unit]
Description=Timer for certificate renewal of %I
[Timer]
Persistent=true
OnCalendar=*:1/15
AccuracySec=1us
RandomizedDelaySec=5m
[Install]
WantedBy=timers.target
Włączasz per usługa, która z certu korzysta. Nazwa po @ to nazwa tej usługi,
bo unit sam ją potem przeładuje:
sudo systemctl enable --now cert-renewer@nginx.timer
systemctl list-timers | grep cert-renewer
Kontrola, czy to faktycznie żyje:
step certificate inspect --short /etc/step/certs/$(hostname -s).crt
sudo systemctl status cert-renewer@nginx.service
Jedna pułapka, konkretnie na laptopy. step ca renew działa dopóki stary
cert jeszcze żyje: to on jest przepustką. Jeśli maszyna była wyłączona dłużej
niż okres ważności, odnowienie się nie uda i trzeba wystawić cert od zera
(znów step ca certificate z provisionerem). Przy rocznych certach to nieistotne,
przy 24-godzinnych laptop wróci z wakacji z martwym certem. Dobierz --not-after
do tego, jak sprzęt naprawdę bywa włączony: serwery krótko, laptopy z zapasem.
I na koniec: skróć czas życia certów. Rok to relikt świata ręcznych certów. Jak odnawianie chodzi samo, zejdź do tygodnia albo doby. Krótki cert to mniejsze okno, gdyby klucz wyciekł, i natychmiastowa informacja, że automat się zepsuł, zamiast cichej bomby zegarowej odpalającej się za rok.
Skąd wiem, że to wszystko faktycznie działa
Piszę sobie do tego narzędzie certops, bo ręczne sprawdzanie openssl s_client przestaje się skalować przy trzeciej
usłudze. Sprawdza CA (m.in. providera Smallstep), certy na endpointach, chain,
daty i trust store’y, lokalnie i po SSH na całej flocie:
certops check vault.cygal.lan:8443
certops fleet trust verify -f certops.yaml
fleet trust verify odpowiada dokładnie na pytanie „czy każda maszyna w domu ma
mój root i czy wszystkie mają ten sam”, czyli na to, co przy ręcznym rozdawaniu
certów najłatwiej przegapić.
Podsumowanie
Efekt po jednym wieczorze: dwa kontenery, jeden Caddyfile, jedna strona self-service. Zielona kłódka na wszystkim w domu, zero klikania „i tak przejdź”, i, co dla mnie najważniejsze, odruch, że czerwony ekran naprawdę oznacza problem, a nie codzienną niedogodność do przeklikania.
Jeśli zaczynasz od zera, zrób jedną rzecz inaczej niż ja: włącz ACME od razu. Reszta jest do przeklejenia z góry.
- Paweł