# План: надёжность и безопасность 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 логирует ошибку, переименовывает файл в `.corrupt-` и бросает `StorageError` (сборщик прерывается, не затирая данные; API отвечает 503). - `save_json_atomic(path, data)`: запись во временный файл в том же каталоге, `flush` + `fsync`, `os.replace`. - `file_lock(path)`: контекстный менеджер на `fcntl.flock` по `.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` малым значением и проверить удаление устаревшей записи.