← Усі статті

Transparent Gateway · Proxmox · cloud-init · DevOps

Розгортаю Transparent Gateway у Proxmox: cloud-init, systemd і перевірка з нуля

14 вересня 2026 р. · 16 хв читання

Ізометрична схема розгортання Transparent Gateway: чиста VM проходить налаштування, стає шлюзом і передає трафік до зовнішнього сервісу після перезавантаження.

Попередній аудит Transparent Gateway закінчився незручним висновком: gateway працював, але шлях від чистого Debian до запущеного застосунку залишався в історії команд і файлах однієї VM. У цій статті перевірив, чи можна пройти цей шлях ще раз — без ручного втручання по дорозі.

Забігаючи наперед: нова VM стала повністю готовою за 189,44 с від початку створення. Після перезавантаження systemd повернув gateway у робочий стан за 52,47 с. Клієнтська VM знову пройшла DNS, TCP, локальний і публічний echo-сервіси. Між стартом сценарію та фінальним PASS нічого не виправляв вручну.

Як завжди, до працюючого результату дійшов не з першої спроби. Cloud-init спіткався об застарілий формат користувача, apt — об назву пакета, а суворий проксі знайшов стару помилку у запиті CONNECT.

Що саме має відтворитися

Скопіювати диск недостатньо. Нова машина повинна отримати мережі, Docker і код застосунку, окремо прийняти секрети, запустити gateway через systemd та провести клієнтський трафік тим самим маршрутом. Якщо будь-який із цих кроків потребує ручного введення команди, задача ще не закрита.

Клієнтська VM не залишилася без інтернету — прибрав лише можливість обійти gateway. Її єдиний маршрут IPv4 за замовчуванням веде до Transparent Gateway, а вже він передає дозволений трафік далі через проксі. Тому успішний запит до публічного сервісу не міг випадково піти напряму. IPv6 у цьому стенді не перевіряв.

Сам маршрут перевіряв у два кроки. Спочатку запит ішов через gateway і локальний тестовий проксі до локального echo-сервісу. Така коротка петля не залежить від зовнішнього DNS, TLS, Cloudflare чи інтернет-каналу, тому окремо перевіряє перехоплення трафіку, роботу relay, авторизацію на проксі та повернення відповіді клієнту. Лише після цього додав зовнішній TLS-проксі й публічний echo-сервіс. Другий крок уже перевірив реальний вихід в інтернет, TLS і зовнішню IP-адресу.

Щоб не змішувати керування gateway, клієнтський трафік і локальну перевірку, призначив три мережеві інтерфейси. eth0 веде до мережі керування 192.168.88.0/24, eth1 приймає трафік клієнта через 192.168.91.1/24, а eth2 з адресою 10.10.20.2/24 підключений до ізольованої лабораторної мережі з тестовими проксі та echo-сервісом. Клієнт має адресу 192.168.91.2/24 і лише один маршрут IPv4 за замовчуванням — через gateway. Сценарій і перевірки запускаються з окремої машини, тому gateway не перевіряє сам себе.

Етап 1. Чистий шаблон у Proxmox

Для чесного тесту знадобилася машина без Docker-образів, Redis і слідів попередніх запусків. Щоб не збирати її основу вручну через інтерфейс Proxmox, додав до репозиторію скрипт create-clean-template.sh. Він запускається від root на хості Proxmox, завантажує Debian 13 genericcloud збірки 20260831-2587, перевіряє контрольну суму й цілісність qcow2, додає диск cloud-init і перетворює VM на чистий шаблон. На стенді новий шаблон отримав номер 9001.

Перед створенням першої VM на Proxmox потрібно підготувати два ізольовані мережеві мости, сховище для файлів cloud-init і чистий шаблон. Ці дії автоматизував трьома власними скриптами. prepare-lab-bridges.sh створює мости, create-clean-template.sh готує шаблон, а clone-stage1.sh робить із нього першу VM, додає три мережеві інтерфейси, генерує початковий cloud-init і запускає машину. Дві команди між ними створюють сховище для cloud-init. Увесь блок виконується один раз від root на хості Proxmox. На моєму стенді файли лежали в /root/tg-rollout; це лише робочий каталог, який можна замінити власним шляхом:

