VPN site-to-site oparty o strongSwan IPsec i XFRM
Table of Contents
Chcąc połączyć ze sobą dwa data-center lub dwa serwery hostujące kontenery potrzebujemy łączności site-to-site. Można to osiągnąć zwykłym VPNem w rodzaju OpenVPN i ręcznym ustawianiem routingu, jednak nie jest to zbyt wydajne, ani eleganckie. Zaprezentuję jak zestawić takie połączenie dzięki strongSwan sterującym zaimplementowanym w jądrze stosem IPsec - XFRM. Dodatkowo pokażę jak się do takiej sieci podłączyć z zewnątrz i podam kilka wskazówek dotyczących zestawienia tego u Hetznera - łącząc dedykowany serwer używający vSwitch’a z serwerem w Hetzner Cloud. Na koniec kilka słów o podłączaniu się do takiego VPNa z zewnątrz.
Topologia i wymagania wstępne #
Zacznijmy od wyjaśnienia, czym zajmują się poszczególne komponenty:
- strongSwan to narzędzie do zestawiania tuneli VPN wykorzystujące protokół IPsec; zarządzane przez plik
ipsec.conf oraz usługę
ipsec.service
- XFRM to implementacja IPsec w linuksowym jądrze; wykorzystuje routing za pomocą VRF, czyli bez zarządzanych za pomocą ifconfig interfejsów tun/tap znanych głównie z OpenVPN; zarządzane przez komendę ip xfrm
- VRF (Virtual Routing and Forwarding) - technologia zaimplementowana w kernelu pozwalająca na istnienie dodatkowych tablic routingu, przypomina VLANy dla protokołu IP; zarządzana przez użytkownika za pomocą komendy ip vrf
- IKE (Internet Key Exchange) to protokół do wymiany kluczy kryptograficznych, jeden z wielu dostępnych dla strongSwan i dość szeroko przyjęty przez nielinuksowych klientów
Co będzie nam potrzebne:
- dwie prywatne podsieci dla serwerów wewnątrz dwóch lokalizacji - na przykład
10.1.0.0/16
i10.2.0.0./16
- interfejsy sieciowe głównych serwerów, za pomocą których chcemy łączyć się między nimi - mogą to być tranzytowe linki do internetu, albo dedykowane - często dostawcy usług jak Hetzner dostarczając prywatne łącza o wyższej dostępności i niższym koszcie (często darmowe) - tu będą na serwerze A interfejs
eth0
na VLANie 4444 oraz na serwerze B osobny interfejseth1
; połączenie to jest routowalne - w tym wypadku w sieci172.16.0.0/24
(konfiguracja routingu po stronie Hetzner Cloud - https://docs.hetzner.com/cloud/networks/connect-dedi-vswitch; serwer A jest fizyczny i używa vSwitch, serwer B jest wirtualny) - PSK (pre shared key) / hasło (np. długi alfanumeryczny ciągi) dla połączenia site-to-site
- opcjonalnie dla klientów zewnętrznych: PKI, którego proste tworzenie opiszę pod koniec artykułu
Topologia przykładowej sieci i usług wygląda następująco:
Instalacja pakietów #
Wstępnym założeniem jest używanie Ubuntu Server z pakietem netplan
do zarządzania połączeniami sieciowymi. Oczywiście każdy inny Linuks też zadziała z odpowiednimi modyfikacjami dla sterowania siecią.
Do samego VPNa będą nam potrzebne dodatkowo strongswan libcharon-extra-plugins libstrongswan-extra-plugins iptables
.
Konfiguracja dodatkowych interfejsów (Hetzner vSwitch) #
Jeśli połączenie VPN będziemy realizować po sieci prywatnej takiej jak Hetzner vSwitch, na początek musimy skonfigurować odpowiednie interfejsy. Na maszynach wirtualnych Hetzner Cloud najlepiej skorzystać z gotowego narzędzia hc-utils
(
https://docs.hetzner.com/cloud/networks/server-configuration/). Na maszynach dedykowanych interfejs można ustawić za pomocą netplan
. Przykładowy plik konfiguracyjny /etc/netplan/01-netcfg.yaml
może wyglądać tak:
---
network:
version: 2
renderer: networkd
ethernets:
enp9s0:
addresses:
- 1.1.1.1/32
- 1.1.1.2/32
routes:
- on-link: true
to: 0.0.0.0/0
via: 1.1.1.255
nameservers:
addresses:
- 213.133.100.100
- 213.133.98.98
- 213.133.99.99
vlans:
vlan.4444:
mtu: 1450
id: 4444
link: enp9s0
addresses: [172.16.0.18/28]
routes:
- on-link: true
to: 172.16.0.0/24
via: 172.16.0.17
Sekcja ethernets
w powyższym prawdopodobnie będzie już skonfigurowana przez instalatora. Po zmianie pliku zmiany można zaaplikować przez użycie netplan apply
lub netplan try
.
Pułapka z MTU #
To, co okazało się dla mnie istotne podczas używania połączenia to odpowiednie MTU. Linuks powinien sam dobrać odpowiednie, ale w moim wypadku ustawił wartość maksymalnego rozmiaru jednostki transportu na taką samą jak fizyczny interfejs, czyli 1500 bajtów. Ze względu na specyfikę Hetznerowego vSwitcha, wartość ta powinna być mniejsza, na przykład 1450 bajtów. Problem odkryłem, kiedy łączność z serwera dedykowanego do maszyny wirtualnej nawiązywana przez VLAN vSwitcha niby działała, ale połączenia SSH zawieszały się całkowicie na komunikacie expecting SSH2_MSG_KEX_ECDH_REPLY
(widocznym przy trybie debugowania ssh -vvvv
).
Konfiguracja strongSwan - site-to-site #
Składnia pliku /etc/ipsec.conf
jest dość prosta - sekcja setup
określa globalne ustawienia, takie jak poziom szczegółowości logów, a sekcje conn ...
określają konkretne połączenia. W sekcjach połączeń strona lewa to strona “nasza”, a prawa to zdalna. Parametr left
określa IP, na którym zostanie zestawione połączenie IPsec, a leftsubnet
to podsieć, którą będziemy routować (podobnie dla right
). Przykładowe pliki konfiguracyjne poniżej.
Na serwerze A:
config setup
charondebug="ike 1, knl 1, cfg 1"
uniqueids=no
conn a-to-b
authby=secret
type=tunnel
auto=route
left=172.16.0.18
leftsubnet=10.0.0.1/16
right=172.16.0.2
rightsubnet=10.1.0.1/16
ike=aes256-sha2_256-modp1024!
keyexchange=ikev2
esp=aes256-sha2_256!
reauth=no
Na serwerze B:
config setup
charondebug="ike 1, knl 1, cfg 1"
uniqueids=no
conn b-to-a
forceencaps=yes # required because of Hetnzer Cloud weird setup of NATed DMZ
authby=secret
type=tunnel
auto=route
left=172.16.0.2
leftsubnet=10.1.0.1/16
right=172.16.0.18
rightsubnet=10.0.0.1/16
ike=aes256-sha2_256-modp1024!
keyexchange=ikev2
esp=aes256-sha2_256!
reauth=no
Dodatkowo należy skonfigurować PSK w pliku /etc/ipsec.secrets
(w obie strony PSK jest taki sam):
172.16.0.2 172.16.0.18 super-secret-psk
172.16.0.18 172.16.0.2 super-secret-psk
Konfiguracja zawiera aż trzy pułapki.
Pułapka pierwsza - NAT #
Pierwsza dotyczy środowiska Hetzner Cloud, a więc maszyny wirtualnej - choć oczywiście problem występuje także w innych konfiguracjach. Czasami strongSwan nie jest w stanie wykryć, że znajduje się za NATem. W takiej sytuacji należy dodać forceencaps=yes
do sekcji połączenia - ale tylko na tym serwerze, gdzie mamy do czynienia z NATem.
Pułapka druga - renegocjacja klucza #
Jeśli połączenie ma być stabilne, a za dodatkową warstwę bezpieczeństwa odpowiada prywatny link, najlepiej będzie zrezygnować z okresowego renegocjowania kluczy szyfrujących połączenie. W przeciwnym wypadku pojawiać się będzie dziwny packet loss. Aby to ustawić, należy dodać reauth=no
i usunąć sekcję ikelifetime=1h
.
Namierzenie tego problemu zajęło mi dość dużo czasu, a było o tyle irytujące, że Prometheus, którego używam do monitorowania serwerów, bardzo szybko zauważał brak połączenia i wywoływał alerty w PagerDuty. Doprowadziło to do odświeżenia przeze mnie starego projektu Sauron, czyli narzędzia do monitorowania packet lossu do zadanych adresów IP i przepisania go tak, żeby mógł wysyłać wyniki do bazy danych InfluxDB. Projekt jest dostępny na GitHubie - https://github.com/danielskowronski/sauron4/
Moje pierwsze podejrzenie dotyczyło stabilności łącza prywatnego, jednak okazało się błędne. W celu badania jakichś zależności postawiłem taki oto dashboard w Grafanie:
Dopiero analiza logów z journalctl
skorelowanych z kilkoma punktami gdzie pokazał się packet loss jedynie na sieci routowanej przez VPN pokazał, w czym tkwi problem - 172.16.0.2 is initiating an IKE_SA
występował zawsze przed utratą połączenia.
Pułapka trzecia - maskarada #
Żeby kontenery na naszych serwerach mogły łączyć się z internetem (będąc w podsieciach sieci 10.0.0.0/8) potrzeba nam klasycznej maskarady w iptables. Jeślibyśmy poprzestali na skonfigurowaniu LXD tak, żeby zarządzane interfejsy (tu lxdbr0
) miały automatycznie zarządzany NAT to łączność z internetem zadziała, jednak między podsieciami (10.1.0.0/16 i 10.2.0.0/16) połączenie się nie uda - reguły PREROUTING
będą źle kierować ruchem.
Aby tego uniknąć, należy zmienić ustawienie interfejsu sieciowego lxc za pomocą lxc network edit lxdbr0
tak, by config.ipv4.nat
miał wartość false
oraz ręcznie ustawić maskaradę. Można to osiągnąć za pomocą takiego ustawienia iptables:
MY_NET=10.1.0
REMOTE_NET=10.2.0
iptables-legacy \
-t nat -A POSTROUTING \
-s "${MY_NET}.0/16" ! -d 10.0.0.0/8 \
-m comment --comment "10.0.0.0/8 lxdbr0" \
-j MASQUERADE
Aby wykonywał się przy każdym zrestartowaniu połączenia VPN, można dodać do /etc/ipsec.conf
do sekcji conn x-to-y
wpisy rightupdown=
i leftupdown=
z podaną ścieżką do naszego skryptu.
Startowanie i testowanie połączenia #
Mając wszystkie pliki na miejscu można startować - serwis zazwyczaj nazywa się ipsec, choć może to być jedynie alias. W Ubuntu 21.10 usługa to strongswan-starter.service
. Stan połączeń możemy sprawdzić za pomocą ipsec status
:
Routed Connections:
ulthar-to-rlyeh{1}: ROUTED, TUNNEL, reqid 1
ulthar-to-rlyeh{1}: 10.1.0.0/16 === 10.2.0.0/16
Security Associations (2 up, 0 connecting):
ulthar-to-rlyeh[685]: ESTABLISHED 100 seconds ago, 172.16.0.2[172.16.0.2]...172.16.0.18[172.16.0.18]
ulthar-to-rlyeh{695}: INSTALLED, TUNNEL, reqid 1, ESP in UDP SPIs: xxxxxxxx_i xxxxxxxx_o
ulthar-to-rlyeh{695}: 10.1.0.0/16 === 10.2.0.0/16
ulthar-to-rlyeh[684]: ESTABLISHED 35 minutes ago, 172.16.0.2[172.16.0.2]...172.16.0.18[172.16.0.18]
ulthar-to-rlyeh{696}: INSTALLED, TUNNEL, reqid 1, ESP in UDP SPIs: xxxxxxxx_i xxxxxxxx_o
ulthar-to-rlyeh{696}: 10.1.0.0/16 === 10.2.0.0/16
Sama komenda ipsec
pozwala na restartowanie połączeń, jednak jeśli używamy systemctl
do startowania połączeń może pojawić się konflikt - osobiście używam ipsec
tylko do diagnostyki, nie kontroli.
Połączenie client-to-site #
Do kompletu warto dodać też możliwość podłączenia się klienta do naszej sieci 10.0.0.0/8 - na przykład ze stacji roboczej. Użyjemy tego samego strongSwana, jednak z nieco innymi ustawieniami szyfrowania tak, by używać natywnych klientów dostępnych w systemach operacyjnych i nieco mocniej zabezpieczyć połączenie. Konkretniej będzie to szyfrowanie asymetryczne z wykorzystaniem prywatnego PKI oraz protokołu MS-CHAPv2
Do zarządzania PKI używam narzędzia
smallstep. Można oczywiście używać ręcznie openssl
, ale szansa przegapienia ważnych flag w certyfikacie CA, czy za długo czasu życia certyfikatu serwera przy obecnie ciągle ulepszanych standardach jest tak duża, że warto nieco pójść na łatwiznę. Przy okazji smallstep potrafi więcej niż tylko ułatwiać generowanie certyfikatów ręcznie.
Na początek potrzebny będzie nam Root CA - możemy to załatwić jedną komendą step ca init
opisaną na
https://smallstep.com/docs/step-cli/reference/ca/init. Rzecz jasna klucz prywatny musimy bezpiecznie przechowywać. Karta inteligentna wydaje się dobrym rozwiązaniem. Sam certyfikat Root CA trzeba zapisać i zdeployować na wszystkie maszyny, które mają mu ufać oraz ustawić całkowite zaufanie - na Linuksach za pomocą /usr/sbin/update-ca-certificates
, na macOS za pomocą Keychain Access.app
.
Następnie potrzebujemy certyfikatu dla serwera. Jeśli będziemy używać macOS lub iOS do łączenia się nie możemy używać ECDSA do tego certyfikatu (sam Root CA może być ECDSA) tylko standardowego RSA. Problem polega na tym, że niby macOS obsługuje kryptografię krzywej eliptycznej w IKEv2 ( https://support.apple.com/pl-pl/guide/deployment/depae3d361d0/web), to nie można tego ustawić z normalnego interfejsu - jedynie przez narzędzia do generowania profili konfiguracyjnych ( https://wiki.strongswan.org/projects/strongswan/wiki/AppleIKEv2Profile i https://github.com/skowronski-cloud/skowronski-cloud-wiki/blob/master/rlyeh/03_vpn.md#note-on-key-type-selection). Taki certyfikat ważny przez 5 lat możemy wygenerować w ten sposób:
step certificate create ipsec.example.com ipsec.crt ipsec.key \
--kty RSA --size 4096 --ca root_ca.crt --ca-key root_ca_key \
--no-password --insecure \
--san ipsec.example.com --san 1.1.1.1 \
--not-after 43800h
Flaga --insecure
jest potrzebna, by ustawić brak hasła do klucza prywatnego. Jako SAN należy podać domenę, warto dodać też adres IP.
Certyfikat CA, serwera oraz klucz serwera wrzucamy jako pliki PEM odpowiednio do /etc/ipsec.d/cacerts/
, /etc/ipsec.d/certs/
oraz /etc/ipsec.d/private/
. Certyfikat serwera podajemy potem w konfigu jako leftcert
Aby dodać klientów do naszego VPNa, do /etc/ipsec.conf
dodajemy:
conn ikev2-vpn
auto=add
compress=no
type=tunnel
keyexchange=ikev2
fragmentation=yes
forceencaps=yes
dpdaction=clear
dpddelay=300s
rekey=no
left=%any
[email protected]
leftcert=server-cert.pem
leftsendcert=always
leftsubnet=10.0.0.0/8
right=%any
rightauth=eap-mschapv2
rightsendcert=never
conn ikev2-vpn-client_a
also=ikev2-vpn
rightid=client_a
eap_identity=client_a
rightsourceip=10.0.255.100/32
conn ikev2-vpn-client_b
also=ikev2-vpn
rightid=client_b
eap_identity=client_b
rightsourceip=10.0.255.200/32
Poprawność instalacji kluczy możemy zweryfikować przy użyciu ipsec listcerts
i ipsec listcacerts
.
Sekcja conn ikev2-vpn
pozwala stworzyć szablon dla konkretnych połączeń doprecyzowanych w tym przykładzie jako ikev2-vpn-client_a
i ikev2-vpn-client_b
. client_a
i client_b
to loginy użytkowników, a rightsourceip
to adresy IP, jakie otrzymają po zalogowaniu się. Parametr leftid
jest bardzo ważny - musi to być parametr, który klienci podadzą w ustawieniach połączenia.
W pliku /etc/ipsec.secrets
musimy wskazać na certyfikat serwera oraz zdefiniować hasła każdego z klientów (dopasowywane według pola eap_identity
):
: RSA /etc/ipsec.d/private/server-key.pem
client_a : EAP plaintext-password-for-client_a
client_b : EAP plaintext-password-for-client_b
Przykładowa konfiguracja zaufania certyfikatu i klienta VPN na macOS:
Podsumowanie #
Konfiguracja strongSwan jest w miarę prosta, lecz można wpaść na wiele pułapek sieciowych i kryptograficznych. Dodatkowo XFRM nie jest tak oczywisty, jak klasyczny routing w Linuksie i wymaga trochę nauki, lecz jest dalece wydajniejszy i bardziej elastyczny od starych rozwiązań.