// статьи / статья
#статья #безопасность#2fa#сессии#бэкапы

> Безопасность без паранойи: 2FA, сессии, секреты и бэкапы

Реальные истории про .env в архивах, dev→prod, «Выйти везде» и что делать, когда страшно.

SkilinPur 25.10.2026 6 мин чтения серия: как это устроено 👁
Безопасность без паранойи: 2FA, сессии, секреты и бэкапы

Я не security-инженер. Но за время работы над сайтом понял: безопасность — это не паранойя и не один большой аудит. Это гигиена, которую делаешь регулярно.

С чего всё началось

Сайт начинался как витрина. Потом появилась админка, вход, роли, статьи, Telegram, 2FA. Чем больше данных и доступа, тем меньше хочется надеяться на «ну меня же никто не знает».

Сейчас в проекте есть:

  • вход по паролю и сессиям;
  • реестр сессий и «Выйти везде»;
  • 2FA через Telegram;
  • TOTP и резервные коды;
  • уведомления о входе;
  • ограничение попыток;
  • CSP, HSTS, nosniff, referrer-policy;
  • блокировка .env, *.db, .git на уровне Caddy;
  • бэкапы без секретов.

Это не «идеальная защита». Но это уже не «сайт на доверии».

Секреты: .env, который попал в архив

Самая показательная история — даже не про код, а про бэкапы.

Долгое время архивы сайта делались «на всякий случай» и включали всё: код, базу, opt/caddy, etc/php. Вместе с этим в архив попадал .env — с ключами Telegram и другими доступами.

Потом выяснилось, что архивы лежат в git-репозитории. Не публичном, но всё равно. Если репозиторий когда-нибудь утечёт или кто-то получит доступ, ключи уйдут вместе с ним.

С бэкапа №117 .env исключён из архива. У более старых бэкапов (№116 и раньше) он внутри. Если репозиторий мог утечь — ключи Telegram стоит ротировать. Это неприятно, но честно.

Урок: бэкап — это тоже публикация. Если архивы уходят в git, относись к ним как к коду, который может стать публичным.

Сейчас .env живёт с правами 600, а Caddy отдаёт 404 на .env, *.db и .git. Но это защита от случайного запроса, а не от того, что файл уже лежит в архиве.

Dev и prod: где доверие ломается

21.10.2026 старый скрипт миграции скопировал базу с dev на prod. Формально — «синхронизация». Фактически — потеря контента: репорты, проекты, участники.

Прод восстановили из архива, dev-базу синхронизировали. Но это был хороший урок: код и контент — разные вещи. Код можно перезаписать. Контент — нет.

Теперь migrate-to-prod.sh по умолчанию переносит только код. Для базы — --with-db, с предварительным сравнением db-compare.php. Для контента — --with-content. Перед любым изменением — бэкап.

Урок: если скрипт может что-то перезаписать, он должен спросить. «Удобно» и «безопасно» — не всегда одно и то же.

Сессии: «Выйти везде» и реестр

Пароль — это вход. Но после входа нужно понимать, где ты вошёл.

В проекте есть таблица user_sessions: токен, IP, user-agent, время создания, последняя активность, флаг отзыва. Сессия привязана к токену, а не только к cookie.

Что это даёт:

  • можно посмотреть историю входов;
  • можно увидеть, какие устройства активны;
  • можно нажать «Выйти везде» и отозвать все сессии;
  • при смене пароля старые сессии завершаются;
  • при подозрительном входе можно быстро закрыть доступ.

Ещё есть таймауты:

  • idle: 30 минут для админа, 2 часа для участника;
  • absolute: 12 часов для админа, 7 дней для участника.

То есть даже если человек не выходит, сессия не живёт вечно. Это неудобно ровно до первого инцидента. Потом — нормально.

Урок: сессия — это ресурс с сроком жизни. Если у неё нет срока, её срок определяет злоумышленник.

2FA: Telegram, потом TOTP

Сначала появилась 2FA через Telegram: при входе бот присылает подтверждение, пользователь нажимает «Подтвердить» или «Это не я».