Bash-код
cd /root/tg-rollout
bash prepare-lab-bridges.sh
bash create-clean-template.sh
pvesm add dir tg-snippets --path /var/lib/vz/tg-snippets --content snippets --nodes pve
install -d -m 0750 /var/lib/vz/tg-snippets/snippets
SSH_PUBLIC_KEYS=/root/tg-rollout/approved-keys.pub bash clone-stage1.sh

На першій VM 211 з’явилася проблема сумісності: створений Proxmox запис user: tgadmin викликав попередження про застарілий формат. SSH і мережа працювали, але cloud-init завершився зі станом degraded done і кодом 2. У генераторі конфігурації замінив одного користувача на список users і створив нову VM 212.

Показати код
extended_status: degraded done
DEPRECATED: 'user' of type string is deprecated.
Use 'users' list instead.
cloud-init exit code: 2
Hardware VM 212 у Proxmox: cloud-init drive, диск 24 ГБ і три мережеві адаптери для management, клієнтської та лабораторної мереж.
Виправлена VM 212: 2 vCPU, 2 ГіБ пам’яті, диск 24 ГіБ, cloud-init і три мережеві інтерфейси.

Для кожної нової VM Proxmox генерує власні MAC-адреси та мережеву конфігурацію, яка прив’язує їх до eth0, eth1 і eth2. Файл з попередньої машини не копіював. Усередині VM перевірив версію Debian, роботу cloud-init, MAC-адреси, IPv4 та маршрути:

Bash-код
hostname
. /etc/os-release
printf '%s\n' "$PRETTY_NAME"
sudo cloud-init status --long
ip -br link
ip -br -4 address
ip -4 route
Показати код
Debian GNU/Linux 13 (trixie)
cloud-init: done (exit code 0)

eth0  UP  192.168.88.114/24
eth1  UP  192.168.91.1/24
eth2  UP  10.10.20.2/24
default via 192.168.88.1 dev eth0

PASS absent: /opt/transparent-gateway
PASS absent: /etc/transparent-gateway
PASS absent: /var/lib/docker
PASS absent: /var/lib/redis

Етап 2. Cloud-init готує хост

Чистий шаблон не знав нічого про конкретний gateway. Після створення VM початкове налаштування виконав cloud-init: задав ім’я хоста, створив користувача tgadmin, додав дозволені відкриті SSH-ключі, налаштував три мережі й установив Docker. Робочі секрети в user-data не додавав — передав їх окремо на наступних етапах.

Повний user-data для кожної VM створює render-stage2-user-data.py із файлів репозиторію, імені машини та дозволених SSH-ключів. Нижче показав лише частину, яка встановлює пакети й запускає підготовку хоста. Мережеву конфігурацію Proxmox генерує окремо для конкретних інтерфейсів VM:

Показати код
#cloud-config
hostname: tg-repro-05
ssh_pwauth: false
disable_root: true
users:
  - name: tgadmin
    groups: [sudo]
    shell: /bin/bash
    lock_passwd: true
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
package_update: true
package_upgrade: true
package_reboot_if_required: false
packages:
  - ca-certificates
  - curl
  - git
  - python3
  - iproute2
  - iptables
  - bind9-dnsutils
  - docker.io
  - docker-compose
  - qemu-guest-agent
runcmd:
  - [systemctl, daemon-reload]
  - [systemctl, enable, --now, docker.service]
  - [systemctl, start, qemu-guest-agent.service]
  - [systemctl, start, tg-bootstrap.service]

На VM 213 підготовка зупинилася на APT. Cloud-init завершився з кодом 1, тому окремо перевірив доступність обох пакетів і знайшов відповідний рядок у журналі. dnsutils не мав доступної версії, тоді як потрібний dig у Debian 13 входить до bind9-dnsutils. Скорочений результат перевірки:

Показати код
status: error
cloud-init exit code: 1

dnsutils:
  Installed: (none)
  Candidate: (none)

