Po uruchomieniu HAProxy przed Traefikiem usługi były dostępne, ale część logów pokazywała adres routera albo kontenera proxy zamiast adresu użytkownika. Samo poprawne kierowanie ruchu nie wystarczało: informację o kliencie trzeba było przekazać przez kolejne warstwy, a następnie odpowiednio zinterpretować.
W poprzednim artykule opisałem uruchomienie HAProxy na EdgeRouterze i rozdzielenie ruchu między dwa serwery. Tutaj skupiam się na adresach klientów, logach i granicach zaufania.
Domeny i adresacja są przykładowe. Router ma adres
10.10.10.254, a sieci Docker korzystają z zakresów10.20.X.0/24. Nie są to rzeczywiste podsieci mojego wdrożenia. Przed zastosowaniem konfiguracji należy dopasować adresy do własnej infrastruktury.
Dlaczego aplikacja widzi adres proxy
Ścieżka połączenia wygląda następująco:
Klient → HAProxy na routerze → Traefik → aplikacja lub serwer WWW
Każde proxy otwiera nowe połączenie do następnego elementu. Bez dodatkowej konfiguracji backend widzi więc adres bezpośredniego nadawcy TCP.
W moim układzie wykorzystuję dwa mechanizmy:
| Odcinek | Sposób przekazania informacji o kliencie |
|---|---|
| HAProxy → Traefik | PROXY protocol v2 |
| Traefik → aplikacja | Nagłówki HTTP, przede wszystkim X-Forwarded-For |
PROXY protocol nie zmienia źródła pakietów. Podobnie nagłówek HTTP nie tworzy bezpośredniego połączenia aplikacji z użytkownikiem. Oba mechanizmy przekazują metadane, które odbiorca musi świadomie wykorzystać.
W LAN lokalny DNS nadal kieruje klientów bezpośrednio do serwerów. Poprawnym adresem w logu może być wtedy prywatny adres klienta. Celem nie jest publiczne IP w każdym wpisie, lecz rzeczywiste źródło danego żądania.
HAProxy → Traefik: PROXY protocol
HAProxy przekazuje HTTPS bez odszyfrowania. Na tym etapie informację o kliencie wysyłam przez PROXY protocol v2, dodając send-proxy-v2 do backendów:
backend https_157
mode tcp
server srv157 10.10.10.157:443 check send-proxy-v2
backend https_251
mode tcp
server srv251 10.10.10.251:443 check send-proxy-v2
backend http_157
mode http
server srv157 10.10.10.157:80 check send-proxy-v2
backend http_251
mode http
server srv251 10.10.10.251:80 check send-proxy-v2
Po stronie Traefika uzupełniłem konfigurację statyczną TOML:
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.proxyProtocol]
trustedIPs = ["10.10.10.254/32"]
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.proxyProtocol]
trustedIPs = ["10.10.10.254/32"]
To fragment do połączenia z istniejącą konfiguracją. Najpierw przygotowałem odbiór w Traefiku i zrestartowałem go po zmianie konfiguracji statycznej. Dopiero potem włączyłem wysyłanie PROXY protocol w HAProxy.
Nie włączałem proxyProtocol.insecure. Traefik ufa informacjom przekazanym przez router, nie przez dowolnego klienta. Na publicznych listenerach HAProxy nie dodawałem accept-proxy.
EntryPointy nadal przyjmują zwykłe połączenia bez PROXY protocol, dzięki czemu bezpośredni dostęp z LAN pozostaje możliwy.
Zaufanie do PROXY protocol i ustawienia forwardedHeaders.trustedIPs to osobne mechanizmy. Nie rozszerzałem zaufania do nagłówków HTTP na całe sieci tylko po to, aby naprawić logi.
Forgejo: zaufanie do Traefika
W istniejącej sekcji [security] pliku app.ini ustawiłem:
[security]
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = 10.20.10.4/32
10.20.10.4 oznacza przykładowy adres Traefika w sieci Forgejo. Po wskazaniu właściwego adresu i restarcie aplikacja zaczęła poprawnie rozpoznawać klientów.
Bezpośrednim HTTP reverse proxy dla Forgejo jest Traefik. HAProxy przekazuje wcześniej informację przez PROXY protocol, dlatego nie zwiększałem mechanicznie limitu do dwóch tylko dlatego, że na schemacie są dwa proxy.
Nie włączałem przy tym uwierzytelniania przez nagłówki reverse proxy. Rozpoznawanie IP i logowanie użytkowników przez nagłówki to różne funkcje.
Dynamiczny adres proxy w odseparowanych stackach
Traefik jest dołączany do sieci poszczególnych stacków przez traefik_network_connector. Dzięki temu aplikacje nie muszą współdzielić jednej dużej sieci, ale adres Traefika może zmieniać się po odtworzeniu kontenera.
W takim układzie alternatywą dla pojedynczego /32 jest stała podsieć stacka i zaufanie do tego zakresu:
networks:
default:
driver: bridge
ipam:
config:
- subnet: 10.20.10.0/24
[security]
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = 10.20.10.0/24
Stała jest podsieć, a nie adres samego Traefika.
To kompromis: zaufanie obejmuje także inne kontenery w tej sieci. Taki zakres powinien być wydzielony i kontrolowany. Nie ma potrzeby ufać całemu 10.0.0.0/8 ani wszystkim sieciom Docker.
Kolizja podsieci Docker
Przy ręcznym wyborze zakresu pojawił się błąd:
invalid pool request: Pool overlaps with other one on this address space
Nowa podsieć nakładała się na istniejącą. Zamiast usuwać sieci innych usług, sprawdziłem ich zakresy i tablicę routingu hosta:
docker network inspect $(docker network ls -q) \
--format '{{.Name}}: {{range .IPAM.Config}}{{.Subnet}} {{end}}'
ip -4 route
Adresację trzeba dopasować również do sieci LAN i VPN, nie tylko do istniejących sieci Docker. Wszystkie zakresy pokazane w tym artykule są przykładami, nie propozycją uniwersalnej adresacji.
Matomo: aplikacja i Apache mają osobne logi
Matomo miało już ustawienia proxy w config/config.ini.php:
[General]
proxy_client_headers[] = "HTTP_X_FORWARDED_FOR"
proxy_host_headers[] = "HTTP_X_FORWARDED_HOST"
proxy_scheme_headers[] = "HTTP_X_FORWARDED_PROTO"
Mimo tego docker logs matomo nadal pokazywało adres proxy. Przyczyną było to, że oglądałem logi dostępu Apache, a nie dane o odwiedzających rozpoznawane przez Matomo.
Zmiana konfiguracji aplikacji PHP nie zmienia automatycznie sposobu logowania w serwerze WWW.
W moim kontenerze moduł mod_remoteip nie był załadowany, a VirtualHost używał formatu combined:
CustomLog ${APACHE_LOG_DIR}/access.log combined
Trwała konfiguracja mod_remoteip
Na hoście przygotowałem /opt/matomo/apache/remoteip.conf:
LoadModule remoteip_module /usr/lib/apache2/modules/mod_remoteip.so
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 10.20.20.4
LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
10.20.20.4 to przykładowy adres Traefika w sieci Matomo. Przy kontrolowanej, stałej podsieci można wskazać odpowiedni CIDR zamiast jednego adresu.
RemoteIPInternalProxy pozwala uwzględniać również prywatne adresy klientów LAN przekazane przez zaufane proxy. Format %a zapisuje adres po przetworzeniu przez moduł. Oryginalny adres połączenia można dodatkowo logować jako %{c}a.
Do istniejącej listy wolumenów dodałem:
volumes:
- /opt/matomo/apache/remoteip.conf:/etc/apache2/conf-enabled/zz-remoteip.conf:ro
To rozwiązanie odpowiada używanemu przeze mnie wariantowi obrazu Apache. Przed zastosowaniem go w innym obrazie trzeba sprawdzić lokalizację modułu i sposób wczytywania konfiguracji. Jeśli moduł jest już włączony, nie należy ładować go ponownie.
Nie zmieniałem entrypoint, command ani VirtualHosta. Nadpisałem używany format combined, zamiast dodawać drugi CustomLog i dublować wpisy.
Po odtworzeniu kontenera nowe logi Apache pokazywały właściwe adresy. Ostrzeżenie o braku ServerName nie było przyczyną problemu.
Ta zmiana nie przepisuje historii i nie zastępuje ustawień prywatności ani anonimizacji IP w Matomo.
Nginx: ten sam problem, inny moduł
Podczas uruchamiania bloga za Traefikiem podobną konfigurację zastosowałem w Nginx. Na odcinku Traefik → Nginx korzystam z nagłówka HTTP, nie z PROXY protocol:
set_real_ip_from 10.20.50.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Zakres 10.20.50.0/24 oznacza tutaj wydzieloną, zaufaną sieć bloga, w której Traefik łączy się z Nginx.
Po przetworzeniu nagłówka $remote_addr zawiera adres klienta. Do diagnostyki przydaje się jednoczesne zapisanie adresu bezpośredniego proxy:
log_format blog
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'proxy=$realip_remote_addr xff="$http_x_forwarded_for"';
Definicję formatu umieszczam w kontekście http, a we właściwym bloku server wybieram go przez:
access_log /var/log/nginx/access.log blog;
W oficjalnym obrazie Nginx pliki z /etc/nginx/conf.d/ są standardowo dołączane w kontekście http, więc można umieścić tam definicję formatu i blok serwera.
Nie dodawałem proxy_protocol do listen 80, ponieważ Traefik przekazuje do tego backendu zwykłe żądania HTTP.
Pozostałe usługi
Nextcloud
Dla Nextclouda istotne jest zaufanie do Traefika jako bezpośredniego reverse proxy. Fragment istniejącej tablicy konfiguracji może wyglądać tak:
'trusted_proxies' => [
'10.20.30.4',
],
Adres musi odpowiadać proxy widzianemu przez Nextcloud. Nie jest to osobny plik PHP do wklejenia, lecz fragment config/config.php.
W moim wdrożeniu po poprawieniu połączenia HAProxy–Traefik Nextcloud pokazywał właściwe adresy, więc nie przebudowywałem jego działającej konfiguracji.
Authelia
Authelia również zaczęła pokazywać właściwy adres po poprawieniu przekazywania informacji między HAProxy a Traefikiem.
Nie dodawałem nowej opcji trusted_proxies do jej konfiguracji. Zachowałem dotychczasową integrację uwierzytelniania. Zaufanie do PROXY protocol na entrypoincie Traefika i zaufanie do nagłówków w obsłudze ForwardAuth to osobne zagadnienia.
Headscale i Headplane
Tutaj ważne było rozdzielenie dwóch komponentów:
- Headscale obsługuje klientów i udostępnia API;
- Headplane jest panelem administracyjnym, który sam wykonuje żądania do API Headscale.
Dla wersji Headscale obsługujących trusted_proxies konfiguracja zaufania znajduje się na głównym poziomie config.yaml:
trusted_proxies:
- "10.20.40.4/32"
Można też wskazać świadomie zaufaną, wydzieloną podsieć:
trusted_proxies:
- "10.20.40.0/24"
Headscale wykorzystuje nagłówki adresu klienta tylko od zaufanego nadawcy. Proxy musi zapewniać wiarygodne wartości, również dla nagłówków innych niż X-Forwarded-For. W obsługiwanych wersjach kolejność sprawdzania to True-Client-IP, X-Real-IP, a następnie X-Forwarded-For.
Nie dodawałem PROXY protocol między Traefikiem a Headscale. Na tym odcinku korzystam z HTTP i nagłówków.
Nie każdy prywatny adres w API oznacza jednak błąd:
Przeglądarka → Traefik → Headplane
|
+→ API Headscale
Gdy Headplane wywołuje API, klientem tego konkretnego połączenia jest Headplane. Prywatny adres jego kontenera może być prawidłowy — nie każda operacja jest bezpośrednim przekazaniem żądania przeglądarki.
Wpisy z /machine/register, /machine/map czy /api/v1/node, które analizowałem, pochodziły z Headscale. Nie traktowałem ich jako dowodu nieprawidłowego działania loggera Headplane. Obsługa adresów w logach poszczególnych ścieżek, w tym połączeń Noise, zależy również od wersji Headscale.
Granice zaufania
Najważniejsza jest nie sama nazwa nagłówka, lecz ustalenie, kto może dostarczać informacje o kliencie:
Traefik ufa PROXY protocol od routera.
Aplikacja ufa nagłówkom od swojego reverse proxy.
Apache i Nginx przetwarzają adresy tylko od wskazanych proxy.
Backendów nie należy udostępniać tak, aby niezaufany klient mógł ominąć proxy i samodzielnie zadeklarować dowolne IP.
Zaufanie do podsieci ułatwia pracę z dynamicznymi kontenerami, ale rozszerza grupę nadawców. Dlatego powinno dotyczyć konkretnej, kontrolowanej sieci, a nie całej prywatnej adresacji.
Poprawne logowanie adresów nie oznacza też, że trzeba przechowywać je bez ograniczeń. Retencja logów, rotacja i ustawienia prywatności analityki pozostają osobnymi decyzjami.
Efekt
Nie była to jedna globalna poprawka. Na wejściu przekazałem metadane przez PROXY protocol, dalej wykorzystałem nagłówki HTTP, a na końcu skonfigurowałem ich interpretację w aplikacjach i serwerach WWW.
Forgejo potrzebowało listy zaufanych proxy. W Matomo kluczowe było rozróżnienie danych aplikacji od logów Apache. Nginx wymagał konfiguracji modułu Real IP. Nextcloud i Authelia korzystały już z poprawionej ścieżki, a przy Headscale trzeba było dodatkowo rozróżnić użytkownika, klienta VPN i serwerowe wywołanie API.
W efekcie logi pokazują rzeczywiste źródła żądań zamiast jednego adresu routera lub kontenera — bez bezwarunkowego zaufania do nagłówków dostarczanych z Internetu.