Помістіть 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.

Схожі статті