bind9-dnsutils:
  Installed: (none)
  Candidate: 1:9.20.26-1~deb13u1

apt.py[DEBUG]: The following packages were not found by APT:
['dnsutils']

У списку пакетів замінив dnsutils на bind9-dnsutils і створив ще одну чисту VM — 214. На ній cloud-init завершився з кодом 0. Docker установився у версії 26.1.5+dfsg1, Compose — 2.26.1-4. Вбудована в cloud-init перевірка підготовки хоста звірила Debian 13, пакети, три мережеві інтерфейси, єдиний маршрут за замовчуванням через мережу керування та контрольну суму власного коду. Стан HOST_READY означав лише те, що хост готовий прийняти застосунок. Сам gateway ще не розгортав.

Успішний bootstrap tg-repro-04: cloud-init done, Docker і Compose встановлено, три NIC перевірено, результат HOST_READY.
VM 214: cloud-init і підготовка хоста завершилися успішно; Docker, Compose, QEMU guest agent та мережі готові.

Етап 3. Розгортання зафіксованої версії

Для наступного кроку в репозиторії є deploy/deploy.py. Після доставки зафіксованої версії коду цей скрипт запускається від root уже на новій VM. Він перевіряє Debian, Docker, каталоги й мережеві інтерфейси, звіряє отриманий коміт і конфігурацію, виконує docker compose config --quiet, збирає образи, запускає контейнери та чекає локальної готовності gateway. Якщо критичний крок падає, скрипт завершується з ненульовим кодом і не друкує секрети.

Після підготовки хоста запустив розгортання такою командою, передав із локального репозиторію в перевіреному Git bundle через SSH:

Bash-код
sudo python3 /var/lib/transparent-gateway/source/deploy/deploy.py \
  --source /var/lib/transparent-gateway/source \
  --commit af939c34587dafc3e76007cf06fe6ee2012e45d2 \
  --lab-fixture --timeout 300

Прапорець --lab-fixture додає локальний echo-сервіс і проксі з авторизацією. Це окремий режим для стенда. Сам застосунок складається із шести контейнерів плюс два тестових.

Показати код
CONFIG_FILES_CHANGED 0
CONTAINERS created_or_replaced=0 unchanged=6
POOL_COUNTS {"active": 1, "raw": 1, "reserve": 1, "validated": 1}
LOCAL_READY elapsed=4.5s; client/DNS/public checks pending

Успішний запуск — це вже дуже добре, але пити каву ще рано. Тепер перевірю, як машина буде реагувати на перезапуски. Для цього в репозиторії є deploy/verify-repeat.py. Його запускає root на gateway після першого успішного розгортання: скрипт зберігає контрольний стан, повторно викликає розгортання тієї самої версії та порівнює конфігурацію, контейнери, Redis, правила iptables і маршрути. На VM 214 усе залишилося без змін. На цьому етапі в пулі ще був один локальний проксі. Остаточну конфігурацію з двома проксі перевірив пізніше, коли створював gateway з нуля:

Показати код
PASS same 6 application and 2 fixture containers
PASS Redis process and data preserved
PASS config bytes/inodes/mtime/permissions unchanged
PASS iptables and policy routing unchanged
IDEMPOTENT_REPEAT PASS

Етап 4. Секрети окремо від Git і cloud-init

Код можна безпечно забрати з Git, але паролі проксі й токени туди класти не варто. Тому шість конфігураційних файлів виніс в окремий набір і передав незалежно від коду. На gateway вони зберігаються в /etc/transparent-gateway/runtime. Каталог належить root і має права 0700. Файл .env має права 0600; решта файлів доступна потрібним контейнерам лише на читання через групу 65532.

ФайлЩо містить
.envПараметри запуску без пароля проксі й токена провайдера
inventory/connectors.jsonДозволені джерела списків проксі
inventory/upstreams.jsonlАдреси проксі та посилання credential_ref
secrets/proxy-credentials.jsonІм’я користувача, пароль проксі та посилання на CA
secrets/upstream-ca.crtВідкритий сертифікат CA для перевірки TLS-проксі
provider-secrets/api-tokenОкремий файл із токеном провайдера; у цьому тесті — тестове значення

