Поместите VPN-клиент в контейнер, и вы получите одно пространство имён с туннелем внутри, а не хост, который направляет весь свой трафик через VPN. Это отдельные проекты, и именно отношение к ним как к одному приводит к тому, что контейнеры случайно становятся привилегированными. Это руководство охватывает только первый случай: один проверенный образ, один защищённый профиль и клиентский процесс, ограниченный собственным сетевым пространством имён.

Начните с артефакта, который кто-то уже одобрил. Шаги ниже предполагают образ, содержащий исполняемый файл openvpn и полный клиентский профиль, сертификат и ключевой материал которого уже разрешены внутри монтирования. Они не собирают ПО, не выпускают учётные данные и не охватывают среды без root и нативные контейнеры Windows, каждое из которых меняет ответ.

Что контейнер получает, а что нет

Храните профиль и всё, на что он ссылается, в локальном каталоге с ограниченными правами, смонтированном только для чтения. Относительные пути внутри профиля должны разрешаться в пределах этого монтирования; это деталь, которую люди упускают, когда сертификат лежит на каталог выше профиля и контейнер его никогда не видит.

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"]

Этот файл даёт три вещи и не просит ничего больше. Ссылку на образ следует закреплять по дайджесту, если так требует ваша политика развёртывания, а профиль должен быть проверенным, а не копией, которую кто-то отредактировал на месте на хосте.

Только для чтения здесь означает, что контейнер не может изменять ваши файлы на хосте; это не значит, что контейнер не может прочитать смонтированный вами закрытый ключ. В этом весь смысл монтирования, и поэтому права на файлы, владение и то, как секрет попал на хост, по-прежнему важны. Никогда не передавайте пароль в командной строке и не запекайте его в слой образа, потому что и то и другое сохраняется в местах, которые вы позже не сможете очистить.

Возможности, entrypoint и ловушка между ними

Нужны две настройки, и они делают несвязанные вещи. NET_ADMIN позволяет манипулировать интерфейсами и маршрутами в пространстве имён контейнера. Запись устройства /dev/net/tun позволяет процессу вообще открыть устройство туннеля. Выдача одного без другого даёт понятную ошибку, что удобно: отсутствующее устройство TUN отказывает при открытии, тогда как отсутствующая возможность отказывает, когда клиент пытается настроить интерфейс.

Ни то, ни другое не оправдывает privileged: true. Привилегированный контейнер получает все возможности плюс доступ к устройствам, и это гораздо большее предоставление, чем нужно этой нагрузке. справка о привилегиях и возможностях среды выполнения объясняет, насколько далеко простирается это предоставление. Если chart или запись в блоге просит об этом, сначала проверьте, не была ли требованием на самом деле отсутствующая ветка устройства или отсутствующая возможность.

Переопределение entrypoint — это та часть, которая кусает. Прямой вызов openvpn правилен для обычного образа и неправилен для любого продуктового образа, чей стартовый скрипт готовит правила межсетевого экрана, файлы резолвера или учётные данные перед запуском демона. Переопределение этого скрипта пропускает подготовку и оставляет вас отлаживать туннель, поднявшийся в наполовину настроенном пространстве имён.

Запускайте клиент на переднем плане и позвольте ему быть основным процессом контейнера. Процесс, который демонизируется, сразу завершается, контейнер перезапускается в цикле, а нужные вам журналы разбросаны по перезапускам, которые выглядят одинаково.

Проверяйте изнутри пространства имён, а не с хоста

Поднимите стек, подтвердите в журнале, что клиент достиг подключённого состояния, а затем осмотрите пространство имён, которое действительно держит туннель.

docker compose up -d
docker compose logs --tail 40 vpn
docker compose exec vpn ip -br addr
docker compose exec vpn ip route

Список адресов должен показывать интерфейс туннеля с адресом из пула, а таблица маршрутов — продвинутые назначения, указывающие на него. Если интерфейса нет, клиент так и не дошёл до его создания. Если он есть, но ни один маршрут его не использует, искать нужно в push или в параметрах маршрутов.

Затем проверьте важное для вас назначение из того пространства имён, которое должно использовать туннель, с помощью приложения, а не только ping. Разрешение имён — отдельный сбой, который стоит проверять явно: продвинутый DNS-адрес сам не обновляет резолвер контейнера, а автономный клиент, игнорирующий push, будет разрешать имена через то, что настроил образ.

Отключённое состояние — часть контракта

Поведению при переподключении нужна собственная проверка, потому что политика перезапуска доказывает, что процесс перезапускается, и ничего не говорит о том, что делает трафик в промежутке. Остановите туннель, попробуйте предполагаемое назначение и посмотрите, отклоняется ли соединение, зависает или тихо удаётся по обычному маршруту контейнера. Последний исход — тот, что важен, и он невидим в здоровом журнале.

Если нагрузка никогда не должна использовать обычный путь, спроектируйте это явно с помощью правил межсетевого экрана и сигнала готовности внутри пространства имён. Kill switch — это политика, которую вы пишете и тестируете, а не свойство, наследуемое от запуска клиента в контейнере.

Наконец, помните о границе: эти маршруты живут в пространстве имён контейнера и не заменяют таблицу маршрутизации хоста. Правила перенаправления, трансляции и межсетевого экрана Docker остаются частью внешнего пути, а опубликованный порт не является интерфейсом VPN.

Похожие статьи