
strongSwan — опенсорсная имплементация IPsec, фреймворка VPN. Несмотря на двадцатилетний стаж, проект продолжает развиваться: последняя на сегодня версия приложения вышла в декабре. У него подробная документация, есть блог с CVE и публичная база тестов. По полезной пропускной способности, задержке и утилизации CPU strongSwan превосходит Wireguard, но остаётся в тени — из-за сложности и малой пригодности для обхода блокировок. Зато перед теми, кто не ленится, он открывает широкий простор для экспериментов.
strongSwan щедр на плагины и криптографические алгоритмы. В версии 6.0.0 к ним добавили ML-KEM — механизм инкапсуляции ключа с постквантовой стойкостью, почитать о котором можно тут. «Постквантовой» предполагает, что расшифровать трафик перехваченным ключом ML-KEM не получится даже на квантовом компьютере — даже потом. Пока от таких атак защищаются эфемерными ключами на эллиптических кривых, но однажды уязвимы, вслед за RSA, станут и они. В статье я расскажу, как подготовиться к этому, настроив strongSwan с ML-KEM и постквантовым предопределённым ключом (PPK).
Мои вводные: сервер на Debian 12 и сотрудница с Arch, которую надо подключить к удалённому рабочему месту в подсети 10.0.0.0/22
и панели администратора в облаке. Адрес сервера — 1.2.3.4
, админки — 5.6.7.8
, доступ из офиса в интернет ограничен. Аутентифицировать друг друга сервер и клиент будут по сертификатам ECDSA. Сертификаты — ещё одна сильная сторона strongSwan: приложение среди прочего поддерживает взаимную аутентификацию по TLS 1.3. Но мне это не подходит: сейчас ML-KEM в TLS выступает довеском к эллиптическим кривым, как элемент одной из гибридных схем, которых в strongSwan нет: его ML-KEM совместим только с протоколом обмена ключами IKEv2, штатным для IPsec.
В репозиториях большинства дистрибутивов, когда я приступал к задаче, нужная сборка 6.0.0 отсутствовала: у Arch — совсем, в нестабильной ветке Debian был отключён ML-KEM, а в официальных образах Docker (раз, два) устарела библиотека. Поэтому я скомпилировал файлы из исходников: взял скрипт для 5.9.14 и добавил флаг --enable-ml
. Перед компиляцией убедился, что IPv6 включён, и установил следующие пакеты:
python3-setuptools ruby3.3-dev libcurl4-gnutls-dev systemd-dev libsystemd-dev libnm-dev libgmp3-dev libssl-dev libwolfssl-dev libbotan-2-dev libgcrypt20-dev libpam0g-dev libip4tc-dev libcap-dev
Сервер
strongSwan не создаёт виртуальный интерфейс в системе, ядро которой поддерживает IPsec, но может добавлять адрес туннеля к физическому, а трафиком управляет на основе политик — определить их надо на фаерволе сервера. Я возьму для этого встроенный скрипт _updown
: открою порты NAT-T и IKEv2, разрешу ESP и создам таблицы nat
и mangle
. nat
будет заменять адрес клиента на публичный адрес сервера для доступа к облаку, mangle
— ограничивать MSS: в зависимости от алгоритмов IPsec по ESP может добавлять к заголовку пакета около ста байт — обычно это приводит к фрагментации, но если на пути попадутся узлы, которые отбрасывают ICMP 3 (Destination unreachable), фрагментация сломается и TCP-сессия клиента завершится по таймауту. Уменьшение MSS помогает это предотвратить. По той же причине форсирую адаптивный MTU, заодно включу форвардинг пакетов:
bob ~ sudo cat << EOF >> /etc/sysctl.d/99-sysctl.conf
net.ipv4.ip_forward = 1
net.ipv4.ip_no_pmtu_disc = 1
EOF
bob ~ sudo sysctl -qp
Скопирую в lib _updown
и в копии допишу: swanctl
up-client:)
iptables -I INPUT 1 -p udp --dport 500 --j ACCEPT
iptables -I INPUT 2 -p udp --dport 4500 --j ACCEPT
iptables -I FORWARD 1 -m policy --pol ipsec --dir in -p esp -s 10.0.0.0/22 -j ACCEPT
iptables -I FORWARD 2 -m policy --pol ipsec --dir out -p esp -d 10.0.0.0/22 -j ACCEPT
iptables -t nat -I POSTROUTING 1 -m policy --pol ipsec --dir out -j ACCEPT
iptables -t nat -I POSTROUTING 2 -s 10.0.2.0/23 -d 5.6.7.8/32 -o ens224 -m policy --pol ipsec --dir out -j ACCEPT
iptables -t nat -I POSTROUTING 3 -s 10.0.2.0/23 -d 5.6.7.8/32 -o ens224 -j MASQUERADE
iptables -t mangle -I FORWARD 1 -m policy --pol ipsec --dir in -s 10.0.0.0/22 -o ens224 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
iptables -t mangle -I FORWARD 2 -m policy --pol ipsec --dir out -s 10.0.0.0/22 -o ens224 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
;;
down-client:)
iptables -D INPUT -p udp --dport 500 --j ACCEPT
iptables -D INPUT -p udp --dport 4500 --j ACCEPT
iptables -D FORWARD -m policy --pol ipsec --dir in -p esp -s 10.0.0.0/22 -j ACCEPT
iptables -D FORWARD -m policy --pol ipsec --dir out -p esp -d 10.0.0.0/22 -j ACCEPT
iptables -t nat -D POSTROUTING -m policy --pol ipsec --dir out -j ACCEPT
iptables -t nat -D POSTROUTING -s 10.0.2.0/23 -d 5.6.7.8/32 -o ens224 -m policy --pol ipsec --dir out -j ACCEPT
iptables -t nat -D POSTROUTING -s 10.0.2.0/23 -d 5.6.7.8/32 -o ens224 -j MASQUERADE
iptables -t mangle -D FORWARD -m policy --pol ipsec --dir in -s 10.0.0.0/22 -o ens224 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
iptables -t mangle -D FORWARD -m policy --pol ipsec --dir out -s 10.0.0.0/22 -o ens224 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
;;
Настрою PKI: с помощью одноимённой утилиты выпущу ключ и сертификат удостоверяющего центра и подпишу сертификат сервера.
bob ~ sudo pki --gen --type ecdsa --size 521
--outform pem > /etc/swanctl/private/ca-key.pem
bob ~ sudo pki --self --ca --lifetime 1825
--in /etc/swanctl/private/ca-key.pem
--type ecdsa --dn "C=RU, O=Bob LLC, CN=strongSwan CA"
--outform pem > /etc/swanctl/x509ca/ca-cert.pem
bob ~ sudo pki --gen --type ecdsa --size 521
--outform pem > /etc/swanctl/private/bob-key.pem
bob ~ sudo pki --pub --in /etc/swanctl/private/bob-key.pem
--type ecdsa | pki --issue --lifetime 912
--cacert /etc/swanctl/x509ca/ca-cert.pem
--cakey /etc/swanctl/private/ca-key.pem
--dn "С=RU, O=Bob LLC, CN=bob.com" --san bob.com
--flag serverAuth --flag ikeIntermediate
--outform pem > /etc/swanctl/x509/bob-cert.pem
Значения --dn
и --san
сервера важны. Если его FQDN не резолвится в публичный адрес, подойдёт сам адрес. Сертификат клиента выпускается так же, но его алиас может быть любым, а --serverAuth
и --ikeIntermediate
не требуются. Будь у сотрудницы клиент на Windows, macOS, Android или iOS, её ключ и сертификат пришлось бы ещё поместить в контейнер PKCS#12.
Теперь сертификат клиента:
bob ~ sudo pki --gen --type ecdsa --size 521
--outform pem > /etc/swanctl/private/alice-key.pem
bob ~ sudo pki --pub --in /etc/swanctl/private/alice-key.pem
--type ecdsa | pki --issue --lifetime 912
--cacert /etc/swanctl/x509ca/ca-cert.pem
--cakey /etc/swanctl/private/ca-key.pem
--dn "C=RU, O=Bob LLC, CN=alice@bob.com" --san "alice@bob.com"
--outform pem > /etc/swanctl/x509/alice-cert.pem
Закончив с PKI, уберу с сервера ключ удостоверяющего центра — выпускать новые сертификаты станет неудобно, зато уменьшу риск компрометации инфраструктуры. Ключ сотрудницы удалю после передачи.
Сертификаты и ключ, которые остаются на сервере:
bob ~ ( cd /etc/swanctl && ls private x509 x509ca )
private/:
bob-key.pem
x509/:
alice-cert.pem bob-cert.pem
x509ca/:
ca-cert.pem
Сгенерирую PPK — строку из 64 случайных символов ASCII c энтропией 330 бит: при аутентификации IKEv2 смешает её идентификатор с результатом ML-KEM. Сочетать последний с PPK — скорее перебор: в усилении классических криптосистем эти инструменты, хоть и не равноценны, самодостаточны, но мой наниматель — пессимист. PPKs появились в strongSwan раньше — в версии 5.7.0, поэтому в отсутствие ML-KEM можно взять их и обменяться ключом с помощью X25519.
bob ~ echo 0x$( dd if=/dev/urandom count=32 bs=1 status=none | xxd -p -c 32 )
0xba8bd35f2ae876d60a781ff19b46580ac0b31c77ae27487324a131dc690a836e
Перехожу к настройке приложения. Меня интересуют два файла: и strongswan.conf
. swanctl/swanctl.conf
Отсутствие опции auth
в директиве remote
последнего означает, что пиры аутентифицируют друг друга дефолтным методом pubkey
. Указывать префикс ppk
в secrets
, наоборот, обязательно, а id
должен соответствовать --san
сертификата. Другая важная штука — селекторы трафика local_ts
и remote_ts
в директиве children
, которые отвечают за маршрутизацию: сервер ожидает из туннеля пакеты, адресат которых входит в префикс его local_ts
— у клиента тот же префикс будет в remote_ts
. Конфиг ниже выдаёт клиенту адрес из 10.0.2.0/23
и туннелирует трафик к 10.0.0.0/22
и 5.6.7.8/32
.
bob ~ sudo mv /etc/swanctl/swanctl.conf{,.bak} 2> /dev/null
bob ~ sudo sed '1d;s/^ //' <<< "
connections {
remote-access {
version = 2
send_certreq = no
pools = home-office
ppk_required = yes
proposals = camellia256ctr-sha512-mlkem768
local {
id = bob.com
certs = bob-cert.pem
}
remote {}
children {
office {
local_ts = 10.0.0.0/22, 5.6.7.8/32
esp_proposals = chacha20poly1305-mlkem768
updown = _updown iptables
}
}
}
}
pools {
home-office {
addrs = 10.0.2.0/23
}
}
secrets {
ppk-alice {
id = alice@bob.com
secret = 0xba8bd35f2ae876d60a781ff19b46580ac0b31c77ae27487324a131dc690a836e
}
}" > /etc/swanctl/swanctl.conf
proposals
и esp_proposals
позволяют захардкодить набор алгоритмов шифрования, аутентификации, проверки целостности и обмена ключами. Хардкод — палка о двух концах: обе опции можно опустить, но возникает риск того, что сервер согласится на слабый алгоритм клиента. С другой стороны, если набор слишком строгий и короткий, клиенту, который его не поддерживает, вернётся ошибка аутентификации. На деле чем разнообразнее клиенты, тем разнообразнее должен быть набор — но я не оставляю пирам выбора: обменяться ключом им придётся по mlkem768
.
Очередь : strongswan.conf
bob ~ sudo mv /etc/strongswan.conf{,.bak} 2> /dev/null
bob ~ sudo sed '1d;s/^ //' <<< "
charon {
load_modular = yes
install_routes = no
plugins {
socket-default {
use_ipv6 = no
}
include strongswan.d/charon/*.conf
}
}
include strongswan.d/*.conf" > /etc/strongswan.conf
Запускаю службу и добавляю её в автозагрузку. strongswan-starter
— артефакт метапакета strongswan
на Debian, который задействует легаси-интерфейс приложения.
bob ~ sudo systemctl disable --now strongswan-starter 2> /dev/null;
sudo systemctl enable --now strongswan
Клиент
Одноразовой ссылкой передаю сотруднице сертификат сервера, сертификат и ключ клиента, клиентские swanctl.conf
и strongswan.conf
:
alice ~ cat /etc/swanctl/swanctl.conf
connections {
remote-access {
remote_addrs = 1.2.3.4
vips = 10.0.2.0/23
send_certreq = no
ppk_required = yes
ppk_id = alice@bob.com
proposals = camellia256ctr-sha512-mlkem768
local {
certs = alice-cert.pem
}
remote {
id = bob.com
}
children {
office {
remote_ts = 10.0.0.0/22, 5.6.7.8/32
start_action = start
esp_proposals = chacha20poly1305-mlkem768
}
}
}
}
secrets {
ppk-alice {
id = alice@bob.com
secret = 0xba8bd35f2ae876d60a781ff19b46580ac0b31c77ae27487324a131dc690a836e
}
}
alice ~ cat /etc/strongswan.conf
charon {
load_modular = yes
half_open_timeout = 30
plugins {
include strongswan.d/charon/*.conf
}
}
include strongswan.d/*.conf
В strongswan.conf
клиента отсутствует install_routes = no
, поэтому демон выделит свои маршруты в локальную таблицу 220. На сервере я отказался от неё ради производительности.
Сертификаты и ключ на клиенте:
alice ~ ( cd /etc/swanctl && ls private x509 )
private/:
alice-key.pem
x509/:
alice-cert.pem bob-cert.pem
Сотрудница подтверждает, что на домашнем роутере разрешён IPsec, и запускает strongswan.service
— на Arch служба одна. В логе сервера:
bob ~ journalctl -fu strongswan | grep -E '(select|establish)'
Feb 26 08:16:38 bob.com charon-systemd[693470]: selected proposal: IKE:CAMELLIA_CTR_256/HMAC_SHA2_512_256/PRF_HMAC_SHA2_512/ML_KEM_768
Feb 26 08:16:38 bob.com charon-systemd[693470]: selected peer config 'remote-access'
Feb 26 08:16:38 bob.com charon-systemd[693470]: IKE_SA remote-access[1] established between 1.2.3.4[С=RU, O=Bob LLC, CN=bob.com]...9.10.11.12[С=RU, O=Bob LLC, CN=alice@bob.com]
Feb 26 08:16:38 bob.com charon-systemd[693470]: selected proposal: ESP:CHACHA20_POLY1305/NO_EXT_SEQ
Feb 26 08:16:38 bob.com charon-systemd[693470]: CHILD_SA remote-access{1} established with SPIs c04da127_i cad1369a_o and TS 10.0.0.0/22, 5.6.7.8/32 === 10.0.2.1/32
Статус физического интерфейса и таблицы 220 на клиенте:
alice ~ ip -4 -br a s dev wlo0 && ip r l t 220
wlo0 UP 192.168.0.2/24 metric 20 10.0.2.1/32
10.0.0.0/22 via 192.168.0.1 dev wlo0 proto static src 10.0.2.1
5.6.7.8 via 192.168.0.1 dev wlo0 proto static src 10.0.2.1
throw 192.168.0.0/24 proto static
throw 192.168.0.1 proto static
Готово. Осталось автообновление сертификатов — но об этом пусть заботится будущий, постквантовый я.
Автор: jhoag