Это удобно: Telegram уже привязан, ничего не надо ставить. Но есть зависимость: если Telegram недоступен или аккаунт потерян, вход усложняется. Поэтому появились резервные коды.

25.10.2026 добавил TOTP: обычный код из приложения-аутентификатора. Он работает без Telegram и без интернета (после настройки). В профиле можно:

  • включить TOTP;
  • посмотреть секрет и otpauth://-ссылку;
  • подтвердить кодом;
  • получить 8 резервных кодов;
  • отключить или перегенерировать коды.

Резервные коды хранятся хэшами, а не в открытом виде. Каждый код одноразовый. Если TOTP включён, при входе он заменяет подтверждение через Telegram: не надо проходить два фактора сразу.

У админа есть сброс TOTP в карточке участника — на случай потерянного телефона.

Урок: второй фактор должен иметь план Б. Иначе он превращается в «второй способ потерять доступ».

Уведомления о входе

При входе приходит сообщение: время, IP, устройство и кнопка «Сменить пароль».

Это не защита сама по себе. Но это быстрый сигнал: «это был не я». Если человек видит вход, которого не было, он может сразу сменить пароль и выйти везде.

Важно не превращать уведомления в шум. Если писать «вход» на каждый запрос, их перестанут читать. Сейчас — только на вход и только при привязке.

Ограничение попыток

Пароль можно подбирать. Поэтому есть лимиты:

  • 5 неудачных попыток с одного IP за 15 минут;
  • 10 неудачных попыток по одному логину за 60 минут;
  • отдельный лимит на репорты — 5 за 10 минут.

Лимит — это компромисс. Слишком строгий — блокирует живых людей. Слишком мягкий — не мешает подбору. Текущие значения не идеальны, но они хотя бы есть.

Урок: rate limit — не «серебряная пуля», а тормоз. Он не делает пароль надёжным, но делает перебор дороже.

CSP, SRI и внешние скрипты

Сайт использует несколько внешних библиотек: marked, DOMPurify, vanilla-tilt, model-viewer. Раньше они подключались с CDN без проверки целостности.

Теперь:

  • версии закреплены;
  • у скриптов есть integrity и crossorigin;
  • html2canvas для генератора обложек лежит локально;
  • CSP ограничивает источники скриптов.

Это не мешает CDN скомпрометировать файл, но если файл изменится, браузер его не выполнит.

Ещё один урок — SSRF. RSS-импорт принимал произвольный URL и делал запрос с сервера. Теоретически так можно было достучаться до внутренних адресов. Теперь запросы идут через safe_http_get: только http/https, блок private/loopback/link-local, DNS pinning, лимит размера и времени.

Урок: любой URL от пользователя — это запрос от твоего сервера. Относись к нему как к чужому коду.

Что делать, когда страшно

У меня есть короткий чек-лист на случай «кажется, что-то не так»:

  1. Не паниковать и не удалять логи. Сначала сохранить состояние.
  2. Проверить аудит и историю входов. Кто, когда, с какого IP.
  3. Сменить пароль и нажать «Выйти везде».
  4. Отозвать ключи и токены, если есть подозрение на утечку.
  5. Проверить бэкапы. Есть ли свежий, разворачивается ли.
  6. Зафиксировать, что произошло. Чтобы не повторить.
  7. Только потом чинить код.

Самое плохое в инциденте — не сам инцидент, а суета, в которой теряются логи и делаются необратимые действия.

Вместо вывода

Безопасность не видна, когда всё хорошо. Её замечают, когда что-то сломалось.

Я не пытаюсь сделать из сайта банк. Но базовые вещи — секреты вне архива, сессии с таймаутом, второй фактор, лимиты, бэкапы, проверка URL — обязательны. Они не «потом». Они — часть продукта.

И да, самое сложное — не включить 2FA. Самое сложное — не потерять доступ, когда телефон остался дома. Поэтому резервные коды и админ-сброс важны не меньше, чем сам второй фактор.

// реакции
SkilinPurучастник InIProject