59 lines
7.3 KiB
Markdown
59 lines
7.3 KiB
Markdown
# План: надёжность и безопасность 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` малым значением и проверить удаление устаревшей записи.
|