OpenVPN у Docker: контракт виконання, який потрібен мінімальному клієнту
Перевірений образ, один захищений профіль, доступ до TUN і NET_ADMIN. Що отримує контейнер, чого він не повинен отримувати і як довести, що тунель працює.
Помістіть 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.