2 - Let's Encrypt¶
K3s Homelab — Sesja 02¶
Data: 2026-02-27
Środowisko: 3x HP T630, k3s v1.34.4, HAProxy, Traefik
Co zbudowaliśmy¶
Działający HTTPS z prawdziwym certyfikatem Let's Encrypt na lokalnym klastrze bez wystawiania go na świat:
Użytkownik → HAProxy:6453 (tcp/passthrough) → Traefik:443 → Pod
↑
cert-manager + Let's Encrypt
(DNS-01 via Cloudflare API)
Czego się nauczyłem¶
1. Helm — menedżer pakietów dla Kubernetes¶
Czym jest Helm:
kubectl apply -fto jednorazowe "wrzuć do klastra" bez śledzenia stanu- Helm zarządza cyklem życia — wie co zainstalował, może upgrade'ować, rollback'ować i odinstalować jako całość jedną komendą
- Paczka w Helmie to chart — zawiera szablony manifestów + domyślne
values.yaml
Workflow z Helmem — zawsze trzy kroki:
# 1. Dodaj repozytorium
helm repo add jetstack https://charts.jetstack.io
# 2. Przejrzyj dostępne opcje konfiguracji
helm show values jetstack/cert-manager | grep -i crd
# 3. Zainstaluj z własnymi wartościami
helm install cert-manager jetstack/cert-manager \
-n cert-manager \
--create-namespace \
--set crds.enabled=true
Ważne flagi:
--create-namespace— tworzy namespace jeśli nie istnieje (nie musisz robićkubectl create nsosobno)--set klucz=wartość— nadpisuje wartość zvalues.yaml- Format charta:
nazwa-repo/nazwa-charta
CRD (CustomResourceDefinition):
- Rozszerzają Kubernetes API o nowe typy zasobów
- Bez CRD cert-managera k8s nie wie co to
Certificate,ClusterIssuer,CertificateRequest crds.enabled=truemówi Helmowi żeby zainstalował te definicje razem z chartem
2. cert-manager — automatyczne zarządzanie certyfikatami¶
Architektura cert-managera (3 pody):
cert-manager— główny kontroler, zarządza cyklem życia certyfikatówcert-manager-cainjector— wstrzykuje CA do webhooków k8scert-manager-webhook— waliduje zasoby cert-managera przy tworzeniu
Dlaczego nie potrzebuje replik HA: cert-manager nie jest w krytycznej ścieżce obsługi requestów użytkownika. Zajmuje się wystawianiem i odnawianiem certyfikatów — operacjami periodycznymi, nie ciągłymi. Jeśli padnie na kilka minut, istniejące certyfikaty dalej działają.
Rotacja certyfikatów:
- cert-manager odnawia certyfikat automatycznie gdy zostaje ⅓ ważności
- Let's Encrypt: certyfikaty 90-dniowe → odnowienie ~30 dni przed wygaśnięciem
- Zero manualnej pracy po konfiguracji
3. Issuer vs ClusterIssuer¶
| Issuer | ClusterIssuer | |
|---|---|---|
| Scope | Jeden namespace | Cały klaster |
| Użycie | Certyfikaty tylko w tym samym namespace | Certyfikaty dla dowolnego namespace |
| Kiedy używać | Izolacja per-team/per-app | Współdzielony CA dla całego klastra |
Dla homelaba: zawsze ClusterIssuer — jeden issuer dla wszystkich aplikacji.
ClusterIssuer jest cluster-scoped jak Node czy PersistentVolume — nie należy do żadnego namespace, nie podajesz -n przy apply.
4. Let's Encrypt — staging vs produkcja¶
Dlaczego staging pierwszy:
- Let's Encrypt produkcja ma rate limits — max 5 certyfikatów na domenę na tydzień
- Staging nie ma limitów ale certyfikaty nie są zaufane przez przeglądarki
- Zawsze testuj na staging, przełącz na prod gdy wiesz że działa
Dwa środowiska:
Staging: https://acme-staging-v02.api.letsencrypt.org/directory
Prod: https://acme-v02.api.letsencrypt.org/directory
5. DNS-01 Challenge — certyfikaty bez otwierania portów¶
HTTP-01 challenge: Let's Encrypt odpytuje http://twojadomena.com/.well-known/acme-challenge/... — wymaga że serwer jest dostępny z internetu.
DNS-01 challenge: Let's Encrypt sprawdza rekord TXT _acme-challenge.twojadomena.com w publicznym DNS — klaster nie musi być dostępny z internetu!
Flow DNS-01 z Cloudflare:
- cert-manager dostaje żądanie certyfikatu
- Używa Cloudflare API żeby dodać rekord
TXT _acme-challenge.cluster.kcn333.com - Let's Encrypt weryfikuje rekord TXT
- cert-manager usuwa rekord TXT
- Certyfikat wystawiony → zapisany jako Secret w k8s
Cloudflare API Token — uprawnienia (zasada least privilege):
Zone | DNS | EditZone | Zone | Read- Zone Resources: Specific zone (tylko Twoja domena)
NIE używaj Global API Key — zbyt szerokie uprawnienia.
6. Przechowywanie sekretów w k8s¶
API Token Cloudflare przechowujemy jako Secret, nie wklejamy bezpośrednio do manifestu ClusterIssuer:
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-token
namespace: cert-manager
type: Opaque
stringData:
api-token: <token>
Dlaczego to ważne: manifest ClusterIssuer może trafić do GitHuba — Secret nie powinien. W produkcji używa się Sealed Secrets lub External Secrets Operator żeby bezpiecznie trzymać sekrety w repo (temat na później).
7. Zasoby cert-managera — przepływ¶
ClusterIssuer — konfiguracja CA (Let's Encrypt + Cloudflare)
Certificate — żądanie certyfikatu dla konkretnej domeny
CertificateRequest — pojedyncze żądanie do CA
Order — sesja ACME
Challenge — weryfikacja kontroli domeny
Secret — gotowy certyfikat (tls.crt + tls.key)
Diagnostyka gdy coś nie działa:
kubectl describe certificate <nazwa>
kubectl get certificaterequest
kubectl get orders
kubectl get challenges
kubectl describe clusterissuer <nazwa>
8. Manifest Certificate¶
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: local-prod-cert-kcn333
spec:
secretName: local-prod-kcn333-tls # nazwa Secretu który zostanie utworzony
commonName: "*.cluster.kcn333.com"
dnsNames:
- "cluster.kcn333.com"
- "*.cluster.kcn333.com" # wildcard dla subdomen
issuerRef:
name: letsencrypt-prod-cluster-issuer
kind: ClusterIssuer
Częsty błąd: w Ingress podawanie nazwy Certificate zamiast nazwy Secret w polu secretName. Traefik potrzebuje nazwy Secretu.
9. Ingress z TLS¶
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
traefik.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: traefik
tls:
- hosts:
- nginx.cluster.kcn333.com
secretName: local-prod-kcn333-tls # nazwa Secretu, nie Certyfikatu!
rules:
- host: nginx.cluster.kcn333.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
10. HAProxy — tryby i TLS¶
Dwa tryby HAProxy:
mode http— HAProxy rozumie HTTP, może modyfikować nagłówki, ale wymaga odszyfrowania TLSmode tcp— HAProxy przepuszcza surowe bajty, "ślepy" na zawartość → TLS Passthrough
Kiedy używać:
- API server k8s (port 6443) →
mode tcp(TLS terminowany przez API server) - Ingress HTTP (port 6480→80) →
mode http(brak TLS) - Ingress HTTPS (port 6453→443) →
mode tcp(TLS terminowany przez Traefik)
Health checki a tryb TCP:
option httpchknie ma sensu wmode tcp— HAProxy nie może parsować HTTP- Traefik odpowiada
404na nieznane ścieżki → domyślnie HAProxy uznaje za "unhealthy" - Rozwiązanie: usunąć
checklub użyćhttp-check expect status 200,301,302,404
11. Debugowanie certyfikatów¶
# Sprawdź jaki certyfikat serwuje serwer
echo | openssl s_client -connect domena.com:443 2>/dev/null | \
openssl x509 -text -noout | grep -A2 "Issuer\|Subject"
# Staging: Issuer zawiera "(STAGING)"
# Prod: Issuer: C = US, O = Let's Encrypt, CN = R10/R11/R12
12. Problem z przekierowaniem HTTP→HTTPS przy niestandardowych portach¶
Traefik przekierowuje HTTP na https://domena.com (port 443). Przy niestandardowych portach (6453 zamiast 443) przekierowanie kieruje na zły port.
Opcje rozwiązania (do przemyślenia):
- Cloudflare Tunnel — ruch wchodzi przez tunel na standardowych portach
- Przepięcie nginx-proxy-manager na inne porty i zajęcie 80/443 przez HAProxy
- Zaakceptowanie niestandardowych portów i rezygnacja z automatycznego przekierowania
Backlog (do zrobienia)¶
- Rozwiązać problem z portami (Cloudflare Tunnel lub przepięcie NPM)
- Traefik dashboard z BasicAuth
- PersistentVolume / PersistentVolumeClaim
- Zablokować bezpośredni dostęp do portów nodów (ufw + NetworkPolicy)
- Flux — GitOps, CD
- RBAC — własni użytkownicy
- Własna aplikacja w Pythonie lub Javie
- Własne Helm charts
- Pełne CI/CD pipeline
- Sealed Secrets / External Secrets Operator
Przydatne komendy¶
# Helm
helm repo add <nazwa> <url>
helm repo update
helm show values <repo/chart>
helm install <release> <repo/chart> -n <namespace> --create-namespace --set klucz=wartość
helm list -A
helm uninstall <release> -n <namespace>
# cert-manager diagnostyka
kubectl get certificate -A
kubectl get clusterissuer
kubectl get challenges
kubectl get orders
kubectl describe certificate <nazwa>
kubectl describe clusterissuer <nazwa>
# Sprawdzenie certyfikatu TLS
echo | openssl s_client -connect <host>:<port> 2>/dev/null | openssl x509 -text -noout | grep -A2 "Issuer\|Subject\|Validity"