Для доставки секретів додав скрипт deploy/secret-transfer.py. Запускаю його на керуючій машині, а він через SSH передає файли на gateway і перевіряє права доступу.

Новий набір замінює попередній лише після успішної перевірки. Якщо з’єднання обірветься, старі файли залишаться на місці — достатньо повторити передачу.

Після доставки перевірив, хто бачить кожен секрет. Токен провайдера доступний лише fetcher, а дані проксі — validator і gateway. Файли підключені до контейнерів лише для читання.

Щоб не перевіряти це вручну, додав deploy/verify-secrets.py. Запускав його на gateway від root. Скрипт звіряє права, доступ контейнерів і авторизацію на проксі.

Окремо він шукає відомі облікові дані у відкритому та закодованому вигляді: у репозиторії, cloud-init, журналах, параметрах Docker і Redis. Саму систему ця перевірка не змінює.

SECRETS_CHECK PASS: права файлів і монтування лише для читання перевірено, тестові credentials відсутні у Git, cloud-init та журналах.
Перша перевірка секретів на VM 214: правильні права, підключення лише на читання та жодного відомого тестового секрету у перевірених місцях.

На VM 214 перевірка завершилася результатом SECRETS_CHECK PASS. Права доступу й підключення файлів лише для читання виявилися правильними.

Відомих тестових значень у перевірених місцях скрипт не знайшов. Але це не гарантує, що в системі немає іншого витоку. API справжнього провайдера на цьому етапі не викликав.

Етап 5. Systemd керує запуском

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

Тому для всіх восьми контейнерів указав restart: no, а запуск передав systemd. Він чекає Docker і network-online.target, виконує попередні перевірки та повідомляє READY=1 лише після готовності gateway. Unit-файл із репозиторію:

Показати код
[Unit]
Description=Transparent Gateway application
Wants=network-online.target
Requires=docker.service
After=network-online.target docker.service
PartOf=docker.service
StartLimitIntervalSec=600
StartLimitBurst=5

[Service]
Type=notify
NotifyAccess=main
ExecStartPre=/usr/bin/python3 /opt/transparent-gateway/deploy/service.py check
ExecStart=/usr/bin/python3 /opt/transparent-gateway/deploy/service.py serve
ExecStopPost=/usr/bin/python3 /opt/transparent-gateway/deploy/service.py stop
Restart=on-failure
RestartSec=15
TimeoutStartSec=420
TimeoutStopSec=90
UMask=0077
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target docker.service

У цьому unit-файлі deploy/service.py — програма, якою systemd керує gateway. Перед запуском вона звіряє версію коду, мережеві інтерфейси, конфігураційні файли та результат docker compose config.

Під час завантаження системи образи не збираються і не завантажуються: systemd запускає вже підготовлені контейнери. Type=notify дозволяє йому відрізнити запущений процес від готового gateway.

Unit потрібно було встановити й увімкнути на gateway. Щоб не копіювати його вручну, додав до репозиторію deploy/install-service.py. Після першого успішного розгортання запустив його на gateway від root.

Перед установленням він перевіряє службу очікування мережі та фіксує потрібну версію коду. Команда була такою:

Bash-код
sudo python3 /var/lib/transparent-gateway/source/deploy/install-service.py \
  --commit af939c34587dafc3e76007cf06fe6ee2012e45d2 \
  --lab-fixture

Першу перевірку після перезавантаження виконав ще з локальним проксі. У журналі з’явилися правильний порядок запуску та BOOT_LOCAL_READY.

Зовнішню мережу на цьому кроці ще не перевіряв.

Локальна перевірка reboot: systemd автоматично запустив gateway після Docker і мережі, Redis та secrets збереглися.
Перша перевірка після перезавантаження VM 214: systemd дочекався залежностей, зберіг Redis і секрети та записав BOOT_LOCAL_READY.

Етап 6. Локальна перевірка маршруту

