Files
ripe-cidr-collector/docs/plan-reliability-security.md
T

59 lines
7.3 KiB
Markdown
Raw Normal View History

2026-09-21 07:29:38 +03:00
# План: надёжность и безопасность ripe_cidr_collector
## Context
Сейчас данные пишутся неатомарно, при повреждении JSON молча превращаются в `{}` (следующий сбор перезапишет хорошие данные), записи копятся бессрочно, `POST /schedule` открыт всем, а сервис слушает `0.0.0.0` без защиты. Цель: сделать хранение устойчивым, ограничить срок жизни адресов (TTL 90 дней), закрыть управляющий эндпоинт токеном. Формат ответа `GET /addresses` не меняется, поэтому потребители не ломаются.
Решения пользователя: токен только на `POST`; TTL 90 дней, настраиваемый (0 = бессрочно).
## Артефакты по правилам проекта (создаются при реализации)
- `docs/plan-reliability-security.md` - этот план (копия в проект первым шагом)
- `docs/summary-reliability-security.md` - итоги, в конце
- обновить `README.md` (токен, TTL, /health, non-root systemd, схема данных)
## Изменения
### 1. Новый модуль `storage.py` (общий, убирает дублирование load/save из обоих файлов)
- `load_json(path, default)`: при отсутствии файла возвращает default; при битом JSON логирует ошибку, переименовывает файл в `<name>.corrupt-<ts>` и бросает `StorageError` (сборщик прерывается, не затирая данные; API отвечает 503).
- `save_json_atomic(path, data)`: запись во временный файл в том же каталоге, `flush` + `fsync`, `os.replace`.
- `file_lock(path)`: контекстный менеджер на `fcntl.flock` по `<path>.lock`. Чтение-изменение-запись в сборщике идёт под блокировкой (защита от одновременного cron и APScheduler).
### 2. `cidr_collector.py`
- Заменить локальные `load_*/save_*` и `save_full_config` на функции из `storage.py`.
- Схема записи: `prefixes`/`ips` остаются списком (совместимость с API), добавляется `seen: {value: {first_seen, last_seen}}`.
- Слияние при успешном получении данных: обновить `last_seen` у увиденных, добавить новые с `first_seen`, удалить те, у кого `last_seen` старше `ttl_days`. При ошибке RIPE/DNS (`None`/пусто) ничего не удаляется.
- Миграция на лету: если `seen` нет, инициализировать `first_seen = last_seen = last_updated` (старые данные не теряются).
- Частота записи: файл пишется, если изменился состав адресов, либо если `last_seen` у какой-то записи старше 24 ч (иначе при запуске каждые 15 минут файл переписывался бы постоянно). Так TTL остаётся корректным, а лишних записей нет.
- `print` заменить на `logging` (INFO/WARNING/ERROR).
- `ttl_days` читается из `config.json` (ключ `ttl_days`, по умолчанию 90).
### 3. `api_server.py`
- `POST /schedule`: зависимость `verify_token` - заголовок `X-API-Key`, сравнение через `secrets.compare_digest`. Токен берётся из переменной окружения `RIPE_API_TOKEN` (не из `config.json`). Если переменная не задана, `POST` возвращает 503 (fail closed).
- Запись конфига через `storage.save_json_atomic` под блокировкой вместо прямого `open(...,'w')`.
- Валидация тела через pydantic-модель (`type: Literal["asn","fqdn"]`, `cron: str`) вместо `Dict[str,str]`.
- `@app.on_event` заменить на `lifespan`; в `shutdown` использовать `scheduler.shutdown(wait=False)`.
- Добавить `GET /health`: время последнего успешного сбора по asn/fqdn, число записей, статус планировщика.
- Ошибки чтения данных (`StorageError`) -> 503 вместо тихого пустого списка.
- `__main__`: хост по умолчанию `127.0.0.1`.
### 4. Развёртывание (только документация в `README.md`)
- systemd: запуск от отдельного пользователя `ripe`, `EnvironmentFile=/etc/ripe-api.env` (`RIPE_API_TOKEN=...`, права 600), опции `NoNewPrivileges=true`, `ProtectSystem=strict`, `ReadWritePaths=/opt/ripe_collector`.
- OpenRC: аналогично (`command_user`, `env` файл).
- Пояснение: `0.0.0.0` оставлен, т.к. потребители удалённые; ограничивать доступ к порту 8000 файрволом.
- Добавить `.gitignore` (`venv/`, `*.corrupt-*`, `*.lock`, `*.tmp`).
### 5. Тесты (минимум, `tests/test_core.py`, pytest)
1. Слияние и TTL: старый адрес удаляется по истечении срока, новый добавляется, при ошибке источника ничего не удаляется.
2. Битый JSON: `load_json` бросает `StorageError`, файл переименован, исходные данные не перезаписываются.
3. Авторизация: `POST /schedule` без токена -> 401, с верным токеном -> 200, без заданного `RIPE_API_TOKEN` -> 503 (`fastapi.testclient`, сеть замокана).
Добавить `pytest` и `httpx` в `requirements.txt` (или `requirements-dev.txt`).
## Критичные файлы
`cidr_collector.py`, `api_server.py`, `config.json` (+`ttl_days`), новый `storage.py`, `README.md`, `requirements.txt`.
## Проверка
1. `source venv/bin/activate && pytest -q` - 3 теста зелёные.
2. На копии текущих `data.json`/`fqdn_data.json`: `python cidr_collector.py run` - миграция без потери адресов (сравнить количество до/после), повторный запуск не переписывает файл.
3. Испортить копию `data.json` - сборщик завершается с ошибкой, файл переименован в `.corrupt-*`, API отвечает 503.
4. Запустить `uvicorn api_server:app`: `curl /addresses` без токена работает; `curl -X POST /schedule` без ключа -> 401, с `X-API-Key` -> 200, `config.json` обновлён; `curl /health` возвращает статусы.
5. Параллельный запуск двух `run --mode asn` - данные не повреждены (lock работает).
6. Выставить `ttl_days` малым значением и проверить удаление устаревшей записи.