Secure Homelab Based on bootc: prywatny ingress, lokalny storage i przewidywalny lifecycle hostów
Table of Contents
- Problem i constraints
- Architektura w jednym akapicie
- WAN ingress i lokalny dostęp
- Dlaczego VM + kontener, a nie jeden duży Docker host
- Dlaczego Caddy jest policy engine, a nie tylko reverse proxy
- Dlaczego bootc + lokalny registry zmieniły operację
- Lokalny registry i kontrolowany rollout
- Security posture: co poprawia, a czego nie udaje
- Day-2 ops: co jest uporządkowane, a co nadal jest po prostu rozsądnie obsługiwane
- Trade-offy, które były świadome
- Co ten projekt mówi o autorze jako inżynierze
Mój domowy ISP blokował klasyczny inbound, ale nie chciałem przenosić prywatnych i stanowych workloadów do chmury. Rozwiązaniem okazał się tani VPS jako publiczny anchor, WireGuard + VXLAN do przeniesienia publicznego endpointu do domu, Caddy jako centralny edge, osobne VM w DMZ dla usług oraz bootc + lokalny registry do przewidywalnego lifecycle hostów.
Efekt nie jest „prywatną chmurą” ani miniaturowym enterprise. To po prostu spójny model operacyjny: publiczny ingress poza domem, prywatny compute i storage u siebie, mniejszy blast radius i jeden powtarzalny sposób budowy, wdrażania i cofania zmian.
Uwaga redakcyjna: w tekście anonimizuję publiczne IP, wybrane domeny i część nazw hostów. Gdy opis upraszczam, robię to świadomie na poziomie modelu logicznego. Source of truth dla bieżącego stanu pozostaje repo, aktualne pliki sieciowe i
Caddyfile; starsze README w repo opisują też wcześniejsze iteracje środowiska.
Problem i constraints
To nie zaczęło się od chęci „zrobienia prywatnej platformy”, tylko od kilku konkretnych ograniczeń:
- ISP blokował normalne wystawienie usług z domu.
- Chciałem trzymać compute i storage lokalnie, bo przy usługach stanowych to właśnie storage szybko robi się najdroższą częścią rachunku.
- Nie chciałem jednego dużego Docker hosta, na którym kompromitacja jednej usługi staje się problemem całego środowiska.
- Nie chciałem też pełnego VPN jako jedynego modelu dostępu do wszystkiego.
- Zależało mi na tym, żeby hosty dało się budować i odtwarzać jako artefakty, a nie przez ręczne SSH i pamięć operatora.
To od razu zawęziło możliwe rozwiązanie: publiczny endpoint musi istnieć poza domowym ISP, właściwe workloady mają siedzieć u mnie, a edge, routing i lifecycle hostów muszą tworzyć jeden powtarzalny model.
Architektura w jednym akapicie
Tani VPS z floating IP pełni rolę publicznego punktu wejścia. Do domu prowadzi tunel WireGuard, a nad nim działa VXLAN, dzięki czemu publiczny endpoint logicznie kończy się na Caddy VM w domowym Proxmoxie. Caddy terminates TLS, wymusza mTLS dla wybranych powierzchni i proxy'uje ruch do backendów w DMZ albo do kilku hostów LAN. Same workloady są organizowane zwykle w modelu 1 VM = 1 usługa albo 1 mały stack, a hosty są dostarczane jako obrazy bootc z lokalnego registry.
Diagram 1: model logiczny
flowchart TB
subgraph WAN["WAN / public edge"]
U["Klient z Internetu"]
DNS["Public DNS<br/>*.example.com -> floating IP"]
VPS["Tani VPS / public anchor<br/>VyOS + floating IP"]
WG1["WireGuard"]
U --> DNS --> VPS --> WG1
end
subgraph HOME["Domowy homelab"]
subgraph EDGE["Edge"]
WG2["WireGuard"]
VX["VXLAN + floating IP"]
CADDY["Caddy VM<br/>TLS, mTLS, reverse proxy, L4"]
WG2 --> VX --> CADDY
end
subgraph DMZ["DMZ 10.20.0.0/24"]
REG["Registry VM<br/>10.20.0.50"]
NEXT["Nextcloud VM<br/>10.20.0.30<br/>Podman stack"]
IMM["Immich VM<br/>10.20.0.40<br/>Podman stack"]
MC["Minecraft VM<br/>10.20.0.60<br/>TCP przez L4"]
BLOG["Blog VM<br/>10.20.0.90<br/>natywny nginx"]
end
VYOS["VyOS<br/>gateway DMZ / DHCP reservations"]
LAN["Wybrane hosty LAN<br/>np. Proxmox UI"]
end
WG1 --> WG2
CADDY --> NEXT
CADDY --> IMM
CADDY --> MC
CADDY --> BLOG
CADDY --> LAN
VYOS --> DMZ
WAN ingress i lokalny dostęp
Najważniejsza decyzja architektoniczna polegała na rozdzieleniu publicznego ingressu od miejsca, w którym realnie działa compute.
Z zewnątrz ruch idzie klasycznie: domena rozwiązuje się do floating IP, ruch trafia na VPS, a stamtąd overlay przenosi go do Caddy VM w domu. To pozwala wystawić usługi mimo blokad ISP, ale bez stawiania właściwych workloadów na VPS.
W domu ta sama domena nie musi robić niepotrzebnego round-trip przez WAN. Lokalny routing i DNS pozwalają rozwiązać wybrane usługi do adresów wewnętrznych, więc użytkownik w LAN dochodzi do tego samego serwisu szybciej i bez wychodzenia przez Internet.
Diagram 2: ten sam adres, dwa różne wejścia
flowchart LR
subgraph WAN["Dostęp z Internetu"]
A["Klient poza domem"] --> B["Public DNS"]
B --> C["floating IP na VPS"]
C --> D["WireGuard + VXLAN"]
D --> E["Caddy VM"]
E --> F["Backend w DMZ lub LAN"]
end
subgraph LAN["Dostęp lokalny"]
G["Klient w LAN"] --> H["Lokalny DNS / routing"]
H --> I["Caddy VM lub backend lokalny"]
I --> J["Usługa w DMZ"]
end
Dlaczego VM + kontener, a nie jeden duży Docker host
Dominujący model tego repo to osobna bootc VM dla jednej usługi albo małego stacka, a wewnątrz niej dopiero Podman i Quadlet. Praktycznie wygląda to tak:
- Nextcloud i Immich działają jako wielokontenerowe stacki wewnątrz własnych VM.
- Minecraft, AdGuard czy registry to pojedyncze workloady we własnych VM.
- Caddy i blog działają jako natywne usługi systemd, bez wciskania wszystkiego do Podmana na siłę.
To nie jest estetyczny detal, tylko świadomy kompromis. Jeśli jedna publicznie wystawiona aplikacja zostanie skompromitowana, atakujący nie ląduje od razu na hoście, który trzyma wszystkie pozostałe usługi. Najpierw musi przejść przez granicę aplikacji, potem przez VM, a dopiero później próbować ruszyć resztę środowiska.
Nie udaję tutaj pełnej mikrosegmentacji enterprise. Główny segment usługowy to nadal prosty DMZ /24, więc bezpieczeństwo opiera się bardziej na sensownych granicach i centralnym edge niż na skrajnym rozdrobnieniu sieci.
Dlaczego Caddy jest policy engine, a nie tylko reverse proxy
Caddy jest najważniejszym hostem na krawędzi tej architektury. To on ma kontakt z publicznym ruchem, widzi overlay do VPS i decyduje, które backendy oraz które zasady dostępu obowiązują dla danej domeny.
W praktyce Caddy robi tu cztery rzeczy:
- Kończy TLS dla publicznych domen.
- Wymusza
mTLSdla prywatnych lub administracyjnych entrypointów. - Rozdziela publiczne share'y od prywatnych powierzchni logowania.
- Obsługuje także wybrane proxy L4/TCP przez
caddy-l4, np. dla Minecrafta.
To ważne, bo dzięki temu edge nie jest tylko „maszyną z certyfikatem”, ale miejscem, w którym polityka publikacji usług jest jawna i centralna.
Dlaczego bootc + lokalny registry zmieniły operację
Najbardziej wartościowa zmiana w tym projekcie nie dotyczy pojedynczej aplikacji, tylko modelu utrzymania hostów.
Wspólna baza bootc-base-homelab dostarcza baseline systemowy dla większości VM: Podmana, Quadlet, qemu-guest-agent, zaufanie do lokalnego registry, bazowy hardening i konfigurację potrzebną pod Proxmoxa. Potem każdy host jest cienką pochodną z własnym Containerfile, nmconnection i usługami runtime.
Workflow jest powtarzalny:
task build IMAGE=caddy-homelab
task push IMAGE=caddy-homelab
task qcow2 IMAGE=caddy-homelab
albo, jeśli potrzebuję instalatora:
task iso IMAGE=caddy-homelab
Po wdrożeniu VM dalszy lifecycle hosta jest już prosty:
sudo bootc upgrade
sudo systemctl reboot
Jeśli nowy obraz okaże się zły:
sudo bootc rollback
sudo systemctl reboot
To nie eliminuje całej złożoności, ale przenosi ją do miejsca, które łatwiej wersjonować i reviewować: do obrazu i repo.
Lokalny registry i kontrolowany rollout
Lokalny registry nie jest tu dodatkiem, tylko elementem modelu operacyjnego. To z niego hosty i usługi pobierają obrazy, dzięki czemu runtime nie musi zależeć bezpośrednio od publicznych registry.
Warto jednak być precyzyjnym. To nie jest magiczne „air-gap everything”. W praktyce daje mi dwa osobne tryby:
- kontenery aplikacyjne mogą aktualizować się z lokalnego registry bez kontaktu z publicznym upstreamem,
- hosty
bootcmogą robić image-based upgrade z tego samego źródła, ale zmiana warstwy systemowej zwykle oznacza reboot hosta.
To nadal bardzo przydatne, tylko uczciwiej opisywać to jako kontrolowany lokalny rollout niż jako „live update wszystkiego offline”.
Security posture: co poprawia, a czego nie udaje
Najmocniejsze decyzje security w tym projekcie są dość proste:
- publiczny ingress kończy się na jednym edge zamiast na wielu przypadkowych hostach,
- prywatne panele są chronione
mTLS, a nie tylko hasłem, - workloady są rozdzielone per VM zamiast żyć razem na jednym Docker hoście,
- hosty mają wspólny baseline z SELinux i image-based lifecycle,
- lokalny registry wspiera model, w którym backend nie musi ufać całemu Internetowi.
To jednak nadal jest homelab, a nie wzorcowa platforma firmowa. Są tu też świadome albo historyczne kompromisy:
- część dokumentacji repo opisuje starsze iteracje środowiska,
- pełny egress control jest bardziej intencją operacyjną niż kompletnie zakodowanym stanem,
- repo nie pokazuje dojrzałego secret managementu,
- edge i registry są realnymi SPOF-ami,
- część materiałów kryptograficznych i sekretów nie nadaje się do traktowania jako publiczny wzorzec.
Najuczciwiej opisać to tak: architektura podnosi koszt ataku i zmniejsza blast radius, ale nie udaje enterprise-grade security.
Day-2 ops: co jest uporządkowane, a co nadal jest po prostu rozsądnie obsługiwane
Najmocniejszą stroną tej architektury jest rebuild i redeploy hostów. Jeśli host padnie albo rollout okaże się zły, mogę wrócić do ostatniego dobrego obrazu, przywrócić jego tożsamość sieciową i podłączyć trwałe dane.
To nie znaczy jednak, że cały problem day-2 ops jest „rozwiązany przez bootc”. Observability jest dziś głównie hostowo-usługowe: systemd, journald, healthchecki i restart policy usług. Backup i recovery danych nadal wymagają klasycznej dyscypliny dla wolumenów i baz. Rollback hosta nie cofnie zmian stanu aplikacji.
To jest ważna różnica: platforma jest dobrze przygotowana do odtwarzania hostów, ale nie udaje pełnego, enterprise'owego stacku telemetrycznego ani automatycznego DR.
Trade-offy, które były świadome
Ten projekt ma sens właśnie dlatego, że nie próbował rozwiązać wszystkiego naraz.
- VPS jest cienkim publicznym anchorem, a nie miejscem uruchamiania aplikacji.
mTLSzastępuje pełny VPN tam, gdzie ważniejsza była wygoda i prosty dostęp przez przeglądarkę.VM + kontenerzwiększa koszt utrzymania względem jednego hosta, ale istotnie poprawia blast radius.- image-based host lifecycle zmniejsza drift, ale utrudnia szybkie ręczne hotfixy po SSH.
- lokalny registry daje większą kontrolę nad rolloutem, ale sam staje się ważnym komponentem operacyjnym.
To nie jest architektura „najlepsza możliwa”. To jest architektura dobrana pod konkretne constraints: blokady ISP, prywatny compute, duży lokalny storage, wygodny dostęp i przewidywalny rollback.
Co ten projekt mówi o autorze jako inżynierze
Najciekawsze w tym projekcie nie jest to, że używa Proxmoxa, Caddy czy bootc. Ciekawe jest to, że kilka warstw zostało zszytych w jeden model operacyjny:
- publiczny ingress poza domowym ISP,
- prywatny compute i storage u siebie,
- centralny edge z jawną polityką dostępu,
- osobne VM dla usług zamiast jednego współdzielonego hosta,
- hosty aktualizowane obrazami zamiast ręcznie pielęgnowanych snowflake'ów.
Z perspektywy inżynierskiej to właśnie jest sedno. Nie lista technologii, tylko umiejętność dobrania i połączenia narzędzi pod konkretne ograniczenia, bez udawania hyperscalera i bez zacierania kompromisów.
Kod infrastruktury, definicje hostów i przykłady workloadów są w repo: github.com/sieciowiecxyz/bootc-proxmox-homelab-example.