5 мин чтения#infrastructure#engineering#business

Тринадцать сайтов под 152-ФЗ — и десять минут без боевого контура

Утром в Хабре появилась статья про новую волну проверок РКН — автоматический бот сканирует публичные сайты, штрафы 100–300 тысяч за каждое нарушение. Тактический день перестал быть тактическим.

К вечеру тринадцать сайтов csylabs показывают одну и ту же политику обработки данных, dev-зеркала всех приложений закрыты basicauth'ом, в реестре РКН оператор корректный. По дороге десять минут боевого контура лежали из-за одного знака доллара.

Что хочет бот РКН

Пять штук на каждом публичном сайте:

→ Политика обработки персональных данных → Отдельное Согласие на обработку — не «галочка с политикой», а именно второй документ → Две галочки в формах — по одной под каждый документ, обе пустые по умолчанию, submit заблокирован пока обе не отмечены → Cookie-баннер на первом визите — пока пользователь не дал согласие, аналитика не загружается → Уведомление в реестре РКН, в котором цели и категории совпадают с тем, что сайт реально делает

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

Один пакет — тринадцать сайтов

Собрал @csylabs/legal — workspace-пакет в монорепозитории. Внутри:

OPERATOR константа — единое описание оператора: ООО ЛИИ, ИНН, ОГРН, юр. адрес, ответственный по ст. 22.1 → getScope(app) per-app — какие категории данных собирает каждый сайт, на каких основаниях, сколько хранит → Генераторы текста политики и согласия, render-агностичные — брендированные сайты ставят свой JSX, простые берут стандартный → Компоненты — <ConsentCheckboxes> (две галочки), <CookieBanner> (accept/decline + gate на Umami), validateConsent() для серверной проверки

Идея простая: оператор в реестре один. Если завтра поменяются реквизиты — одна правка в OPERATOR, тринадцать сайтов перерисовываются автоматически.

Тринадцать сайтов. Пять публичных с формами или аналитикой, восемь за авторизацией. На каждом — /legal/privacy. Где формы — /legal/consent и две галочки. Где Umami — cookie-баннер.

vox по дороге окончательно перебрендировал — оператор из личного имени переехал в ООО ЛИИ. Голос и аудио убрал из обрабатываемых данных: LiveKit пересылает в реальном времени, ничего не сохраняет, поэтому это не PD по факту, и в политике не должно быть.

Caddy basicauth на dev-зеркалах

Логика простая: бот РКН сканит публичные сайты, dev-зеркала — с недописанной политикой и тестовыми данными — не должны быть видны.

(dev_basicauth) {
    basicauth {
        {$CADDY_DEV_USER} {$CADDY_DEV_PASS_HASH}
    }
}

Snippet в Caddyfile, import dev_basicauth в каждом dev-блоке. Креды через env-переменные из .env, значение пароля — bcrypt-хеш через caddy hash-password.

Что пошло не так

PR смерджил. GitHub Actions отработал штатно: собрал контейнеры, обновил Caddyfile, перезаписал .env, рекреейтнул Caddy. Через минуту — все публичные сайты лежат. Caddy в петле перезапусков.

В логах:

parsing caddyfile tokens for 'basicauth':
username and password cannot be empty or missing,
at /etc/caddy/Caddyfile:16
import chain ['(import dev_basicauth)']

Полез в .env:

$ grep "^CADDY_DEV" /opt/csylabs/docker/.env
CADDY_DEV_USER=csydev
CADDY_DEV_PASS_HASH=

Хеш пустой. Хотя в GitHub Secrets лежит правильный $2a$14$FGVR.... Хотя workflow пишет heredoc с 'EOF' — single-quoted, без shell-подстановки.

docker inspect csylabs-caddy --format '{{json .Config.Env}}':

"CADDY_DEV_PASS_HASH=",
"CADDY_DEV_USER=csydev",

Пусто.

Долго не понимал, потом дошло. Когда docker compose читает .env для своих ${VAR} подстановок в YAML — он раскрывает $ shell-стилем по самому значению. Bcrypt-хеш $2a$14$FGVR... для compose выглядит как $2 (нет такой переменной, пусто) + a + $14 (нет, пусто) + $FGVR (нет, пусто). После всех подстановок остаётся «a» или вообще пусто, в зависимости от того, как версия compose обрабатывает вырожденные случаи. У меня дошло как пусто.

Caddy на пустой пароль в basicauth отвечает отказом конфиг-парсера. Restart, отказ, restart. Сайты лежат.

Починка. Обернуть значение в одинарные кавычки:

CADDY_DEV_PASS_HASH='$2a$14$FGVR...'

В одинарных кавычках compose значение не интерполирует, читает как есть. Хеш доезжает до контейнера целиком, basicauth парсится, Caddy поднимается.

На проде поправил руками через SSH — sed по .env, потом docker compose up -d --force-recreate caddy. Сайты вернулись минутой позже. Около десяти минут от первого 502 до возврата 200 на главной.

В workflow поправил отдельным PR'ом — подстановку секрета вынес из общего heredoc'а в отдельный printf с явными одинарными кавычками:

cat > /opt/csylabs/docker/.env << 'EOF'
... остальные секреты ...
CADDY_DEV_USER=${{ secrets.CADDY_DEV_USER }}
EOF
printf "CADDY_DEV_PASS_HASH='%s'\n" '${{ secrets.CADDY_DEV_PASS_HASH }}' \
  >> /opt/csylabs/docker/.env

Урок принят, отдельно: знаков доллара в значениях .env Docker Compose не любит. Я знал это в 2020-м, забыл к 2026-му. В документации compose про это есть — на одной строке посреди раздела про ENV, легко проскочить.

(хотя казалось бы)

Что закрыто на сегодня

/legal/privacy живёт на всех тринадцати поддоменах csylabs, везде один и тот же оператор, одни и те же реквизиты → Публичные сайты с формами — две галочки, серверная проверка, cookie-баннер где есть Umami → vox окончательно под ООО ЛИИ, голос/аудио из обрабатываемых данных вычеркнут → Dev-зеркала за basicauth, бот РКН не видит → Хотфикс воркфлоу: следующий деплой переменные в .env пишет правильно

Что не делал и почему

Не делал DSAR-эндпоинт. По ст. 14 субъект имеет право на доступ/исправление/удаление данных, и достаточно email-канала с 30-дневным SLA — формальная буква закона выполнена. Структурную форму подниму когда пойдут реальные запросы.

Не интегрировал согласие при регистрации в авторизованных приложениях. Внешних оплачивающих клиентов в них пока нет; когда пойдёт онбординг — две галочки в форму better-auth добавятся как пятиминутное изменение.

Не правил пакеты, не перетряхивал зависимости, не лез в Hasura-схему. Цель была — закрыть один регуляторный риск. Подмести всё подряд по дороге — не цель.

Сегодня была одна задача — её и закрыл. С поправкой на знак доллара.

Читать по теме