Стан Up біля контейнера доводить лише те, що процес запущений. Він нічого не говорить про придатність проксі та весь маршрут клієнтського запиту.

Тому перед першим запитом чекер звіряє чотири пули — raw, validated, reserve та active. Вони мають бути свіжими, походити з останнього циклу validator і містити придатний проксі. Повторне ранжування старих даних не повинно робити їх «свіжими».

Спочатку перевірив лише локальну частину маршруту. Клієнтська VM надіслала запит через gateway і тестовий проксі до echo-сервісу в ізольованій мережі. Зовнішній DNS, TLS і Cloudflare тут не брали участі, тому можливу помилку довелося б шукати всередині стенда. Запит

повернувся успішно. Отже, gateway перехопив трафік, relay передав його проксі, авторизація спрацювала, а відповідь повернулася клієнту.

Вихід в інтернет цей тест ще не перевіряв: зовнішнього проксі на цьому етапі
не було. Публічний HTTPS і DNS-over-HTTPS залишилися для наступного кроку

LAB_READINESS PASS: свіжі пули upstream, маршрут клієнта через gateway і локальний transparent TCP echo; публічні перевірки ще попереду.
Перший LAB_READINESS PASS: локальні TCP та echo працюють через gateway.

LAB_READINESS PASS підтвердив лише внутрішню частину маршруту. Вихід в інтернет, TLS і DNS-over-HTTPS ще залишалися неперевіреними.

Для наступного кроку знадобилися дві зовнішні точки. Публічний echo-сервіс мав показати вихідну IP-адресу, а TLS-проксі — дати gateway контрольований шлях до нього.

Етап 7. Публічний echo-сервіс і окремий TLS-проксі

Почав із публічної точки призначення. Для неї створив невеликий echo-сервіс і виніс його в окремий репозиторій TransparentGatewayEcho. Сервіс працює і зараз на echo.devdays.net.ua.

У відповіді він повертає визначену ним IP-адресу клієнта, дозволені діагностичні заголовки та новий ідентифікатор запиту. Приймає лише GET / без параметрів і тіла та обмежує частоту запитів.

Публічний echo-сервіс налаштував через наявний Nginx Proxy Manager. Він приймає HTTPS-запити на порту 443 і передає їх контейнеру tg-public-echo на порт 8080 у внутрішній мережі Docker. Порт 8080 назовні не відкритий.

Показати код
Cloudflare → Nginx Proxy Manager:443 → tg-public-echo:8080

У Nginx Proxy Manager вказав схему http, ім’я tg-public-echo і порт 8080. Налаштування заголовків та обмежень виніс у файл npm-advanced.conf у репозиторії й додав його в поле Advanced.

Після цього на машині з NPM оновив код і перезапустив сервіс:

Bash-код
git pull --ff-only
docker compose -f compose.npm.yaml config --quiet
docker compose -f compose.npm.yaml up -d --build --wait --remove-orphans

До підміни адреси Nginx Proxy Manager записує адресу TCP-клієнта у службовий заголовок X-TG-Peer. Для цього використовується $realip_remote_addr.

Echo-сервіс довіряє CF-Connecting-IP лише для запитів із мереж Cloudflare. Інакше зовнішню IP-адресу можна було б підставити власноруч.

Nginx Proxy Manager тут не обов’язковий: використав його, бо він уже працював на віддаленій машині з echo-сервісом.

Фільтрація Cloudflare

Для перевірки публічного echo-сервісу додав до репозиторію TransparentGatewayEcho файл verify.py. Запускав його з кореня репозиторію. Скрипт перевіряє HTTPS-сертифікат, формат JSON, унікальність ідентифікатора запиту та обмеження частоти звернень. Також стежить, щоб у відповідь не потрапили cookies або заголовки авторизації.

Перший запуск завершився помилкою JSONDecodeError. Спочатку це виглядало як неправильна відповідь echo-сервісу, але запит до нього взагалі не дістався. Cloudflare відхилив стандартний User-Agent бібліотеки Python і повернув замість JSON власну HTML-сторінку з HTTP 403 та кодом 1010.

