Homelab od zera, cz. 3: Tailscale i reverse proxy — cały homelab spoza domu, bez otwierania portów
Tailscale (VPN na WireGuard) jako subnet router z DNS AdGuarda, do tego Nginx Proxy Manager jako reverse proxy. Dostęp do całej sieci domowej z dowolnego miejsca, blokowanie reklam poza domem i ładne nazwy zamiast portów — a wszystko bez wystawiania czegokolwiek na świat.
Read in EnglishW części 1 postawiliśmy Ubuntu Server (192.168.0.200), w części 2 AdGuard Home dla całego domu. Wszystko działa super — w domu. Poza domem Portainer i AdGuard są nieosiągalne, a reklamy wracają, bo telefon pyta już inny DNS.
No i część 3 nastała wielkimi krokami. Dziś sprawimy, że w każdym miejscu na świecie będziemy mieć dostęp do naszej sieci i wytniemy niechciany ruch — nawet sącząc drinka w gorącej Hiszpanii. A całkiem serio: AdGuard to tylko wisienka na torcie. Skoro mamy dostęp do całej sieci — routera, usług, wszystkiego, co postawimy — to czemu by z tego nie skorzystać?
Można by otworzyć porty na routerze i wystawić usługi do internetu. Nie róbcie tego. Każdy otwarty port to zaproszenie: w kilka minut zaczynają pukać boty, skany i próby logowania. Zasada blue teamu jest prosta — nie wystawiamy na świat niczego, czego nie musimy.
Zamiast tego zrobimy dwie rzeczy:
- Tailscale — prywatny, szyfrowany tunel (VPN na WireGuard), dzięki któremu telefon trafia do domowej sieci z dowolnego miejsca. Bez jednego otwartego portu na routerze.
- Nginx Proxy Manager — reverse proxy, żeby zamiast pamiętać
:9443i:3000wchodzić na ładneportainer.madejczyadguard.babuszka.
Czym jest Tailscale
Tailscale to nakładka na WireGuard, która buduje prywatną sieć (tzw. tailnet) między waszymi urządzeniami. Każde dostaje własny adres 100.x.y.z i łączy się z pozostałymi bezpośrednio, szyfrowanym tunelem. Nie przekierowujecie żadnych portów, nie wystawiacie niczego publicznie — urządzenia uwierzytelniają się kontem i widzą tylko siebie nawzajem.
Krok 1: Tailscale na serwerze
Łączymy się z serwerem i instalujemy oficjalnym skryptem (to znów curl | sh, czyli uruchamiacie cudzy skrypt jako root — jak w części 2, zaufajcie źródłu albo zajrzyjcie do środka przed uruchomieniem):
curl -fsSL https://tailscale.com/install.sh | sh
Potem podnosimy interfejs i zaczynamy zabawę:
sudo tailscale up
W terminalu pojawi się link — otwórzcie go w przeglądarce i zalogujcie się (Google, GitHub albo e-mail). Tu zakładamy nową sieć. Serwer od razu wskoczy do admin console na login.tailscale.com. Sprawdźcie swój adres z tailnetu:
tailscale ip -4
To wasze 100.x — po nim dostaniecie się do serwera z każdego innego urządzenia w tailnecie. Zapiszcie go, bo przyda się przy DNS.
W admin console serwer od razu widać jako podłączone urządzenie ze statusem Connected.
Protip: wyłączcie wygasanie klucza
Domyślnie klucz urządzenia wygasa co jakiś czas i trzeba logować się od nowa. Dla serwera, który ma chodzić 24/7, to kłopot — pewnego dnia tunel po prostu padnie. W admin console wejdźcie w serwer → Disable key expiry. Dla stałych maszyn to must-have.
Krok 2: Tailscale na telefonie
Instalujecie apkę Tailscale (Android/iOS), logujecie się tym samym kontem i włączacie przełącznik. Telefon jest już w tailnecie i widzi serwer po jego 100.x — nawet przez LTE, po drugiej stronie kraju.
Na razie widać jednak tylko serwer. Chcemy widzieć cały dom i mieć DNS AdGuarda. To dwa kolejne kroki.
Krok 3: Subnet router — cała sieć domowa pod ręką
Żeby z telefonu dostać się nie tylko do serwera, ale też do routera, kamer czy innych urządzeń, serwer musi rozgłaszać całą podsieć domową. Najpierw włączamy przekazywanie pakietów — dzięki temu serwer zaczyna przepuszczać przez siebie ruch do reszty sieci, czyli staje się bramką (subnet routerem):
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
Teraz rozgłaszamy podsieć (to ta sama /24, którą liczyliśmy w części 1):
sudo tailscale up --advertise-routes=192.168.0.0/24
Tailscale może przy okazji rzucić ostrzeżeniem, że karta sieciowa ma nieoptymalne ustawienie UDP GRO — tunel mógłby pompować UDP szybciej. To nie błąd, w homelabie można spokojnie zignorować. Jak chcecie wycisnąć maksimum, włączcie GRO małym skryptem, który odpali się przy każdym starcie interfejsu (podmieńcie enp0s3 na swoją kartę z ip a):
printf '#!/bin/sh\n\nethtool -K enp0s3 rx-udp-gro-forwarding on rx-gro-list off\n' | \
sudo tee /etc/networkd-dispatcher/routable.d/50-tailscale
sudo chmod 755 /etc/networkd-dispatcher/routable.d/50-tailscale
sudo systemctl restart networkd-dispatcher
W admin console przy serwerze pojawi się oczekująca trasa — wejdźcie w Edit route settings i ją zatwierdźcie (zaznaczcie 192.168.0.0/24). Na telefonie w ustawieniach Tailscale włączcie „Use subnet routes”. Od tej chwili spoza domu wchodzicie na 192.168.0.1 (panel routera) czy dowolne inne urządzenie, jakbyście siedzieli w salonie. Do tego jeszcze wrócimy na końcu, bo to łatwo przeoczyć.
Krok 4: DNS AdGuarda nawet poza domem
To obietnica z końca części 2. Chcemy, żeby telefon poza domem dalej pytał nasz AdGuard, więc reklamy i phishing lecą w kosz wszędzie.
W admin console wchodzimy w DNS → Nameservers → Add nameserver. Gotowce na liście (Google, Cloudflare, Quad9) nas nie interesują — to publiczne DNS-y, a my chcemy wskazać własnego AdGuarda. Wybieramy więc Custom…
W pole wpisujemy adres 100.x serwera (ten z tailscale ip -4, u mnie 100.70.115.78) i włączamy Override local DNS.
Gotowe — gdy Tailscale jest aktywny, wszystkie zapytania DNS telefonu idą do AdGuarda przez szyfrowany tunel.
Używamy adresu
100.x, a nie192.168.0.200, bo działa niezależnie od tras subnetu i jest pewniejszy, gdy jesteście poza domem.
Krok 5: Nginx Proxy Manager — koniec z pamiętaniem portów
Mamy już dostęp do wszystkiego, ale adresy typu 192.168.0.200:9443 są brzydkie i łatwo je pomylić. Reverse proxy to taki strażnik: przyjmuje żądanie, patrzy na nazwę (np. portainer.madej) i przekazuje je do właściwej usługi. Jeden punkt wejścia, wiele usług.
Użyjemy Nginx Proxy Managera — to nginx z panelem w przeglądarce, więc hosty dodajecie klikając, bez ręcznego pisania configów. (Jest jeszcze Caddy, ale ja osobiście wolę nginx.)
NPM chce portów 80 i 443. W Portainerze dodajemy nowy stack (pamiętajcie, żeby przypiąć wersję zamiast latest):
services:
npm:
image: jc21/nginx-proxy-manager:2.11.3
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
Wpadka: port 80 zajęty
Klikam Deploy i… deploy nie wchodzi. Wiecie dlaczego? AdGuard zajął nam port 80 i NPM nie ma się gdzie podpiąć:
Spokojnie, ogarniemy. Lepiej, żeby to NPM sterował ruchem wchodzącym na port 80 (to główny port HTTP), a AdGuardowi 80 do niczego nie jest potrzebny — do DNS używa 53. Przenosimy więc panel AdGuarda na 3000.
AdGuard jest w trybie network_mode: host, więc port panelu siedzi w jego własnym configu, nie w compose. Najpierw go znajdujemy:
sudo find / -name AdGuardHome.yaml 2>/dev/null
Otwieramy go w nano i na samej górze, w sekcji http:, zmieniamy port w address z 0.0.0.0:80 na 0.0.0.0:3000 (Ctrl+S zapisuje, Ctrl+X wychodzi):
AdGuard czyta config przy starcie, więc restartujemy kontener, żeby zmiana weszła:
docker restart adguardhome
Sprawdzamy, czy AdGuard żyje na nowym porcie — i owszem, żyje, ale się napracował: 23 809 zapytań DNS, z czego 2969 (12%) zablokowanych, średni czas 102 ms. Widać, że sieć ładnie pracuje i filtruje.
Teraz w Portainerze robimy redeploy stacka nginx — i tym razem NPM wstaje bez problemu, z portami 80/443/81.
Panel NPM otworzycie na http://192.168.0.200:81. Domyślny login to admin@example.com / changeme — zmieńcie go natychmiast po pierwszym wejściu. Domyślne hasło w usłudze wystawionej choćby w sieci lokalnej to klasyk, na którym ludzie się przejeżdżają. Po zalogowaniu NPM od razu poprosi o zmianę danych konta i hasła.
Krok 6: ładne nazwy — strażnik i tłumacz
Teraz najfajniejsze. Potrzebujemy dwóch klocków, które grają razem:
- NPM (strażnik) — z nazwy wie, do której usługi i na jaki port przekazać ruch;
- AdGuard (tłumacz) — tłumaczy nazwę na adres IP serwera. To przecież serwer DNS, czyli z definicji tłumacz nazw na adresy. U nas tylko wygląda to ładniej.
Zaczynamy od strażnika. W NPM wchodzimy w Hosts → Proxy Hosts → Add Proxy Host i dla każdej usługi wpisujemy nazwę, schemat, adres serwera i port. Dobrze włączyć też Block Common Exploits:
nginx.madej→http→192.168.0.200:81portainer.madej→https→192.168.0.200:9443(schemathttps, bo Portainer ma certyfikat self-signed)adguard.babuszka→http→192.168.0.200:3000
Dla beki ustawiłem adguard.babuszka — żebyście wiedzieli, że to jest w pełni dowolne. Nazwę wymyślacie, jaką chcecie, i nigdzie w świecie to na nic nie wpływa, bo działa tylko wewnątrz waszej sieci.
Teraz tłumacz. W AdGuardzie wchodzimy w Filtry → Przepisywanie DNS → Dodaj przepisywanie DNS i każdą nazwę kierujemy na adres serwera (tego, gdzie siedzi NPM — nie na port usługi, NPM sam wie resztę):
nginx.madej→192.168.0.200portainer.madej→192.168.0.200adguard.babuszka→192.168.0.200
Uwaga, jeśli bawicie się w różne końcówki jak ja (
.madeji.babuszka): wildcard*.homeobejmie tylko nazwy kończące się na.home. Przy mieszanych sufiksach albo dodajcie każdą nazwę osobno (jak wyżej), albo zróbcie wildcard per sufiks (*.madej,*.babuszka).
Sprawdzamy w domu — wpisujemy adguard.babuszka, nginx.madej, portainer.madej i każda nazwa ładnie wchodzi na swoją usługę, bez portów.
A HTTPS?
NPM ma wbudowane Let’s Encrypt, ale dla wewnętrznych nazw .madej/.babuszka się nie sprawdzi — Let’s Encrypt wystawia certyfikaty tylko dla prawdziwych, publicznych domen. W sieci prywatnej spokojnie wystarczy http (ruch i tak leci szyfrowanym tunelem Tailscale). Jeśli kiedyś zechcecie prawdziwej kłódki, trzeba podpiąć własną domenę i certyfikat przez DNS challenge — ale to temat na osobny wpis.
Efekt — money shot na LTE
I teraz pointa całej części 3. Jesteście na mieście, wyłączacie Wi-Fi, zostaje samo LTE/5G, włączacie przełącznik Tailscale w telefonie i wpisujecie adguard.babuszka. Wchodzi — panel logowania AdGuarda, przez komórkę, z dowolnego miejsca.
Jeśli u was nazwy od razu nie zadziałają spoza domu, sprawdźcie dwie rzeczy, które łatwo przeoczyć (sam się na nich zatrzymałem):
- trasa subnetu zatwierdzona w admin console (
Machines → serwer → Edit route settings → 192.168.0.0/24); - „Use subnet routes” włączone w apce na telefonie — bez tego telefon widzi trasę, ale jej nie używa i nie dociera do adresów
192.168.0.x.
Szybki test, który rozdziela problemy: jeśli http://100.70.115.78:3000 (sam adres tailnetu serwera) działa, a nazwy nie — to tunel jest OK i problem leży tylko w trasie subnetu (punkty wyżej). Jeśli nawet 100.x nie wchodzi — to telefon nie jest realnie połączony.
Efekt końcowy:
- wchodzicie na
portainer.madejiadguard.babuszkajak w domu; - otwieracie panel routera na
192.168.0.1i inne urządzenia z sieci; - reklamy i phishing są wycinane, bo DNS dalej idzie do AdGuarda;
- a na routerze nie ma ani jednego otwartego portu na świat.
To jest ta różnica między „wystawiłem usługę do internetu i mam nadzieję, że nikt jej nie znajdzie” a „nic nie wystawiłem, a i tak mam dostęp do wszystkiego”.
Co dalej
Dałem wam piaskownicę — co w niej zrobicie, jest już po waszej stronie. Nikt wam nie podrzuci strony phishingowej, której AdGuard nie wytnie; jesteście chronieni tak dobrze, jak sami sobie to dokonfigurujecie, w domu i poza nim.
Ale to nie wszystko. W kolejnych częściach pokażę, jak skanuje się własne sieci w poszukiwaniu podatności i jak postawić SIEM (Wazuh), żeby na bieżąco sprawdzać, czy wszystko jest zdrowe i nic podejrzanego się nie dzieje. Po drodze jeszcze klucze SSH zamiast haseł i liczenie podsieci od podstaw.
Pamiętajcie: najlepsze zabezpieczenie to takie, którego napastnik w ogóle nie widzi. Nie wystawiajcie się na świat bez potrzeby — i chrońcie swoich bliskich.