429f9ad232
Fable review of the full diff from the v1.0 audit (Phase 11, bd64e80) to
HEAD plus a complete pass over the docs/security.md checklist (former spec
7.6). No exploitable findings. One defence-in-depth fix: the application
login is passed to saslpasswd2 behind a -- end-of-options marker so a
login starting with - can never be parsed as a flag. Accepted risks
unchanged; plan § D closed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
107 lines
7.6 KiB
Markdown
107 lines
7.6 KiB
Markdown
# Безопасность
|
||
|
||
**Что здесь.** (1) **Обязательные требования** — чеклист, который v1.0 обязан
|
||
выполнять; полный аудит на v1.0 пройден. Предрелизная ревизия (план § D,
|
||
модель Fable, 2026-08-06) прошла по всему дифу от аудита v1.0 (Фаза 11) до
|
||
HEAD и по чек-листу целиком: эксплуатируемых находок нет; одна правка
|
||
defence-in-depth — `--` перед логином в argv `saslpasswd2`
|
||
([internal/app/sasl.go](../internal/app/sasl.go)). (2) **Принятые риски** —
|
||
сознательные отступления сверх обязательного, чтобы решение не потерялось.
|
||
|
||
Hardening сверх обязательного (security-заголовки, проверка origin, cookie
|
||
`__Host-` с обнаружением дублей — Фаза 14) закрыт; история — в
|
||
[CHANGELOG.md](../CHANGELOG.md) и `git log`.
|
||
|
||
Продуктовые границы: [product.md](product.md). Устройство as-built:
|
||
[architecture.md](architecture.md).
|
||
|
||
---
|
||
|
||
## Обязательные требования
|
||
|
||
Панель публична из интернета — пункты ниже **не опциональны**.
|
||
|
||
### Первичная инициализация администратора
|
||
|
||
- Одноразовая secret-ссылка `/setup/<token>`, **не** env с готовым хэшем пароля.
|
||
- Токен ≥128 бит (`crypto/rand`); дублируется в `/data/setup-token`.
|
||
- Срок жизни токена — **10 минут**; после истечения или рестарта без завершённой
|
||
настройки — перегенерация и новый вывод в лог.
|
||
- Rate limiting на `/setup/<token>` по IP, отдельно от логина.
|
||
- Сравнение токена — **константное по времени** (`subtle.ConstantTimeCompare`).
|
||
- Неудачные попытки **не** инвалидируют токен досрочно (защита от DoS настройки).
|
||
- После создания администратора — токен навсегда недействителен, `/setup/*` → 404.
|
||
- Пароль администратора — только bcrypt (или argon2) в SQLite; без plaintext/MD5.
|
||
- `PANEL_USERNAME` / `PANEL_PASSWORD_HASH` в env **не используются**.
|
||
|
||
### SASL-пароли приложений
|
||
|
||
- Панель **генерирует** пароль при создании/перевыпуске, показывает **один раз**.
|
||
- В `sasldb2` — в форме, требуемой SASL (не plaintext в панели); утерян — только
|
||
перевыпуск.
|
||
|
||
### Ввод и конфигурация
|
||
|
||
- Серверная валидация email/доменов (whitelist символов); клиентская не считается
|
||
защитой.
|
||
- Режим «список адресов» — каждый адрес принадлежит домену приложения до записи.
|
||
- `postfix reload` и любой `exec` — **без** shell-интерполяции пользовательского
|
||
ввода; аргументы отдельными элементами.
|
||
- Запись в конфиг-файлы — с экранированием (нет инъекции директив Postfix).
|
||
|
||
### Аутентификация и сессии
|
||
|
||
- Rate limiting на логин (по IP, с блокировкой/задержкой).
|
||
- Сессии: криптографически случайный токен; cookie `HttpOnly`, `Secure`, `SameSite`.
|
||
- Сессии в SQLite (SHA-256 токена, не сам токен); скользящий idle
|
||
(`PANEL_SESSION_IDLE_DAYS`).
|
||
|
||
### Вывод и процесс
|
||
|
||
- Рендер через `html/template` с автоэкранированием (очередь, лог, журнал, темы).
|
||
- Процесс панели **не root** (`user=panel` в supervisord); доступ к путям через
|
||
группу `selfpost` и минимальные права.
|
||
|
||
### Почтовый тракт (связанное с безопасностью)
|
||
|
||
- **Не open relay** — только SASL; `reject_unauth_destination`;
|
||
`smtpd_sender_login_maps` + `reject_sender_login_mismatch`.
|
||
- TLS обязателен до передачи кредов (465 wrapper / 587 `encrypt`).
|
||
- `TRUSTED_PROXY_CIDR` — только явно доверенные прокси для `X-Forwarded-For`
|
||
при rate-limit логина; пусто = XFF игнорируется.
|
||
|
||
---
|
||
|
||
## Принятые риски
|
||
|
||
Здесь, а не в [implementation-plan.md](implementation-plan.md): план — про
|
||
несделанную работу, принятый риск — решение с условием возврата.
|
||
|
||
- **`POST` без `Sec-Fetch-Site` и без `Origin` пропускается.**
|
||
Клиент, не посылающий ни одного из двух — по-настоящему старый браузер или
|
||
webview с замороженным движком, — остаётся уязвим к CSRF с любого сайта.
|
||
Принято сознательно: панель однопользовательская, админ выбирает браузер
|
||
сам, а строгий режим не «защитил бы» такой клиент, а просто сломал бы в нём
|
||
панель. Ужесточение — одна строка в `originAllowed`
|
||
([internal/web/security.go](../internal/web/security.go)): вернуть `false`
|
||
вместо `true` в ветке «нет обоих заголовков».
|
||
- **CSRF-токены, привязанные к сессии, не делаются.** Проверка origin
|
||
закрывает соседний поддомен, но зависит от поведения браузера; токен — нет.
|
||
Цена — скрытое поле примерно в двух десятках форм. Триггером вернуться к
|
||
вопросу считать появление требования «устойчиво независимо от браузера».
|
||
От XSS внутри самой панели не спас бы и токен: код, исполняющийся в origin
|
||
панели, отправит запрос сам — против этого работают автоэкранирование
|
||
`html/template` и CSP, поэтому шаблоны не должны содержать
|
||
inline-скриптов и inline-стилей.
|
||
- **Send-log может навсегда остаться `queued` после рестарта панели или
|
||
пересоздания контейнера.** Log-tailer стартует с конца `mail.log`; файл не в
|
||
`/data` и теряется при recreate. Rename-ротация это не лечит. См. [architecture.md](architecture.md)
|
||
§ Log tailer — known gaps.
|
||
|
||
## Как этот список пополняется
|
||
|
||
Предрелизная проверка на уязвимости ([implementation-plan.md](implementation-plan.md)
|
||
§ D, модель Fable) закрывает каждую находку одним из двух способов: правка до
|
||
тега — либо запись сюда, с обоснованием и условием возврата, как у пунктов выше.
|
||
Третьего варианта («посмотрели и ладно») нет.
|