Замінив User-Agent на TG-Echo-Verify/1.0 і додав до діагностики HTTP-статус та тип відповіді. Після цього запит дістався echo-сервісу й повернув очікуваний JSON.

Окремо навмисно надіслав власне значення CF-Connecting-IP. Цього разу HTTP 403 був очікуваним: правило Cloudflare розпізнало спробу підмінити адресу й не пропустило запит далі. Echo-сервіс знову не брав участі в обробці, тому його поведінку при прямому отриманні такого заголовка цей тест не перевірив.

Bash-код
python3 verify.py https://echo.devdays.net.ua/ --rate-test
PUBLIC_ECHO_CHECK PASS: HTTPS, request ID, безпечні заголовки та rate limit HTTP 429 перевірено; зовнішню IP приховано.
Публічний echo-сервіс: TLS, ідентифікатор запиту, захист відповіді та обмеження частоти HTTP 429 пройшли перевірку; зовнішню IP-адресу приховано.

Echo-сервіс записує в журнал лише ідентифікатор запиту та статус відповіді.

Cloudflare приховує адресу сервера у звичайній DNS-відповіді. Але echo-сервіс за призначенням показує адресу, з якої прийшов клієнтський запит.

Власний проксі для повної перевірки

Публічний echo-сервіс показав, звідки прийшов запит. Але він не міг перевірити обмеження самого зовнішнього проксі.

Для повного циклу перевірки створив контрольований TLS-проксі й виніс його в окремий проєкт — TransparentGatewayProxy. Він приймає CONNECT на TCP-порту 3129 лише з дозволених адрес і лише до визначених цілей.

Gateway підключається до цього проксі напряму, без Nginx Proxy Manager і Cloudflare. Перевірка на зовнішньому сервері підтвердила TLS, відмову без правильних даних, заборону сторонніх адрес і HTTPS-запит до публічного echo-сервісу.

Docker-контейнер TLS proxy healthy; локальні перевірки TLS, автентифікації, заборонених цілей і public echo пройшли, зовнішню IP приховано.
Зовнішній проксі працює; PROXY_LOCAL_CHECK PASS. Клієнтський маршрут через gateway на цьому кроці ще не перевірений.

Для перевірки з боку gateway використав verify.py із репозиторію TransparentGatewayProxy. На gateway він лежить як /usr/local/sbin/tg-verify-external-upstream. Скрипт перевіряє TLS, відмову без правильних даних, публічний echo та DNS-over-HTTPS, але не змінює конфігурацію проксі:

Bash-код
sudo python3 /usr/local/sbin/tg-verify-external-upstream \
  --directory /etc/transparent-gateway/external-upstream \
  --remote --dns
Захищені файли upstream з правами 0700 і 0600; перевірки TLS proxy, public echo та DNS-over-HTTPS із gateway завершилися UPSTREAM_REMOTE_CHECK PASS.
Gateway дістався зовнішнього TLS-проксі: права переданих файлів, публічний echo і DNS-over-HTTPS перевірені, UPSTREAM_REMOTE_CHECK PASS.

Публічний проксі має доступ лише до моїх контрольованих цілей. Водночас дозвіл IP-адреси спільної CDN не гарантує потрібний домен: проксі не аналізує SNI всередині CONNECT.

Локальний тестовий проксі бачить тільки локальний echo-сервіс. У фінальному активному пулі є обидва проксі, а relay вибирає той, що придатний для конкретної цілі.

Проксі справний. Validator так не вважає

На цьому етапі в пулі було два проксі. Локальний тестовий проксі вів лише до echo-сервісу всередині стенда, а щойно розгорнутий TLS-проксі мав забезпечити вихід до публічного echo-сервісу.

Локальний проксі пройшов перевірку, а зовнішній — ні. Validator формував запит CONNECT echo.devdays.net.ua HTTP/1.1 без номера порту. Суворий зовнішній проксі очікував CONNECT echo.devdays.net.ua:443 HTTP/1.1 і правильно відхилив неправильний запит.

