Jak zbudowałem prywatną platformę usługową w domu mimo blokady portów przez ISP
Table of Contents
- Proxmox, ZFS, VyOS, Caddy, WireGuard, VXLAN, bootc i lokalny registry jako jedna spójna architektura
- TL;DR
- 1. Problem, który chciałem rozwiązać
- 2. Czego nie chciałem robić
-
3. Założenia projektowe
- 3.1. Tanio utrzymać compute i storage
- 3.2. Prywatna chmura w domu
- 3.3. Infrastruktura jako kod i możliwość działania offline
- 3.4. Warstwowe bezpieczeństwo: wirtualizacja, konteneryzacja i sieć
- 3.5. Spójny model dostępu: wszędzie HTTPS
- 3.6. Prostota operacyjna i szybkie wdrażanie nowych usług
- 3.7. IaC na tyle uporządkowane, żeby pomagało także AI
- 4. Architektura w jednym zdaniu
- 5. Główne komponenty i ich rola
- 6. Jak działa ruch z Internetu i jak działa ruch w domu
- 7. Jak z LAN dostać się do DMZ i po co mi ip route
- 8. Caddy jako centralny reverse proxy i policy engine
- 9. DMZ, VyOS i firewall: jak ograniczam blast radius
- 10. mTLS jako warstwa redukująca ryzyko, także przy opóźnionych aktualizacjach
- 11. bootc, monorepo, Taskfile i bootable containers
- 12. Lokalny registry i fully offline rollout
- 13. Backup, disaster recovery i storage
- 14. Observability i recovery
- 15. Przykładowe wdrożenie nowej usługi
- 16. Trade-offy
- 17. Najważniejsze wnioski architektoniczne
- 18. Zakończenie
Proxmox, ZFS, VyOS, Caddy, WireGuard, VXLAN, bootc i lokalny registry jako jedna spójna architektura
Ten wpis nie jest o „postawieniu kilku kontenerów w domu”. To case study o tym, jak zaprojektowałem prywatną platformę usługową pod realne ograniczenia: brak klasycznego inboundu z Internetu, chęć trzymania compute i storage u siebie, potrzeba prywatności sieciowej i obliczeniowej, mocna izolacja usług oraz przewidywalny model wdrożeń i rollbacku.
TL;DR
Mój domowy ISP blokował klasyczne otwieranie portów, więc nie mogłem po prostu wystawić usług z własnej sieci. Jednocześnie nie chciałem hostować prywatnych workloadów w public cloud, bo zależało mi na prywatności danych i ruchu, dużym lokalnym storage, tanim compute oraz możliwości utrzymywania środowiska także w modelu offline.
Zaprojektowałem więc architekturę, w której tani VPS z dokupionym floating IP pełni rolę publicznego punktu wejścia i pierwszej warstwy ochrony, ale właściwe usługi działają na domowym sprzęcie w Proxmoxie. Publiczny ingress jest doprowadzany do domu przez WireGuard (L3) i VXLAN (L2), czyli model L2 over L3, ruch kończy się na Caddy VM działającym w domowym DMZ (demilitarized zone, wydzielona sieć dla usług wystawianych lub pośredniczących), a same workloady żyją jako osobne VM bootc, zwykle z dodatkowymi kontenerami Podmana uruchamianymi przez Quadlet.
W praktyce dało mi to kilka rzeczy naraz:
- możliwość publikowania usług mimo ograniczeń ISP,
- tani lokalny compute i storage zamiast płacenia stale za drogi cloud storage,
- prywatny runtime i prywatny storage na własnym sprzęcie,
- warstwową izolację
Internet -> OVH edge/VPS -> Caddy -> VM -> kontener, - publiczny dostęp do wybranych usług przez HTTPS i mTLS bez ciągłego VPN,
- szybki lokalny dostęp do tych samych domen bez niepotrzebnego round-trip przez WAN,
- image-based lifecycle hostów z rollbackiem przez bootc,
- możliwość fully offline rolloutu nowych obrazów hostów i usług z lokalnego registry,
- łatwe dodawanie kolejnych usług do DMZ bez przebudowy całego Internet-facing modelu.
To nie jest miniaturowa kopia hyperscalera. To świadomie pragmatyczna architektura: prosta tam, gdzie prostota zwiększa niezawodność, i bardziej złożona tam, gdzie złożoność rzeczywiście rozwiązuje problem.
1. Problem, który chciałem rozwiązać
Na pierwszy rzut oka problem brzmiał banalnie: chciałem mieć własne usługi działające na domowym sprzęcie. Dopiero po chwili okazało się, że klasyczne rozwiązania średnio tu pasują.
Miałem siedem głównych wymagań:
- Mój ISP blokował normalne wystawianie portów z domu, więc prosty port forwarding odpadał.
- Chciałem trzymać compute i storage lokalnie, bo przy usługach stanowych to właśnie storage bardzo szybko staje się jedną z najdroższych części rachunku chmurowego. Zamiast płacić stale za duże wolumeny block/object storage, postawiłem na własny storage oparty o Proxmox + ZFS, lokalne snapshoty oraz Proxmox Backup Server, a warstwę disaster recovery uzupełniłem o tani, szyfrowany storage w Hetznerze na wypadek utraty całej lokalizacji.
- Zależało mi na prywatności i kontroli nad szyfrowaniem: zarówno dane, jak i wykonywanie obliczeń miały odbywać się na moim sprzęcie, a nie na cudzym VPS. Chciałem też wymusić spójny model szyfrowania ruchu: usługi webowe miały działać przez HTTPS zarówno z Internetu, jak i lokalnie w LAN.
- Architektura miała być bezpieczna nawet wtedy, gdy zapomnę czegoś zaktualizować. Nie chciałem polegać wyłącznie na „zawsze wszystko musi być idealnie załatane”. Dodatkowe warstwy, takie jak mTLS, izolacja VM, firewall i ograniczony egress, miały obniżać ryzyko nawet przy niedoskonałej higienie aktualizacji.
- Dostęp miał być wygodny, bez pamiętania o ciągłym uruchamianiu VPN, szczególnie na telefonie, gdzie pełnotunelowy VPN potrafi niepotrzebnie zjadać baterię.
- Dostęp lokalny miał pozostać szybki, więc nie chciałem, żeby urządzenia w domu dochodziły do moich usług przez publiczny WAN i VPS. Zależało mi na tym, żeby ta sama domena działała zarówno z Internetu, jak i lokalnie, ale w sieci domowej rozwiązywała się bezpośrednio do lokalnych adresów usług.
- Środowisko miało działać jako Infrastructure as Code i dawać się aktualizować offline. To oznaczało potrzebę lokalnego registry, bootable containers i jednolitego modelu deploymentu nowych hostów oraz usług.
To od razu ustawiło kierunek architektury:
- publiczny adres musi istnieć poza domowym ISP,
- VPS ma być tylko cienkim publicznym anchorem, a nie hostem aplikacyjnym,
- właściwe workloady mają siedzieć w domu,
- lokalny registry ma być częścią architektury, a nie dodatkiem,
- całość ma być bezpieczna, ale też przewidywalna operacyjnie.
2. Czego nie chciałem robić
Przeniesienie wszystkiego do chmury
To byłoby najprostsze pod kątem publicznego dostępu, ale kompletnie nie rozwiązywało problemu kosztów długoterminowych, prywatności i kontroli nad środowiskiem. Przy usługach stanowych, większym storage i docelowo także GPU rachunek bardzo szybko przestaje być zabawny.
Jeden duży Docker host z wieloma usługami
To wygodne na start, ale słabe z punktu widzenia blast radius. Jeśli jedna publicznie wystawiona usługa zostaje skompromitowana, atakujący ląduje na hoście, na którym siedzi wszystko inne.
Full VPN jako jedyny sposób dostępu
VPN jest świetnym narzędziem administracyjnym, ale nie chciałem, żeby każda prywatna usługa była dostępna wyłącznie przez „najpierw odpal klienta VPN”. W praktyce oznacza to większy koszt poznawczy, gorszą ergonomię i mniej wygodny dostęp na urządzeniach mobilnych.
Mutable VM zarządzane przez ręczne SSH
To działa, dopóki środowisko jest małe i dopóki pamięta się każdy ręcznie wklejony hotfix. Później zaczyna się klasyczne „czemu ta maszyna różni się od tamtej”, „co dokładnie tam było zmienione” i „jak to odtworzyć po awarii”.
3. Założenia projektowe
3.1. Tanio utrzymać compute i storage
Jednym z głównych założeń było obniżenie kosztu długoterminowego utrzymania usług. Nie chciałem płacić stale za duży compute, drogi storage i transfer w chmurze, skoro mogłem postawić mocniejszy sprzęt u siebie i wykorzystać go lepiej kosztowo.
3.2. Prywatna chmura w domu
Prywatne usługi, dane i compute miały pozostać na moim sprzęcie. Chodziło zarówno o prywatność danych, jak i prywatność samego ruchu sieciowego oraz wykonywania obliczeń. VPS miał być tylko cienką warstwą ekspozycji do Internetu.
3.3. Infrastruktura jako kod i możliwość działania offline
Nie chciałem środowiska opartego na ręcznym klikaniu i poprawkach przez SSH. Zależało mi na modelu, w którym hosty i usługi są definiowane jako artefakty, można je odtwarzać przewidywalnie, a aktualizacje da się przygotowywać i przeprowadzać także w modelu offline.
3.4. Warstwowe bezpieczeństwo: wirtualizacja, konteneryzacja i sieć
Bezpieczeństwo miało wynikać z kilku warstw naraz, a nie z jednej granicy. Każda usługa działa w osobnej VM, a wewnątrz VM zwykle dodatkowo w kontenerach. Do tego dochodzi separacja sieciowa, firewall, lokalny registry i centralny edge kontrolujący publikację usług.
3.5. Spójny model dostępu: wszędzie HTTPS
Jednym z twardych założeń było to, że usługi webowe mają być dostępne wyłącznie przez HTTPS — zarówno z Internetu, jak i lokalnie w domu. Ta sama domena miała działać w obu światach, ale zawsze w modelu TLS.
3.6. Prostota operacyjna i szybkie wdrażanie nowych usług
To środowisko miało być nie tylko bezpieczne, ale też praktyczne. Chciałem móc wdrożyć nową usługę — choćby serwer Minecraft dla znajomych — bez wielodniowego dłubania, ręcznego konfigurowania każdej warstwy i chaosu w utrzymaniu.
3.7. IaC na tyle uporządkowane, żeby pomagało także AI
Środowisko miało być na tyle wystandaryzowane, żeby nowy deployment dało się przygotować szybko również półautomatycznie, także z pomocą agentów AI generujących lub modyfikujących definicje deploymentu. Benefit jest prosty: uporządkowane IaC przyspiesza prototypowanie nowych usług.
4. Architektura w jednym zdaniu
Kupiłem tani VPS z dokupionym floating IP, a następnie „przeniosłem” ten publiczny adres do domu przez overlay zbudowany jako VXLAN (L2) nad WireGuardem (L3), tak żeby publiczny ingress kończył się nie na VPS, tylko na Caddy VM w moim Proxmoxie.
To jest sedno całego projektu.
5. Główne komponenty i ich rola
5.1. Floating IP
Floating IP jest stabilnym publicznym endpointem dla domen i ruchu TCP/HTTPS. Dzięki niemu nie jestem przywiązany do ograniczeń domowego ISP, a publiczne DNS zawsze wskazują na jeden przewidywalny adres.
5.2. VPS
VPS nie hostuje właściwych aplikacji. Jest cienkim publicznym anchorem, który:
- posiada publiczny endpoint,
- zestawia tunel do domu,
- stanowi pierwszą warstwę ochrony przed ruchem z Internetu,
- daje mi też praktyczną warstwę „taniego bufora” przed bardziej prymitywnymi atakami wolumetrycznymi.
Dodatkowo po stronie OVH mam edge firewall i ochronę sieciową operatora. To nie jest magiczna tarcza nieśmiertelności, ale w praktyce oznacza, że losowy Internet najpierw rozbija się o infrastrukturę dostawcy i VPS, a nie od razu o mój Proxmox w domu. To podnosi koszt ataku na homelab i daje mi sensowną pierwszą linię ochrony.
5.3. WireGuard jako transport L3
WireGuard daje prosty, bezpieczny tunel warstwy 3 między VPS a domowym edge.
5.4. VXLAN jako overlay L2
VXLAN buduje nad WireGuardem warstwę 2. Dzięki temu floating IP może zostać logicznie zakończony po stronie domu, na interfejsie vxlan100 Caddy VM.
5.5. Caddy VM
Caddy VM to centralny reverse proxy i policy engine tej architektury. Ma kilka interfejsów i kilka odpowiedzialności naraz:
- interfejs WireGuard,
- interfejs VXLAN z publicznym floating IP,
- interfejs w DMZ,
- interfejs w LAN.
To właśnie Caddy:
- terminates TLS,
- wymusza mTLS dla prywatnych powierzchni,
- reverse proxy'uje ruch HTTP/HTTPS do backendów,
- dzięki xcaddy i pluginowi caddy-l4 potrafi też proxy'ować wybrane usługi L4/TCP, np. Minecraft, a więc nie tylko porty 80 i 443.
5.6. VyOS i SDN w Proxmoxie
VyOS w moim Proxmoxie definiuje warstwę SDN dla izolowanej sieci usługowej. To on pełni rolę bramy dla DMZ, dostarcza DHCP oraz statyczne przypisania adresów IP tam, gdzie chcę mieć bardziej przewidywalny układ.
5.7. DMZ
DMZ to skrót od demilitarized zone. W praktyce chodzi o wydzieloną sieć pośrednią: nie tak zaufaną jak domowy LAN, ale też nie wystawioną na chaos świata zewnętrznego. U mnie DMZ 10.20.0.0/24 jest główną siecią usługową dla VM serwisowych.
5.8. Lokalny registry
Registry jest centralnym magazynem obrazów hostów i usług. To dzięki niemu mogę robić rollouty i rollbacki bez zależności od publicznego Internetu. To też jeden z filarów modelu offline.
5.9. bootc i bootable containers
Hosty VM buduję jako bootable containers. Są one kompilowane w moim monorepo przez Taskfile, publikowane do lokalnego registry, a następnie uruchamiane lub aktualizowane jako spójne artefakty. To nie są „VM klejone ręcznie”, tylko hosty traktowane jak buildowane obrazy.
Diagram 1: pełna architektura logiczna
flowchart LR
%% =========================
%% WAN / Internet
%% =========================
subgraph WAN["WAN / Internet"]
direction TB
U["1. User / Client<br/><b>HTTP request starts here</b>"]
DNS["2. Public DNS records<br/>blog.domain.com → 42.42.42.42<br/>cloud.domain.com → 42.42.42.42<br/>proxmox.domain.com → 42.42.42.42<br/>mc.domain.com → 42.42.42.42"]
VPS["3. VPS / OVH<br/>main IP: 12.12.12.12<br/>floating IP: 42.42.42.42<br/>edge firewall / first public buffer"]
WG_VPS["4. WireGuard L3<br/>wg0: 10.255.255.1/30"]
U --> DNS --> VPS --> WG_VPS
end
%% =========================
%% Local home network
%% =========================
subgraph LOCAL["Local home network"]
direction LR
subgraph PVE_NODE["Proxmox node"]
direction LR
subgraph DMZ["DMZ 10.20.0.0/24"]
direction LR
REG["Registry VM (bootc)<br/>10.20.0.50<br/>Storage for local bootc container images<br/>offline updates and rollbacks"]
subgraph CADDYVM["5. Caddy VM (bootc)"]
direction TB
WG_HOME["wg0<br/>10.255.255.2/30"]
VX["vxlan100<br/>L2 over WireGuard"]
FIP_LOCAL["floating IP lands here<br/>42.42.42.42/32<br/>bound on vxlan100"]
CADDY["6. Caddy VM (bootc)<br/>10.20.0.10<br/>TLS + mTLS<br/>reverse proxy + L4 + DNS challenge"]
WG_HOME --> VX --> FIP_LOCAL --> CADDY
end
subgraph SVCS["7. Isolated services"]
direction TB
NEXT["Nextcloud VM (bootc)<br/>10.20.0.30<br/>Podman container"]
BLOG["Blog VM (bootc)<br/>10.20.0.90<br/>Podman container<br/>blog.domain.com"]
OTHER["Other services VM (bootc)<br/>10.20.0.40<br/>Podman / Quadlet<br/>services.domain.com"]
MC["Minecraft VM (bootc)<br/>10.20.0.60<br/>TCP via L4<br/>mc.domain.com"]
end
end
subgraph MGMT["LAN ↔ DMZ routing"]
direction TB
VYOS["VyOS VM<br/>LAN: 192.168.1.69/24<br/>DMZ GW: 10.20.0.1/24<br/>bridges LAN to DMZ<br/>DHCP + static leases"]
end
end
PC["Admin PC / Laptop<br/>192.168.1.x/24<br/><br/>ip route add 10.20.0.0/24 via 192.168.1.69<br/>to reach DMZ services<br/><br/>Monorepo + Taskfile<br/>build / push images"]
end
%% Public ingress from WAN into home infra
WG_VPS --> WG_HOME
%% Caddy to services
CADDY --> NEXT
CADDY --> BLOG
CADDY --> OTHER
CADDY --> MC
%% LAN / DMZ management path
PC --> VYOS
VYOS --> REG
VYOS --> CADDY
VYOS --> NEXT
VYOS --> BLOG
VYOS --> OTHER
VYOS --> MC
%% Build / rollout path from Admin PC
PC -. build / push .-> REG
REG -. pull bootc images .-> CADDY
REG -. pull bootc images .-> NEXT
REG -. pull bootc images .-> BLOG
REG -. pull bootc images .-> OTHER
REG -. pull bootc images .-> MC
%% Colors
classDef wan fill:#dbeafe,stroke:#1d4ed8,color:#111827,stroke-width:2px;
classDef local fill:#fde68a,stroke:#d97706,color:#111827,stroke-width:2px;
classDef proxmox fill:#fed7aa,stroke:#ea580c,color:#111827,stroke-width:2px;
classDef net fill:#dbeafe,stroke:#2563eb,color:#111827,stroke-width:2px;
classDef caddy fill:#dcfce7,stroke:#16a34a,color:#111827,stroke-width:2px;
classDef svc fill:#f3e8ff,stroke:#9333ea,color:#111827,stroke-width:2px;
classDef vyos fill:#dbe4f0,stroke:#475569,color:#111827,stroke-width:2px;
classDef registry fill:#e5e7eb,stroke:#6b7280,color:#111827,stroke-width:2px;
class U,DNS,VPS,WG_VPS wan;
class PC local;
class PVE_NODE proxmox;
class WG_HOME,VX,FIP_LOCAL net;
class CADDY caddy;
class NEXT,BLOG,OTHER,MC svc;
class VYOS vyos;
class REG registry;
6. Jak działa ruch z Internetu i jak działa ruch w domu
To jest najważniejsza część całej architektury. Bez niej całość brzmi jak „jest jakiś tunel i jakiś reverse proxy”.
6.1. Gdy jestem poza domem, w WAN / Internecie
Przepływ dla usługi HTTPS wygląda tak:
- użytkownik trafia na domenę, np.
cloud.example.com, - publiczny DNS rozwiązuje domenę do floating IP,
- floating IP jest osiągalny przez VPS,
- VPS przekazuje ruch tunelem WireGuard,
- nad WireGuardem działa VXLAN, więc publiczny endpoint jest logicznie obecny na Caddy VM,
- Caddy kończy TLS,
- jeśli dana ścieżka jest prywatna, Caddy wymusza mTLS,
- Caddy reverse proxy'uje ruch do backendu w DMZ albo do wybranego hosta w LAN.
Dla usług nie-HTTP, np. Minecrafta, ścieżka jest podobna, ale zamiast reverse proxy HTTP używany jest L4 proxy przez caddy-l4.
6.2. Gdy jestem w domu, w LAN
Nie chciałem, żeby laptop stojący obok racka dochodził do usługi przez wycieczkę: dom -> Internet -> VPS -> tunel -> dom. To byłoby absurdalne i zwiększałoby latencję.
Dlatego w LAN:
- ta sama domena jest rozwiązywana lokalnie do lokalnego adresu,
- certyfikat nadal jest poprawny, bo używam ACME DNS-01 challenge,
- ruch idzie lokalnie i nadal po HTTPS,
- nie ma niepotrzebnego round-trip przez VPS i WAN.
Innymi słowy: ta sama domena, ten sam TLS, inna lokalna ścieżka routingu.
Diagram 2: ta sama domena, dwie ścieżki dostępu
flowchart LR
subgraph WAN[Użytkownik poza domem]
U1[Klient] --> DNS1[Public DNS]
DNS1 --> FIP1[Floating IP]
FIP1 --> VPS1[VPS]
VPS1 --> WG1[WireGuard]
WG1 --> VX1[VXLAN]
VX1 --> C1[Caddy]
C1 --> B1[Backend]
end
subgraph LANPATH[Użytkownik w domu]
U2[Klient w LAN] --> LDNS[Lokalny DNS]
LDNS --> C2[Caddy lub lokalny backend]
C2 --> B2[Backend]
end
7. Jak z LAN dostać się do DMZ i po co mi ip route
DMZ jest oddzielną siecią usługową, więc komputer w zwykłym LAN nie musi domyślnie wiedzieć, jak do niej dojść. Jeśli chcę z lokalnego komputera pingować albo administracyjnie dochodzić do hostów w 10.20.0.0/24, mogę dodać statyczną trasę przez mój lokalny next-hop do DMZ.
Na mojej stacji roboczej działa to tak:
sudo ip route add 10.20.0.0/24 via 192.168.1.69
To mówi lokalnemu komputerowi: „jeśli chcesz dojść do DMZ 10.20.0.0/24, wyślij pakiety do hosta 192.168.1.69, który ma trasę dalej do VyOS i sieci usługowej”. Po tym mogę:
- pingować hosty w DMZ,
- robić SSH do usług administracyjnych,
- diagnozować routing i connectivity bez obchodzenia tego przez Internet.
Jeżeli chcę, żeby to było trwałe, robię to już nie jednorazowym ip route, tylko w konfiguracji mojego routera LAN albo systemowym network managerze.
Diagram 3: routing z komputera w LAN do DMZ
flowchart LR
PC[Komputer w LAN 192.168.1.x] -->|ip route via 192.168.1.69| NextHop[Host LAN z trasą do DMZ]
NextHop --> VyDMZ[VyOS DMZ gateway 10.20.0.1]
VyDMZ --> Host[VM w DMZ 10.20.0.x]
8. Caddy jako centralny reverse proxy i policy engine
Samo stwierdzenie „mam reverse proxy” nic nie znaczy. Tu chodzi o to, że Caddy jest centralnym punktem kontroli polityki dostępu.
8.1. Co robi Caddy
Caddy:
- terminates TLS,
- reverse proxy'uje usługi HTTP/HTTPS,
- wymusza mTLS na prywatnych powierzchniach,
- rozdziela publiczne i prywatne entrypointy,
- dzięki xcaddy i caddy-l4 obsługuje także wybrane usługi TCP.
To oznacza, że zarówno aplikacja webowa, jak i np. Minecraft mogą korzystać z tego samego modelu publicznej ekspozycji.
8.2. Przykład: publiczna strona i prywatna usługa
Poniżej uproszczony przykład idei konfiguracji.
sieciowiec.xyz {
encode gzip zstd
reverse_proxy 10.20.0.30:8080
}
cloud.example.com {
tls {
client_auth {
mode require_and_verify
trusted_ca_cert_file /etc/caddy/mtls/ca.crt
}
}
reverse_proxy 10.20.0.40:8080
}
Pierwsza domena jest zwykłą publiczną usługą HTTPS. Druga domena wymaga certyfikatu klienta i dopiero wtedy wpuszcza ruch dalej do backendu.
8.3. Przykład: Minecraft przez L4
Dla Minecrafta model jest podobny, ale dotyczy warstwy TCP. W praktyce używam Caddy zbudowanego przez xcaddy z pluginem caddy-l4, żeby móc proxy'ować ruch inny niż standardowe HTTP/S.
To pozwala wystawić port gry bez stawiania osobnego publicznego hosta lub osobnego zewnętrznego tunelu tylko dla tej jednej usługi.
Diagram 4: Caddy jako punkt decyzyjny
flowchart TB
In[Przychodzący ruch] --> CaddyEdge[Caddy VM]
CaddyEdge -->|HTTPS public| Web[Publiczna usługa web]
CaddyEdge -->|HTTPS + mTLS| Private[Prywatny panel / admin]
CaddyEdge -->|TCP przez caddy-l4| Game[Minecraft]
CaddyEdge -->|HTTPS do LAN| PVEUI[Proxmox UI lub inny host LAN]
9. DMZ, VyOS i firewall: jak ograniczam blast radius
9.1. DMZ jako główna sieć usługowa
Główna sieć usługowa to DMZ 10.20.0.0/24. Jej bramą jest VyOS 10.20.0.1, a większość usług dostaje w niej własny adres IP. Ten segment jest celowo prosty i płaski — bardziej zależało mi na czytelności i przewidywalności niż na udawaniu enterprise’owej mikrosegmentacji dla jednej osoby.
9.2. VyOS jako gateway i SDN glue
VyOS spina tę architekturę od strony routingu i adresacji. To on:
- wystawia bramę dla DMZ,
- dostarcza DHCP,
- trzyma statyczne przypisania adresów tam, gdzie chcę przewidywalnego IP,
- współpracuje z Proxmoxowym SDN.
9.3. Firewall Proxmoxa i ograniczony egress
To jeden z ważniejszych elementów bezpieczeństwa. Firewall Proxmoxa blokuje:
- niepotrzebny dostęp usług do Internetu,
- niepotrzebny ruch między serwisami,
- przypadkowe east-west movement, jeśli usługa nie musi widzieć innych hostów.
To ma bardzo praktyczny efekt: jeśli atakujący uzyska RCE (remote code execution) w aplikacji, to nie ląduje od razu w świecie pełnej swobody. Host może nie mieć dostępu do Internetu, może nie widzieć innych serwisów i może być zamknięty tylko do kilku koniecznych zależności.
To nie czyni RCE „niemożliwym”, ale znacznie utrudnia jego operacjonalizację. Bez egressu dużo trudniej:
- pobrać dodatkowe payloady,
- wyprowadzić dane na zewnątrz,
- zestawić reverse shell,
- skanować szerzej środowisko i poruszać się bocznie.
Właśnie dlatego ograniczony egress jest dla mnie jedną z niedocenianych warstw obrony.
Diagram 5: co widzi atakujący po RCE
flowchart TB
Internet2[Internet] --> Caddy2[Caddy]
Caddy2 --> VM1[VM usługi]
VM1 --> CTR[Kontener aplikacji]
CTR -. próba wyjścia do Internetu .-> BlockEgress[Firewall: egress deny]
CTR -. próba ruchu do innych VM .-> BlockEastWest[Firewall: deny east-west]
CTR -. tylko dozwolone zależności .-> AllowDeps[DNS / NTP / Registry lub nic]
10. mTLS jako warstwa redukująca ryzyko, także przy opóźnionych aktualizacjach
Jedną z ważnych lekcji z prawdziwego życia jest to, że nie wszystko zawsze jest zaktualizowane idealnie w tej samej sekundzie. Dlatego nie chciałem polegać wyłącznie na tym, że „łatki załatwią wszystko”.
Dla prywatnych powierzchni administracyjnych używam mTLS. To oznacza, że sam fakt trafienia na domenę i posiadania hasła nie wystarcza. Trzeba jeszcze posiadać ważny certyfikat klienta.
Praktyczny benefit jest prosty: jeśli nawet jakiś serwis nie został jeszcze zaktualizowany, to jego powierzchnia ataku nadal jest ograniczona, bo prywatny interfejs nie jest wystawiony jako zwykły „login page dla całego Internetu”.
To nie zastępuje aktualizacji. To daje dodatkowy margines bezpieczeństwa.
11. bootc, monorepo, Taskfile i bootable containers
11.1. Co rozumiem przez bootable containers
Hosty VM buduję jako bootable containers. Oznacza to, że bazowy system operacyjny hosta jest opisany i budowany jak obraz, a nie składany ręcznie po bootstrappingu.
11.2. Monorepo jako source of truth
Definicje hostów, usług i związanych elementów IaC trzymam w monorepo. Build orchestration robi Taskfile, który porządkuje proces budowy i publikacji artefaktów.
Kod tej infrastruktury, przykłady dodawania nowych serwisów oraz monorepo z definicjami IaC znajdują się tutaj: github.com/sieciowiecxyz/bootc-proxmox-homelab-example.
To repo pełni rolę praktycznego uzupełnienia tego wpisu: artykuł wyjaśnia architekturę i decyzje projektowe, a repo pokazuje, jak te założenia są odwzorowane w kodzie i w build pipeline opartym o Taskfile.
11.3. Co trafia do obrazu hosta
Wspólna baza hosta dostarcza m.in.:
- baseline systemowy,
- Podmana i Quadlet,
- zaufanie do lokalnego registry,
- konfiguracje potrzebne do działania w Proxmoxie,
- wybrane elementy hardeningu.
Diagram 6: skąd bierze się VM host
flowchart LR
Repo[Monorepo] --> Task[Taskfile]
Task --> Build[Build bootable container]
Build --> Reg[Local Registry]
Reg --> VM[bootc VM host]
VM --> App[Podman / Quadlet service]
12. Lokalny registry i fully offline rollout
Lokalny registry to nie jest dodatek. To centralny element całego modelu deploymentu.
12.1. Co daje registry
Registry:
- przechowuje obrazy hostów i usług,
- pozwala robić rollout i rollback bez zależności od publicznych registry,
- umożliwia utrzymywanie backendów bez pełnego Internetu,
- porządkuje supply chain.
12.2. Jak wygląda offline update path
Model jest prosty:
- nowy obraz hosta lub aplikacji trafia do lokalnego registry,
- VM usługi widzi tylko registry, a nie cały Internet,
- usługa lub host może okresowo pobrać nowy obraz z registry,
- rollout odbywa się lokalnie, bez wyjścia do publicznej sieci.
To jest właśnie ten sens „fully offline rolloutu”: host albo usługa mogą aktualizować się z lokalnego, kontrolowanego źródła artefaktów, nawet jeśli same są praktycznie air-gapped od Internetu.
12.3. Ważne doprecyzowanie: reboot czy bez rebootu
Tu trzeba być precyzyjnym, bo technologia lubi robić figle semantyczne.
- Aktualizacja usług kontenerowych z lokalnego registry może odbywać się bez kontaktu z Internetem i bez rebootu kernela hosta.
- Aktualizacja hosta przez bootc daje image-based upgrade/rollback, ale zmiana hostowego systemu zwykle wiąże się z rebootem hosta, a nie magicznym „live reload wszystkiego”.
To rozróżnienie jest ważne, bo dzięki niemu tekst pozostaje technicznie uczciwy.
Diagram 7: offline rollout
sequenceDiagram
participant Repo as Monorepo
participant Task as Taskfile
participant Reg as Local Registry
participant VM as VM service host
participant App as Service container
Repo->>Task: build nowego obrazu
Task->>Reg: push obrazu
VM->>Reg: periodical pull
Reg-->>VM: nowy obraz hosta lub usługi
VM->>App: restart / rollout usługi
13. Backup, disaster recovery i storage
13.1. Dlaczego storage był jednym z głównych powodów budowy tej architektury
Wiele osób patrzy na cloud głównie przez pryzmat vCPU i RAM, a to storage bardzo często okazuje się najdroższym, najbardziej lepkim elementem całego rachunku. Przy większej liczbie usług stanowych i archiwów danych płacenie stale za rosnące wolumeny storage staje się po prostu słabym dealem.
13.2. Lokalny model storage
Mój podstawowy model storage to:
- Proxmox + ZFS,
- lokalne snapshoty,
- Proxmox Backup Server,
- offsite copy na tani, szyfrowany storage w Hetznerze.
13.3. Disaster recovery
To oznacza, że awaria pojedynczej VM to jedno, ale utrata całej lokalizacji to drugie. Dlatego lokalny backup nie wystarczał. Offsite encrypted storage był potrzebny po to, żeby nie skończyć z elegancko zbackupowanym zerem po pożarze, zalaniu albo kradzieży całego środowiska.
14. Observability i recovery
14.1. Observability
Podstawową warstwą operacyjną są:
systemd,journald,- logi aplikacji,
- healthchecki,
- restart polityk usług.
To nie jest jeszcze pełny, scentralizowany stack telemetryczny, ale dla jednoosobowej platformy daje sensowny poziom operacyjnej kontroli.
14.2. Recovery hosta
W przypadku awarii hosta ścieżka odtworzenia jest czytelna:
- postaw ostatni dobry obraz hosta,
- przywróć tożsamość sieciową,
- podłącz albo odtwórz trwałe dane,
- uruchom usługę.
14.3. Ograniczenia rollbacku
Rollback hosta nie cofa zmian w bazie danych i wolumenach aplikacyjnych. Bootc świetnie rozwiązuje klasę problemów infrastrukturalnych, ale nie zastępuje backupu danych.
15. Przykładowe wdrożenie nowej usługi
W praktyce dodanie nowej usługi wygląda u mnie mniej więcej tak:
- przygotowuję definicję hosta i usługi w monorepo,
- Taskfile buduje odpowiedni artefakt,
- obraz trafia do lokalnego registry,
- uruchamiam nową VM w DMZ,
- VyOS i DHCP/static lease dają jej przewidywalny adres,
- Caddy dostaje nową regułę publikacji,
- firewall dostaje tylko te wyjątki, które są rzeczywiście potrzebne.
To właśnie dlatego taka architektura jest „build to last”: nowa usługa nie wymaga nowego pomysłu na Internet, tylko wpisuje się w istniejący model.
16. Trade-offy
16.1. Prostota sieci kontra mocniejsza mikrosegmentacja
Wybrałem prosty model jednej głównej sieci usługowej zamiast budowania osobnych segmentów dla każdej aplikacji.
Zysk: mniejszy koszt poznawczy i prostsza operacja.
Koszt: większa odpowiedzialność spoczywa na granicach VM i firewallu.
16.2. Image-based infra kontra SSH hotfixing
Zysk: przewidywalność, powtarzalność, rollback.
Koszt: część zmian wymaga przebudowy obrazu, a nie tylko szybkiej korekty na żywym hoście.
16.3. mTLS zamiast full VPN
Zysk: dużo lepsza ergonomia dostępu, szczególnie na mobile.
Koszt: trzeba zarządzać certyfikatami klienta i świadomie pilnować urządzeń końcowych.
16.4. VM + kontener kontra niższy narzut zasobów
Zysk: lepsza izolacja i mniejszy blast radius.
Koszt: więcej maszyn i więcej elementów do utrzymania.
16.5. Lokalny registry kontra większa liczba zależności własnych
Zysk: większa kontrola nad rolloutem i mniejsza zależność od Internetu.
Koszt: registry staje się ważnym komponentem operacyjnym i potencjalnym SPOF-em.
17. Najważniejsze wnioski architektoniczne
Najcenniejsza lekcja z tego projektu nie dotyczy pojedynczej technologii, tylko sposobu łączenia ich w jedną spójną całość. Floating IP, VPS, WireGuard, VXLAN, Caddy, VyOS, bootc i lokalny registry nie są tu osobnymi zabawkami, lecz elementami jednego modelu: prywatnej platformy usługowej, która ma być jednocześnie dostępna, przewidywalna operacyjnie i trudniejsza do naruszenia.
Z perspektywy architektury najważniejsze okazały się cztery rzeczy:
- publiczny ingress oddzielony od właściwego compute,
- warstwowa izolacja oparta o VM, kontenery i separację sieciową,
- spójny model publikacji usług przez centralny edge,
- host lifecycle i rollouty oparte o obrazy, a nie ręczne zmiany na żywym systemie.
To właśnie te decyzje sprawiają, że środowisko pozostaje rozwijalne. Nowa usługa nie wymaga nowego pomysłu na Internet, nowego sposobu zarządzania hostem ani nowego modelu bezpieczeństwa. Wpisuje się w istniejący układ.
18. Zakończenie
Ten projekt zaczął się od prostego problemu: chciałem mieć prywatne usługi na własnym sprzęcie, mimo że mój ISP nie pozwalał mi po prostu otworzyć portów i wystawić ich do Internetu.
Mogłem wrzucić wszystko do chmury albo skleić jeden wielki host z kontenerami. Zamiast tego zbudowałem architekturę, w której:
- VPS z floating IP daje publiczny punkt wejścia i pierwszą warstwę ochrony,
- publiczny ingress jest przenoszony do domu przez VXLAN nad WireGuardem,
- Caddy VM jest centralnym reverse proxy i policy engine,
- DMZ w Proxmoxie jest główną siecią usługową,
- VyOS definiuje SDN, routing i adresację,
- firewall ogranicza Internet i ruch boczny,
- mTLS zmniejsza ryzyko dla prywatnych powierzchni,
- bootc, monorepo, Taskfile i lokalny registry pozwalają robić spójne rollouty, również offline.
To nie jest architektura „najbardziej efektowna”. To jest architektura, która rozwiązuje konkretny problem w sposób przewidywalny, bezpieczny i rozsądny operacyjnie.