# 08 · Безопасность

## Что панель защищает

| Ресурс | Как защищён |
|---|---|
| Ключ API Fornex | хранится в файле настроек вне корня сайта; не возвращается ни одним маршрутом; не пишется в журнал |
| Пароль панели | только хэш `password_hash()`; пустой хэш означает «вход запрещён» |
| Токены API | в базе лежат префикс и хэш; полный токен показывается один раз при выпуске |
| Запись в зону | только по `id`, только через план, снимок перед каждым изменением |
| Журнал | каждая попытка, сухой прогон и отказ с причиной |

## Два способа входа

1. **Сессия.** Логин и пароль, cookie `HttpOnly`, `SameSite=Lax`, `Secure` при HTTPS.
   Ограничение попыток: 10 неудач с адреса за 15 минут → `429`.
2. **Bearer-токен.** Для скриптов. По умолчанию «только чтение»; запись — только если токен
   выпущен с этим правом. Токен проверяется по хэшу, отзыв действует сразу.

CSRF-токен нужен для пишущих запросов из браузера. Скрипту он не нужен: у него нет сессии,
которую можно угнать.

## Доверенный адрес

`AUTO_LOGIN_IP` выдаёт сессию без пароля с одного адреса — те же права, что у обычного входа,
включая запись. Механизм существует для рабочего места внутри своей сети.

- Оставляйте пустым, если сеть и адрес не ваши целиком.
- Вход пишется в журнал событием `auth.autologin` с автором `panel:autologin`, чтобы «кто вошёл
  без пароля» не было тайной.
- Адрес клиента берётся из соединения; `X-Forwarded-For` учитывается только когда запрос
  приходит с loopback (nginx на том же хосте), поэтому подделать адрес заголовком нельзя.

## Ограничение доступа по адресам

Панель — административная поверхность с ключом DNS за ней. Рекомендуется закрыть её списком
разрешённых адресов на веб-сервере:

```nginx
allow 192.0.2.0/24;
allow 2001:db8::/32;
deny all;
```

Ограничения частоты на самом API нет — его ставит веб-сервер.

## Секреты в отчётах и журнале

Правило проекта: **имя, тип, место, маска**. Значение — никогда.

- `GET /api/meta` отдаёт адрес API, таймауты, типовой IP и список типов записей — не ключ.
- `GET /api/tokens` показывает префикс, дату, право записи — не значение.
- Журнал хранит действие, объект и итог; ключ и пароль в него не попадают.
- Сообщение об ошибке от Fornex передаётся как есть, но ключ в нём не появляется: он уходит
  только в заголовке запроса.

## Снимки зоны

Снимок — BIND-файл и JSON с идентификаторами записей. Он не содержит секретов, но содержит
полную зону: поддомены, адреса, TXT-записи подтверждений. Поэтому `SNAPSHOT_DIR` — вне корня
сайта и не отдаётся по HTTP.

## Что делать при подозрении на утечку ключа

1. Перевыпустить ключ в кабинете Fornex (старый отозвать).
2. Заменить `FORNEX_API_KEY` в файле настроек, перезагрузить PHP-FPM.
3. Проверить журнал: `php backend/bin/fornex audit --limit=100` — искать неожиданные действия.
4. Проверить выпущенные токены и отозвать лишние: `GET /api/tokens`, `DELETE /api/tokens/{id}`.
5. Сменить пароль панели: новый `password_hash()`, перезапуск PHP-FPM.

## Токены: практика

| Задача | Право |
|---|---|
| Мониторинг зоны, чтение записей | только чтение |
| Скрипт, который правит записи по расписанию | запись, отдельным токеном |
| Одноразовая проверка | токен с записью, отозванный после |

Токен удобнее держать в файле с правами `600` и передавать в заголовке `Authorization`.

## Сообщить об уязвимости

Приватно, через GitHub → Security → Advisories. Не в issue: публичный отчёт до исправления —
это подсказка тому, кто ищет цель. Полные правила — в
[SECURITY.md](https://github.com/paulmann/fornex-control/blob/main/SECURITY.md).
