Тринадцать сайтов под 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-схему. Цель была — закрыть один регуляторный риск. Подмести всё подряд по дороге — не цель.
Сегодня была одна задача — её и закрыл. С поправкой на знак доллара.