OpenVPN w Dockerze: kontrakt wykonawczy potrzebny minimalnemu klientowi
Sprawdzony obraz, jeden chroniony profil, dostęp do TUN i NET_ADMIN. Co dostaje kontener, czego nie wolno mu dostać i jak udowodnić, że tunel działa.
Wstaw klienta VPN do kontenera, a dostaniesz jedną przestrzeń nazw z tunelem w środku, nie host, który kieruje cały swój ruch przez VPN. To osobne projekty, a traktowanie ich jako jednego jest właśnie tym, jak kontenery przypadkiem stają się uprzywilejowane. Ten przewodnik obejmuje tylko to pierwsze: jeden sprawdzony obraz, jeden chroniony profil i proces klienta ograniczony do własnej przestrzeni nazw sieci.
Zacznij od artefaktu, który ktoś już zatwierdził. Poniższe kroki zakładają obraz zawierający plik wykonywalny openvpn oraz pełny profil klienta z certyfikatem i materiałem klucza już rozwiązanymi wewnątrz montowania. Nie budują oprogramowania, nie wydają poświadczeń i nie obejmują środowisk bez roota ani natywnych kontenerów Windows, z których każde zmienia odpowiedź.
Co kontener dostaje, a czego nie
Trzymaj profil i wszystko, na co się powołuje, w lokalnym katalogu z ograniczonymi uprawnieniami, zamontowanym tylko do odczytu. Ścieżki względne w profilu muszą się rozwiązywać w obrębie tego montowania; to szczegół, który ludzie pomijają, gdy certyfikat leży o katalog wyżej niż profil i kontener nigdy go nie widzi.
services:
vpn:
image: ${VPN_CLIENT_IMAGE:?set the reviewed image reference}
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ./vpn:/vpn:ro
working_dir: /vpn
entrypoint: ["openvpn"]
command: ["--config", "/vpn/client.ovpn"]
Ten plik daje trzy rzeczy i nie prosi o nic więcej. Odwołanie do obrazu powinno być przypięte przez digest, jeśli tak mówi twoja polityka wdrażania, a profil powinien być tym sprawdzonym, a nie kopią, którą ktoś edytował na miejscu na hoście.
Tylko do odczytu oznacza tu, że kontener nie może zmieniać twoich plików na hoście; nie oznacza, że nie może odczytać prywatnego klucza, który zamontowałeś. Na tym polega cały sens montowania i dlatego uprawnienia plików, własność i sposób, w jaki sekret trafił na host, wciąż mają znaczenie. Nigdy nie przekazuj hasła w wierszu poleceń ani nie zapiekaj go w warstwie obrazu, bo oba przetrwają w miejscach, których później nie posprzątasz.
Uprawnienia, entrypoint i pułapka pomiędzy nimi
Potrzebne są dwa ustawienia i robią one niezwiązane ze sobą rzeczy. NET_ADMIN pozwala na manipulację interfejsami i trasami w przestrzeni nazw kontenera. Wpis urządzenia /dev/net/tun pozwala procesowi w ogóle otworzyć urządzenie tunelu. Danie jednego bez drugiego daje wyraźny błąd, co jest wygodne: brakujące urządzenie TUN zawodzi przy otwieraniu, natomiast brakujące uprawnienie zawodzi, gdy klient próbuje skonfigurować interfejs.
Żadne z nich nie uzasadnia privileged: true. Uprzywilejowany kontener dostaje wszystkie uprawnienia plus dostęp do urządzeń, a to znacznie większe nadanie niż potrzebuje to obciążenie. dokumentacja uprawnień i możliwości środowiska wykonawczego wyjaśnia, jak daleko sięga to nadanie. Jeśli chart lub wpis na blogu o to prosi, najpierw sprawdź, czy wymaganiem nie była faktycznie brakująca ścieżka urządzenia albo brakujące uprawnienie.
Nadpisanie entrypoint to ta część, która gryzie. Bezpośrednie wywołanie openvpn jest poprawne dla zwykłego obrazu i błędne dla każdego obrazu produktu, którego skrypt startowy przygotowuje reguły zapory, pliki resolvera lub poświadczenia przed uruchomieniem demona. Nadpisanie tego skryptu pomija przygotowanie i zostawia cię z debugowaniem tunelu, który wstał w na wpół skonfigurowanej przestrzeni nazw.
Uruchamiaj klienta na pierwszym planie i pozwól mu być głównym procesem kontenera. Proces, który demonizuje się, kończy się natychmiast, kontener restartuje się w pętli, a potrzebne logi są rozproszone po restartach, które wszystkie wyglądają tak samo.
Weryfikuj od wewnątrz przestrzeni nazw, nie z hosta
Podnieś stos, potwierdź w logu, że klient osiągnął stan połączenia, a następnie sprawdź przestrzeń nazw, która faktycznie trzyma tunel.
docker compose up -d
docker compose logs --tail 40 vpn
docker compose exec vpn ip -br addr
docker compose exec vpn ip route
Lista adresów powinna pokazywać interfejs tunelu z adresem z puli, a tabela tras powinna pokazywać wypchnięte trasy do miejsc docelowych. Jeśli interfejsu nie ma, klient nigdy nie dotarł do momentu jego utworzenia. Jeśli istnieje, ale żadna trasa go nie używa, miejscem do szukania są push lub opcje tras.
Następnie sprawdź miejsce docelowe, na którym ci zależy, z przestrzeni nazw, która powinna używać tunelu, za pomocą aplikacji, a nie tylko pingiem. Rozwiązywanie nazw to osobna awaria, którą warto przetestować jawnie: wypchnięty adres DNS nie aktualizuje resolvera kontenera sam z siebie, a samodzielny klient, który ignoruje push, będzie rozwiązywał nazwy przez to, co skonfigurował obraz.
Stan rozłączenia jest częścią kontraktu
Zachowanie przy ponownym łączeniu wymaga własnej kontroli, bo polityka restartu dowodzi, że proces się restartuje, i nie mówi nic o tym, co robi ruch w międzyczasie. Zatrzymaj tunel, spróbuj zamierzonego miejsca docelowego i obserwuj, czy połączenie jest odrzucane, wisi, czy po cichu udaje się zwykłą trasą kontenera. Ostatni wynik jest tym, który ma znaczenie, i jest niewidoczny w zdrowym logu.
Jeśli obciążenie nigdy nie może używać zwykłej drogi, zaprojektuj to jawnie za pomocą reguł zapory i sygnału gotowości wewnątrz przestrzeni nazw. Kill switch to polityka, którą piszesz i testujesz, a nie właściwość, którą dziedziczysz, uruchamiając klienta w kontenerze.
Na koniec pamiętaj o granicy: te trasy żyją w przestrzeni nazw kontenera i nie zastępują tabeli routingu hosta. Reguły przekazywania, tłumaczenia i zapory Dockera pozostają częścią zewnętrznej drogi, a opublikowany port nie jest interfejsem VPN.