Files
ripe-cidr-collector/docs/plan-reliability-security.md
T
ayurishchevandClaude Sonnet 5 bcf8156085 Initial commit: RIPE CIDR/FQDN collector
Collector daemon, FastAPI server (addresses, diff, collect, sources),
SQLite storage with change journal, Docker Compose deployment,
tests, documentation and project rules.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 07:29:38 +03:00

7.3 KiB
Raw Blame History

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