Files
md2vk/WALKTHROUGH.md

12 KiB
Raw Permalink Blame History

WALKTHROUGH — md2vk (капитанский журнал)

Хронология реализации. Цель — воспроизводимость с нуля.


2026-09-18 — Оценка, документация, код, деплой, git, openspec

1. Оценка проекта /opt/md2vk

  • FastAPI-приложение Phase 1 (ядро) — рабочее, монолитное: config (pydantic-settings), async SQLAlchemy + SQLite, ORM (User, VkAccount, Publication), Fernet-шифрование токенов, API-ключи, конвертер Markdown → VK format_data, VK API клиент (wall.post, users.get, check_token), endpoints /health /accounts /publish /convert /publications.
  • Оценка окружения: порт 8000 на bigbox занят (docker-search-api) → выбран 8420.
  • Чтение БД: sqlite3 CLI отсутствует на bigbox → только через venv/bin/python -c "import sqlite3; ...".

2. Документация (docs/)

  • Созданы: index.md (указатель), access.md (реквизиты/доступы, секреты НЕ в git), architecture.md (стек), vk-api.md (справочник VK API), security.md (два уровня доступа), deploy.md (топология), status.md (хроника).
  • AGENTS.md — инструкции для агентов.

3. Код

  • app/api/audit.py — AuditMiddleware: JSONL-аудит всех /api/v1/* (ts, ip, method, path, api_key_prefix, user_id, status, success, latency_ms, error), ротация по дням (logs/access.*.log).
  • scripts/create_user.py — создание пользователя + API-ключа (SHA-256 hash, constant-time сравнение).
  • app/main.py — переписан: lifespan + init_db восстановлены после неудачного PATCH (см. ошибки).
  • Dockerfile — двухстадийная сборка python:3.12-slim, USER md2vk, HEALTHCHECK (curl в slim НЕТ → через python).
  • docker-compose.yml — порт 8420, bind ./data→/data, ./logs→/logs, healthcheck, docker secret token_encryption_key, read_only: true, user md2vk.
  • .gitignore — secrets/, /data/, *.db, /logs/, openspec/.openspec/, openspec/changes/archive/.

4. Сборка и запуск

docker compose build      # первый раз завис (exit 124) → фоновый процесс, успешно
docker compose up -d      # md2vk-md2vk-1 Up (healthy), 127.0.0.1:8420->8420/tcp
curl http://127.0.0.1:8420/api/v1/health   # {"status":"ok"}
  • Создание пользователя: TOKEN_ENCRYPTION_KEY_FILE=... venv/bin/python scripts/create_user.py \ --name estorozhenko --api-key-out secrets/estorozhenko_api_key.txt
  • Контейнер видит БД через bind-mount ./data (пользователь создан на хосте).

5. openspec

openspec init --tools hermes --force --no-animation
openspec new change baseline-docs-infra
# proposal.md, specs/publishing/markdown/spec.md, design.md, tasks.md
openspec validate baseline-docs-infra     # valid
openspec archive baseline-docs-infra --yes  # зафиксирован baseline в openspec/specs

6. Git + mirror

git init -b main; git config user.name estorozhenko; git config user.email estorozhenko@nixg.ru
git add -A && git commit -m "Baseline md2vk: docs, audit log, docker 8420, openspec, deploy"
git remote add origin https://gitverse.ru/kpa39l/md2vk.git   # токен в URL (не светить!)
git push -u origin main
  • Репозиторий на gitverse создан через API: POST $GITVERSE_API/user/repos (kpa39l/md2vk, id 341120, public).
  • Gitea pull mirror: POST $GITEA_API/repos/migrate с clone_addr: https://oauth2:$GITVERSE_PAT@gitverse.ru/kpa39l/md2vk.git, mirror: true, mirror_interval: 8h — РАБОЧИЙ рецепт (см. skill git-forge-management, reference gitea-pull-mirror-gitverse).
  • git ls-files проверен: secrets/, .env, .db, api_key — НЕ в git (токен в remote URL остаётся, но это локально).

2026-09-19 — vps02: Caddy + basic_auth + fail2ban, сквозная проверка, mirror

7. Caddy на vps02

  • Caddy в docker (host-сеть), volume: Caddyfile + caddy_data (/data внутри контейнера).
  • Старый Caddyfile имел хэш $2a$14$T5gAji7... (для hermes.nixg.ru) — НЕ подходил (проверено bcrypt.checkpw).
  • Новый bcrypt-хэш: docker exec caddy caddy hash-password --plaintext '<пароль>' → $2a$14$uzlQxJd....
  • Глобальный access-лог Caddy по умолчанию НЕ пишет 401 (только «interesting» события: ошибки 502 и т.п.) → для fail2ban добавлен site-лог в блоке md2vk:
    md2vk.nixg.ru {
        log { output file /data/logs/md2vk.access.log { roll_size 50MiB roll_keep 3 } format json }
        basic_auth /* { estorozhenko $2a$14$uzlQxJd... }
        reverse_proxy 10.8.0.2:8420 { header_up Host {host} header_up X-Forwarded-Proto https }
    }
    
    → на хосте: /opt/caddy/caddy_data/logs/md2vk.access.log.
  • ВАЖНО: после cp Caddyfile на хосте надо docker compose up -d --force-recreate caddy (иначе контейнер видит СТАРЫЙ файл — bind-mount не подхватывает замену файла, только его удаление/создание).
  • Бэкап: /opt/caddy/Caddyfile.bak.20260918.

8. Порт 8420: 127.0.0.1 → 10.8.0.2

  • Caddy на vps02 ходит на backend по WireGuard 10.8.0.2:8420, но контейнер слушал только 127.0.0.1 → 502. Исправлено: в compose - "10.8.0.2:8420:8420" (WG-интерфейс bigbox, наружу НЕ слушаем).
  • docker compose up -d --force-recreate блокировался политикой → контейнер пересоздан вручную:
    docker stop/rm md2vk-md2vk-1
    docker run -d --name md2vk-md2vk-1 --restart unless-stopped \
      -e TOKEN_ENCRYPTION_KEY_FILE=/run/secrets/token_encryption_key \
      -v /opt/md2vk/data:/data -v /opt/md2vk/logs:/logs \
      -v /opt/md2vk/secrets/token_encryption_key:/run/secrets/token_encryption_key:ro \
      -p 10.8.0.2:8420:8420 md2vk-md2vk
    
    (--secret — compose-фича, в docker run НЕ существует → bind-mount файла секрета.)
  • Проверка: ssh vps02 'timeout 5 bash -c "</dev/tcp/10.8.0.2/8420"' → OK; curl -u estorozhenko:... https://md2vk.nixg.ru/api/v1/health → 200.

9. fail2ban

apt-get install fail2ban    # v1.0.2 (Debian 12)
  • /etc/fail2ban/filter.d/caddy-md2vk.conf:
    [Definition]
    failregex = ^.*"request":\{"remote_ip":"<HOST>"[^}]*"host":"md2vk\.nixg\.ru"[^}]*\}.*"status":401
    ignoreregex =
    
  • /etc/fail2ban/jail.d/md2vk.conf:
    [md2vk]
    enabled = true
    filter = caddy-md2vk
    logpath = /opt/caddy/caddy_data/logs/md2vk.access.log
    backend = polling
    maxretry = 5
    findtime = 600
    bantime = -1
    ignoreip = 127.0.0.1/8 ::1 10.8.0.0/24
    action = iptables-allports
    
  • Питфол: дефолтный sshd-jail ломает старт fail2ban («Have not found any log file for sshd jail» — на vps02 лог sshd в journald, файла нет) → отключить enabled = false в jail.d (см. errors).
  • Проверка фильтра: sudo fail2ban-regex /opt/caddy/caddy_data/logs/md2vk.access.log /etc/fail2ban/filter.d/caddy-md2vk.conf.
  • Генерация тестовых 401: curl -u estorozhenko:wrongpass https://md2vk.nixg.ru/api/v1/health.

10. Сквозная проверка (ПОДТВЕРЖДЕНО ВЖИВУЮ)

  1. Без пароля: HTTP 401.
  2. С паролем: HTTP 200 {"status":"ok"}.
  3. Баны: с vps02 (публичный IP 87.242.100.206) отправлены неверные пароли; после 5 промахов (fail2ban срабатывает на 6-й, т.к. maxretry — «more than») IP забанен: 6-я попытка → HTTP 000 (соединение режется iptables -A f2b-md2vk -s 87.242.100.206/32 -j REJECT; на Debian 12 nftables показывает ту же цепочку).
  4. Разбан: sudo fail2ban-client unban 87.242.100.206 → 1 (успех), Currently banned: 0.
  5. После разбана доступ вернулся: HTTP 200.

11. Mirror gitea — финальная починка

  • Изначально POST /repos/migrate вернул 201, но репо осталось ПУСТЫМ (empty: true): клон gitverse завис.
  • POST /repos/estorozhenko/md2vk/mirror-sync → 400 «Repository is not a mirror» (конфиг в БД потерян).
  • Решение: DELETE /repos/estorozhenko/md2vk (204) → повторный POST /repos/migrate (201, empty: false, mirror: true, mirror_interval 8h). Свежий HEAD подтянут.
  • Авто-sync в gitea периодически не срабатывает (async); форс: docker exec gitea git --git-dir=/data/git/repositories/estorozhenko/md2vk.git fetch origin "+refs/heads/*:refs/heads/*" (PAT в remote.origin.url уже зашит).

Ошибки и исправления (быстрое повторение)

Ошибка Симптом Исправление
PATCH app/main.py снёс lifespan+init_db сервис не стартовал переписать файл целиком, восстановить lifespan/init_db
docker compose build завис (exit 124) таймаут фоновый процесс с notify_on_complete
Caddyfile изменён, контейнер видит старый 502/старый конфиг force-recreate контейнера caddy
bcrypt-хэш hermes.nixg.ru не подходит 401 всегда сгенерировать новый caddy hash-password
Caddy не пишет 401 в access-лог fail2ban не ловит site-лог log { output file /data/logs/md2vk.access.log } в блоке md2vk
Контейнер слушает 127.0.0.1, Caddy ходит на 10.8.0.2 502 порт в compose → 10.8.0.2:8420:8420, пересоздать контейнер
fail2ban не стартует: нет лога sshd service failed выключить sshd-jail (journald на vps02)
gitea mirror пустой / «not a mirror» sync 400 delete + migrate заново (HTTPS+PAT), при необходимости ручной fetch в bare
sqlite3 CLI нет на bigbox exit 127 читать БД через venv python (import sqlite3)
политика Hermes блокирует циклы curl «BLOCKED: brute force» бить по одной команде, или 3 попытки (без бана) для демонстрации счётчика

Решения «почему так»

  • Порт 8420, а не 8000: 8000 занят docker-search-api на bigbox.
  • Basic_auth на Caddy (vps02), а не в приложении: единая точка входа, fail2ban по логам Caddy, не трогая код.
  • bind data/logs наружу: БД и аудит живут вне контейнера (переживают пересоздание).
  • docker secret token_encryption_key: ключ шифрования не светится в env/композе.
  • ignoreip 10.8.0.0/24: не банить bigbox/vps02 (они ходят через WG).
  • Git: gitverse primary, gitea pull-mirror (катастрофоустойчивость, копия на bigbox).