Через відхилений CONNECT зовнішній проксі не пройшов перевірку й не потрапив до активного пулу. У ньому залишився лише локальний проксі. Внутрішній маршрут працював, але дістатися публічного echo-сервісу gateway не міг, тому повна перевірка готовності завершилася помилкою.

Виправив формування адреси: порт тепер указується явно, а IPv6 записується у квадратних дужках. Старий модульний тест очікував неправильний рядок, тому виправив і його та додав перевірку від повторення помилки.

Після цього залишалося перевірити весь маршрут. Для цього в основному репозиторії є deploy/verify-readiness.py. Запускав його із параметром --public; через SSH він опитує gateway і виконує свіжий запит із клієнтської VM.

Перевірка охопила DNS через UDP, TCP-з’єднання, обидва echo-сервіси, TLS і вихідні IP-адреси. Три сторонні UDP-пакети збільшили лічильник DROP, отже обійти дозволений DNS-маршрут не вдалося. Повна перевірка нарешті пройшла:

FULL_CLIENT_READINESS PASS: свіжі пули з двома upstream, DNS, TCP, local і public echo через gateway та блокування прямого non-DNS UDP.
FULL_CLIENT_READINESS PASS на VM 214: по два проксі в пулах, DNS, TCP, локальний і публічний echo та блокування стороннього UDP.

Етап 8. Перевірка після перезавантаження

Один успішний запуск ще не доводить, що gateway переживе перезавантаження. Перед ним зберіг контрольний стан VM 214: ідентифікатор завантаження, версію коду, ідентифікатори контейнерів, конфігурацію та окремий запис у Redis.

Потім справді перезавантажив VM. Застосунок і Docker Compose вручну не запускав.

Докази потрібно було зібрати і з gateway, і з клієнтської VM. Щоб не робити це вручну, додав до репозиторію deploy/verify-full-boot.py. Скрипт можна запустити з будь-якого комп’ютера, що має SSH-доступ до обох машин.

Через SSH скрипт перевіряє нове завантаження та автоматичний запуск gateway. Потім із VM 215 повторює DNS, TCP і запити до обох echo-сервісів. Систему він не змінює:

Bash-код
python3 deploy/verify-full-boot.py
FULL_REBOOT_CHECK PASS: автоматична готовність за 52,53 с після reboot, збереження Redis і secrets та успішні DNS, TCP, local і public echo з клієнта.
VM 214 після перезавантаження: автоматична готовність за 52,53 с, Redis, конфігурація й секрети збережені, FULL_REBOOT_CHECK PASS.

Спочатку скрипт отримав REBOOT_CHECK PASS: gateway автоматично запустився, але клієнтські перевірки ще тривали. Після DNS, TCP і обох echo-сервісів з’явився фінальний FULL_REBOOT_CHECK PASS.

Тепер залишалося складніше питання: чи повториться цей результат на новій VM, а не лише після перезавантаження вже підготовленої машини.

Етап 9. Створення gateway з чистого шаблону

Перезавантаження довело, що вже підготовлена VM повертається до роботи. Але чи зможу отримати такий самий gateway без її диска, контейнерів і Redis?

Для цієї перевірки додав до репозиторію сценарій deploy/proxmox/recreate-lab.py. Через SSH він керує Proxmox, старим gateway, новою VM та клієнтом.

Сценарій зберіг із VM 214 лише шість конфігураційних файлів, вимкнув її та створив нову VM із чистого шаблону. Старий диск, образи Docker, томи, Redis і ключі SSH не переносилися.

Сценарій отримав новий ключ SSH через QEMU guest agent по вже довіреному з’єднанню з Proxmox. Після цього підключився до нової адреси із суворою перевіркою ключа.

Cloud-init завершився без помилок. До передачі коду й секретів сценарій перевірив, що на VM немає застосунку, конфігурації, контейнерів, образів і томів Docker.

Лише після цього він доставив секрети, розгорнув gateway, налаштував systemd і запустив клієнтські перевірки та пошук витоків. Після перезавантаження всі перевірки повторилися.

