mirror of
https://gitverse.ru/kpa39l/md2vk.git
synced 2026-09-29 01:50:04 +00:00
12 KiB
12 KiB
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 secrettoken_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. - ВАЖНО: после
cpCaddyfile на хосте надо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. Сквозная проверка (ПОДТВЕРЖДЕНО ВЖИВУЮ)
- Без пароля: HTTP 401.
- С паролем: HTTP 200
{"status":"ok"}. - Баны: с 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 показывает ту же цепочку). - Разбан:
sudo fail2ban-client unban 87.242.100.206→ 1 (успех), Currently banned: 0. - После разбана доступ вернулся: 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).