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ń.