EN
← wszystkie wpisy

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.

  • homelab
  • Tailscale
  • nginx
  • VPN
Read in English

W 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:

  1. 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.
  2. Nginx Proxy Manager — reverse proxy, żeby zamiast pamiętać :9443 i :3000 wchodzić na ładne portainer.madej czy adguard.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
Terminal: instalacja Tailscale, sudo tailscale up z linkiem logowania i tailscale ip -4 zwracające adres 100.70.115.78
Instalacja, logowanie i adres serwera w tailnecie: 100.70.115.78 (linki logowania zamaskowane — to jednorazowe tokeny).

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.

Admin console Tailscale: maszyna madej z adresem 100.70.115.78, wersją i statusem Connected
Serwer w admin console (e-mail konta zamaskowany).

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
Terminal: włączenie ip_forward w sysctl i tailscale up z parametrem advertise-routes dla 192.168.0.0/24
Włączamy przekazywanie pakietów i rozgłaszamy całą podsieć 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
Terminal: tworzenie skryptu networkd-dispatcher z komendą ethtool włączającą UDP GRO i ręczne jej wywołanie
Opcjonalny protip: włączenie UDP GRO dla lepszego throughputu tunelu.

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…

Admin console Tailscale, sekcja Nameservers z rozwiniętą listą Add nameserver i opcją Custom
Nie publiczne DNS-y, tylko „Custom…” — wskażemy własny serwer (nazwa tailnetu zamaskowana).

W pole wpisujemy adres 100.x serwera (ten z tailscale ip -4, u mnie 100.70.115.78) i włączamy Override local DNS.

Global nameservers z wpisanym adresem 100.70.115.78 i włączonym przełącznikiem Override DNS servers
Serwer 100.70.115.78 jako globalny nameserver, Override DNS servers włączony.

Gotowe — gdy Tailscale jest aktywny, wszystkie zapytania DNS telefonu idą do AdGuarda przez szyfrowany tunel.

Używamy adresu 100.x, a nie 192.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ąć:

Portainer, błąd deploymentu stacka nginx: failed to bind host port 0.0.0.0:80/tcp: address already in use
„address already in use” — klasyk. Port 80 trzyma AdGuard.

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
Terminal: find znajduje plik /opt/adguardhome/conf/AdGuardHome.yaml i otwarcie go w nano
Config AdGuarda: /opt/adguardhome/conf/AdGuardHome.yaml.

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):

Edytor nano z plikiem AdGuardHome.yaml, pole address zmienione na 0.0.0.0:3000, hash hasła zamaskowany
address: 0.0.0.0:3000 — port 80 zwolniony (hash hasła admina zamaskowany).

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.

Panel AdGuard Home na porcie 3000: 23809 zapytań DNS, 2969 zablokowanych (12%), statystyki klientów
AdGuard na :3000, port 80 wolny. 23 809 zapytań, 12% wyciętych.

Teraz w Portainerze robimy redeploy stacka nginx — i tym razem NPM wstaje bez problemu, z portami 80/443/81.

Portainer: kontener nginx-npm-1 w stanie running z opublikowanymi portami 80, 443 i 81
nginx-npm-1 w stanie running. Port 80 pokonany.

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.

Nginx Proxy Manager, okno Edit User ze zmianą danych administratora, e-mail zamaskowany
Zmiana domyślnego konta admina (e-mail zamaskowany). Hasło też zmieniamy od razu.

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:81
  • portainer.madej → https → 192.168.0.200:9443 (schemat https, bo Portainer ma certyfikat self-signed)
  • adguard.babuszka → http → 192.168.0.200:3000
Nginx Proxy Manager, formularz New Proxy Host dla portainer.madej, schemat https, adres 192.168.0.200, port 9443
Proxy host dla Portainera: schemat https, port 9443, Block Common Exploits włączony.

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.200
  • portainer.madej → 192.168.0.200
  • adguard.babuszka → 192.168.0.200
AdGuard Home, Przepisywanie DNS: trzy wpisy nginx.madej, portainer.madej i adguard.babuszka wskazujące na 192.168.0.200
Trzy przepisania DNS, wszystkie na 192.168.0.200. Resztę załatwia NPM.

Uwaga, jeśli bawicie się w różne końcówki jak ja (.madej i .babuszka): wildcard *.home obejmie 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.

Przeglądarka z adresem adguard.babuszka/login.html otwierającym panel logowania AdGuard Home
adguard.babuszka działa. Nazwa → tłumacz (AdGuard) → strażnik (NPM) → usługa.

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.

iPhone na 5G: przeglądarka otwiera adguard.babuszka i pokazuje panel logowania AdGuard Home
Telefon na 5G, bez Wi-Fi: adguard.babuszka otwiera się jak w domu.

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):

  1. trasa subnetu zatwierdzona w admin console (Machines → serwer → Edit route settings → 192.168.0.0/24);
  2. „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.
iOS, ustawienia Tailscale, ekran Subnet Routing z włączonym przełącznikiem Use Tailscale Subnets
„Use Tailscale Subnets” włączone — telefon realnie korzysta z tras do sieci domowej.

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.madej i adguard.babuszka jak w domu;
  • otwieracie panel routera na 192.168.0.1 i 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.

← wszystkie wpisy