Сценарій запускається від root на довіреній машині з SSH-доступом до всіх вузлів стенда. З кореня репозиторію команда виглядає так:

Bash-код
sudo python3 deploy/proxmox/recreate-lab.py \
  --evidence "$PWD/evidence/stage9"

Сценарій прив’язаний до цього стенда: VM 214, нова VM 216, клієнт 215 і шаблон 9001. На іншому стенді потрібно змінити адреси, номери VM та ключі.

Щоб випадково нічого не затерти, сценарій відмовляється працювати з уже наявною VM або каталогом результатів. Після завершення безпечно повторювати лише окрему перевірку, яка не змінює систему.

ВимірюванняФактичний результат
Початок сценарію9 вересня 2026, 09:14:43 UTC
Створення VM та її запуск20,81 с
Cloud-init і підготовка хоста від початку створення VM97,63 с
Розгортання застосунку78,27 с
Перша повна готовність від початку створення VM189,44 с
Готовність нової VM після перезавантаження52,47 с
Повний сценарій разом із перевірками й перезавантаженням366,31 с
Ручні виправлення після старту сценарію0

Шаблон залишився незмінним, а нова VM отримала інший machine-id. Після перезавантаження збереглися контейнери, запис у Redis, конфігурація, секрети й сертифікат проксі.

Пули залишалися свіжими. DNS, TCP та обидва echo-сервіси відповіли з новими ідентифікаторами запитів. Операції Proxmox теж завершилися без помилок:

Показати код
VM 214  Shutdown  OK
VM 9001 Clone     OK
VM 216  Start     OK
VM 216  Reboot    OK

Сценарій не повинен сам собі видавати довідку про успіх. Тому додав незалежну перевірку deploy/verify-recreate.py і запустив її після створення нової VM.

Скрипт читає збережені результати, звіряє нову VM, стан старої машини й незмінність шаблону. Потім ще раз перевіряє завантаження та весь клієнтський маршрут. Нічого в системі він не змінює:

Bash-код
sudo python3 deploy/verify-recreate.py \
  --evidence "$PWD/evidence/stage9"
Показати код
TEMPLATE 9001 -> NEW VM 216 / tg-repro-05 / 192.168.88.116
PASS fresh machine identity; empty application state before deployment
PASS first boot: cloud-init, deployment, systemd, secrets and client DNS/TCP/local/public echo
TIMING clone=20.81s | deployment=78.27s | first-ready=189.44s
BOOT 2026-09-09 09:19:41 | READY 52.47s after boot
FULL_CLIENT_READINESS PASS
FULL_REBOOT_CHECK PASS
RECREATE_CHECK PASS

Каталог результатів доступний лише root. Усередині є копія робочої конфігурації, тому публікувати його цілком не можна. До статті потрапив лише безпечний скорочений результат.

Що тепер відтворюється — і чого тест ще не доводить

Тепер шлях від чистого Debian до робочого Transparent Gateway відтворюється кодом. Нова VM отримує мережеві налаштування, потрібну версію застосунку й секрети.

Після цього systemd запускає gateway і чекає готовності всього маршруту. Під час послідовного створення чистих VM знайшов помилки з cloud-init, пакетом DNS і запитом CONNECT.

Але це ще не повне відновлення після втрати хоста. Для фінального тесту шість конфігураційних файлів узяв із VM 214.

Щоб пережити зникнення цієї машини, потрібні окрема резервна копія конфігурації та перевірена процедура відновлення. Оновлення токенів і сертифікатів проксі, як і API справжнього провайдера, залишилися поза межами тесту.

Версію застосунку та збірку хмарного образу Debian зафіксував. Версії всіх пакетів APT і базових образів Docker — ні. Тому сценарій дає повторюване розгортання, але не обіцяє побітово однакового результату через рік.

Перевірка готовності також не замінює моніторинг, резервні копії та повний аудит безпеки.

Усі команди й скрипти зібрані в репозиторії TransparentGateway. Але найкращий доказ тут — не документація і не журнал старої VM, а нова VM 216: вона пройшла шлях від чистого шаблону до фінального PASS без ручного втручання.