mirror of
https://gitverse.ru/kpa39l/md2vk.git
synced 2026-09-29 18:05:04 +00:00
Compare commits
6 Commits
09e960a3a9
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c564d6397f | |||
| c7d7b9451c | |||
| 6684d1653f | |||
| 902ee91cf8 | |||
| 85b50a3f42 | |||
| 8d7a34c134 |
@@ -0,0 +1,62 @@
|
|||||||
|
# AGENTS.md — инструкции для агентов (Hermes и др.), работающих с /opt/md2vk
|
||||||
|
|
||||||
|
## О проекте
|
||||||
|
|
||||||
|
md2vk — сервис публикации Markdown на стену VK через официальный VK API (wall.post + format_data).
|
||||||
|
FastAPI + SQLAlchemy async + SQLite + Fernet. Документация — в `docs/`, начни с `docs/index.md` и `docs/access.md`.
|
||||||
|
|
||||||
|
## Правила работы
|
||||||
|
|
||||||
|
1. **Сначала читай доки проекта**: `docs/index.md`, `docs/access.md`, `docs/status.md`,
|
||||||
|
`docs/deploy.md` и прошлые сессии (`session_search`), потом действуй.
|
||||||
|
2. **Не удаляй файлы и данные пользователя** без явного подтверждения (БД `data/md2vk.db`,
|
||||||
|
`secrets/`, пользователи). Перезапись/миграция БД — только по явной команде.
|
||||||
|
3. **Секреты в git не коммитятся**: `secrets/token_encryption_key`,
|
||||||
|
`secrets/estorozhenko_api_key.txt`, `.env` — в .gitignore. Токены gitverse/gitea — в
|
||||||
|
`/opt/hermes/.hermes/secrets/git-tokens.env` (source его, не ищи по каталогам).
|
||||||
|
4. **Прод-порт 8420** (не 8000 — занят docker-search-api на bigbox). Контейнер слушает 127.0.0.1:8420.
|
||||||
|
5. **Изменения в прод-код** — через git: сначала commit/push в gitverse (истина), gitea подтянет pull mirror.
|
||||||
|
6. **Caddy/fail2ban живут на vps02** (ssh-алиас `vps02`, sudo по ключу):
|
||||||
|
- Caddyfile: `/opt/caddy/Caddyfile`, reload: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile`
|
||||||
|
- fail2ban: jail `md2vk`, ban после 5 попыток, unban: `sudo fail2ban-client -c /etc/fail2ban unban <ip>`
|
||||||
|
- Пароль basic auth — только bcrypt-хэш в Caddyfile; генерировать `docker exec caddy caddy hash-password`.
|
||||||
|
7. **Рестарт docker на bigbox** — локально (`docker compose up -d`), не на vps02.
|
||||||
|
8. **Наблюдаемость по умолчанию** (делать сразу, не спрашивать): лог авторизации +
|
||||||
|
метрики доступности/успешности/активной сессии. Лог: `/opt/md2vk/logs/access.{date}.log` (JSONL).
|
||||||
|
9. **Хэштеги/теги — строчными буквами**; документация — с точными маппингами портов и доступов.
|
||||||
|
10. **Не запускать ручные бэкапы** — делает крон (homelab-backup, 2:45).
|
||||||
|
|
||||||
|
## Команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/md2vk
|
||||||
|
make docker-up # собрать+поднять (или: docker compose up -d --build)
|
||||||
|
curl -s http://127.0.0.1:8420/api/v1/health # {"status":"ok"}
|
||||||
|
docker logs md2vk # логи контейнера
|
||||||
|
ls -t logs/ # аудит-лог (JSONL)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Публикация через API (пример)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
API_KEY=$(cat secrets/estorozhenko_api_key.txt)
|
||||||
|
curl -s http://127.0.0.1:8420/api/v1/publish \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"api_key\":\"$API_KEY\",\"vk_account_id\":1,\"message_md\":\"**Привет** из md2vk\"}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Сначала добавить VK-аккаунт: `POST /api/v1/accounts` c `access_token` (VK OAuth-токен, scope wall).
|
||||||
|
|
||||||
|
## Git-схема
|
||||||
|
|
||||||
|
- Истина: gitverse.ru (kpa39l/md2vk) — push через `https://kpa39l:<PAT>@gitverse.ru/kpa39l/md2vk.git`
|
||||||
|
(SSH-ключ ~/.ssh/gitverse также работает); PAT в `/opt/hermes/.hermes/secrets/git-tokens.env` ($GITVERSE_PAT)
|
||||||
|
- Зеркало: gitea.nixg.ru (estorozhenko/md2vk) — pull mirror, обновляется автоматически (8h)
|
||||||
|
- Ветки: main (default)
|
||||||
|
|
||||||
|
## Ключевые лица/доступы
|
||||||
|
|
||||||
|
- Владелец: estorozhenko (пользователь Hermes), kpa39l (gitverse-аккаунт, email kpa39l@yandex.ru)
|
||||||
|
- vps02: 87.242.100.206 (estorozhenko, sudo по ключу), Caddy + fail2ban
|
||||||
|
- bigbox: 10.8.0.2 (локальный хост), проект /opt/md2vk
|
||||||
|
- DNS: nixg.ru (Jino), A-запись md2vk → 87.242.100.206
|
||||||
@@ -13,6 +13,7 @@ COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/pytho
|
|||||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
COPY app/ app/
|
COPY app/ app/
|
||||||
|
COPY static/ static/
|
||||||
COPY .env.example .env
|
COPY .env.example .env
|
||||||
|
|
||||||
# Read-only rootfs — только /data для БД и /logs для аудита
|
# Read-only rootfs — только /data для БД и /logs для аудита
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# PRD — md2vk (публикация Markdown в VK)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
Сервис для публикации статей в формате Markdown на стену VK (личную или сообщества) через
|
||||||
|
официальный VK API (wall.post + format_data). Конвертация Markdown → VK-разметка, шифрование
|
||||||
|
токенов, архив публикаций.
|
||||||
|
|
||||||
|
## Пользователи
|
||||||
|
- Лично (estorozhenko) — публикация статей на стены VK.
|
||||||
|
|
||||||
|
## Функциональные требования
|
||||||
|
- [x] Конвертация Markdown → VK format_data (жирный, курсив, код, ссылки, заголовки, цитаты, блоки кода)
|
||||||
|
- [x] Публикация поста на стене VK (wall.post) с VK-токеном
|
||||||
|
- [x] Управление VK-аккаунтами (добавить, список, удалить) — ручной ввод access_token (Phase 1)
|
||||||
|
- [x] Архив публикаций с фильтрацией (Publication)
|
||||||
|
- [x] Конвертация без публикации (preview)
|
||||||
|
- [x] Аутентификация по API-ключу (заголовок Authorization: Bearer + тело api_key)
|
||||||
|
- [x] Шифрование VK-токенов (Fernet, AES-128-CBC + HMAC-SHA256)
|
||||||
|
- [x] Аудит-лог всех /api/v1/* (JSONL, ротация по дням)
|
||||||
|
- [ ] OAuth-флоу VK ID (нужны client_id/secret VK-приложения) — открытая задача
|
||||||
|
- [ ] Планировщик отложенных постов (status=scheduled → wall.post) — открытая задача
|
||||||
|
- [ ] Web UI (форма поста + превью) — открытая задача
|
||||||
|
|
||||||
|
## Нефункциональные требования
|
||||||
|
- [x] HTTPS наружу (Caddy на vps02 + Let's Encrypt), basic_auth (пользователь estorozhenko, bcrypt)
|
||||||
|
- [x] Защита от брутфорса: fail2ban на vps02, ban после 5 неудачных попыток (bantime=-1, ручной unban)
|
||||||
|
- [x] Слушает ТОЛЬКО на WG-интерфейсе 10.8.0.2:8420 (наружу не доступен, кроме Caddy)
|
||||||
|
- [x] БД и логи — bind-mount на хосте (вне контейнера)
|
||||||
|
- [x] Контейнер read_only: true (кроме /data и /logs)
|
||||||
|
- [x] Rate limiting: RATE_LIMIT_PER_MINUTE=10 в конфиге (in-memory bucket НЕ реализован — открытая задача)
|
||||||
|
- [ ] Graceful shutdown, retry при сетевых ошибках VK API — открытые задачи
|
||||||
|
|
||||||
|
## Границы (что НЕ делаем в Phase 1)
|
||||||
|
- НЕ «взламываем» VK: только официальный OAuth 2.0 / VK API
|
||||||
|
- Лонгриды/статьи через API не публикуются (только wall.post с анонсами/ссылками) — ограничение VK API
|
||||||
|
- Планировщик отложенных постов не включается в текущую итерацию
|
||||||
|
- Полный OAuth-флоу — отдельная задача (после получения client_id/secret)
|
||||||
|
|
||||||
|
## Критерии готовности
|
||||||
|
- [x] Сквозная проверка: https + auth (401/200) + ban после 5 попыток + ручной unban
|
||||||
|
- [x] Pull mirror gitverse → gitea синхронизирован
|
||||||
|
- [x] Документация в docs/ (index, access, architecture, vk-api, security, deploy, status) + AGENTS.md
|
||||||
|
- [x] openspec baseline зафиксирован
|
||||||
|
|
||||||
|
## Доступы (реквизиты)
|
||||||
|
- Домен: https://md2vk.nixg.ru (vps02, Caddy)
|
||||||
|
- Пользователь basic_auth: estorozhenko (bcrypt-хэш в Caddyfile, пароль НЕ в git)
|
||||||
|
- Backend: bigbox 10.8.0.2:8420 (WG)
|
||||||
|
- Git: gitverse.ru/kpa39l/md2vk (primary) → gitea.nixg.ru/estorozhenko/md2vk (pull mirror, 8h)
|
||||||
|
- openspec: /opt/md2vk/openspec/
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# md2vk — Статус проекта
|
# md2vk — Статус проекта
|
||||||
|
|
||||||
> История изменений — в [docs/status.md](docs/status.md). Структура и реквизиты — в [docs/](docs/index.md).
|
> История изменений — в [docs/status.md](docs/status.md) и [WALKTHROUGH.md](WALKTHROUGH.md).
|
||||||
|
> Требования — [PRD.md](PRD.md). Задачи — [TODO.md](TODO.md). Структура и реквизиты — в [docs/](docs/index.md).
|
||||||
|
|
||||||
## Текущее состояние
|
## Текущее состояние
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# TODO — md2vk
|
||||||
|
|
||||||
|
Формат: | дата | задача | статус | закрыта в |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2026-09-18 | Оценка проекта md2vk (FastAPI ядро, Phase 1) | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-18 | Документация: docs/ (index, access, architecture, vk-api, security, deploy, status) + AGENTS.md | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-18 | Аудит-лог авторизации (JSONL, ротация по дням) — app/api/audit.py | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-18 | Docker: Dockerfile (двухстадийный python:3.12-slim), compose :8420, bind data/logs, healthcheck, docker secret | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-18 | scripts/create_user.py — создание пользователя/API-ключа | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-18 | openspec init + change baseline-docs-infra (validate → archive) | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-18 | git init + push gitverse (kpa39l/md2vk) | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-19 | Pull mirror gitverse → gitea (estorozhenko/md2vk), пересоздан migrate, sync подтверждён | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-19 | Caddy на vps02: md2vk.nixg.ru + basic_auth estorozhenko (bcrypt) + site-лог md2vk.access.log | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-19 | fail2ban на vps02: jail md2vk (maxretry=5, findtime=600, bantime=-1, ignoreip WG) | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-19 | Сквозная проверка: https + auth + ban (6-я попытка режется) + unban (доступ вернулся) | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-19 | Порт 8420 переведён с 127.0.0.1 на 10.8.0.2 (Caddy на vps02 ходит по WG) | ✅ закрыта | @session:this |
|
||||||
|
| 2026-09-18 | Планировщик отложенных постов (status=scheduled → wall.post) | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | OAuth-флоу VK ID (кнопка «Прикрепить аккаунт»; нужны client_id/secret VK-приложения) | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | Web UI (форма поста + превью) | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | Unit/integration тесты (pytest) | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | Rate limiting (in-memory bucket, RATE_LIMIT_PER_MINUTE) | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | Retry при сетевых ошибках VK API | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | Graceful shutdown | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | Read-only rootfs: /logs мешает read_only: true — пересмотреть | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | CORS (ограничение по origin) | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | Обработка длинных постов (multipart: пост + комментарии) | 🔵 открыта | |
|
||||||
|
| 2026-09-18 | Hermes skill md2vk (проверка сервиса, публикация, добавление аккаунта) | 🔵 открыта | |
|
||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
# 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. Сборка и запуск
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```bash
|
||||||
|
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` блокировался политикой → контейнер пересоздан вручную:
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```bash
|
||||||
|
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).
|
||||||
+36
-1
@@ -6,11 +6,12 @@ import logging
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||||
|
|
||||||
from app.database import init_db
|
from app.database import init_db
|
||||||
from app.api.v1 import router as api_v1_router
|
from app.api.v1 import router as api_v1_router
|
||||||
from app.api.audit import AuditMiddleware
|
from app.api.audit import AuditMiddleware
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||||||
logger = logging.getLogger("md2vk")
|
logger = logging.getLogger("md2vk")
|
||||||
@@ -38,6 +39,40 @@ app.add_middleware(AuditMiddleware)
|
|||||||
|
|
||||||
app.include_router(api_v1_router)
|
app.include_router(api_v1_router)
|
||||||
|
|
||||||
|
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/favicon.ico", include_in_schema=False)
|
||||||
|
async def favicon() -> FileResponse:
|
||||||
|
"""Favicon (SVG-бренд)."""
|
||||||
|
return FileResponse(STATIC_DIR / "favicon.svg", media_type="image/svg+xml")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||||
|
async def index() -> HTMLResponse:
|
||||||
|
"""Стартовая страница сервиса."""
|
||||||
|
return HTMLResponse(
|
||||||
|
"""<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>md2vk</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 80px auto; padding: 0 20px;
|
||||||
|
background: #0d1117; color: #e6edf3; }
|
||||||
|
h1 { color: #58a6ff; }
|
||||||
|
code { background: #1f2630; padding: 2px 6px; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>md2vk</h1>
|
||||||
|
<p>Сервис публикации Markdown на стену ВКонтакте.</p>
|
||||||
|
<p>API: <code>/api/v1</code> · <a href="/docs" style="color:#58a6ff">Swagger UI</a> · <a href="/redoc" style="color:#58a6ff">ReDoc</a></p>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
@app.exception_handler(Exception)
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
async def global_exception_handler(request: Request, exc: Exception):
|
||||||
|
|||||||
+3
-1
@@ -4,7 +4,9 @@ services:
|
|||||||
# Внутренний порт в контейнере — 8420; наружу слушает 127.0.0.1:8420
|
# Внутренний порт в контейнере — 8420; наружу слушает 127.0.0.1:8420
|
||||||
# (8000 на bigbox занят docker-search-api). Caddy на vps02 → 10.8.0.2:8420.
|
# (8000 на bigbox занят docker-search-api). Caddy на vps02 → 10.8.0.2:8420.
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8420:8420"
|
# Слушаем на WG-интерфейсе bigbox (10.8.0.2), т.к. Caddy на vps02 идёт по WireGuard.
|
||||||
|
# 127.0.0.1 не подходит — vps02 не видит loopback bigbox. Наружу (публичный IP) не слушаем.
|
||||||
|
- "10.8.0.2:8420:8420"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
- ./logs:/logs
|
- ./logs:/logs
|
||||||
|
|||||||
+8
-10
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
- Весь https://md2vk.nixg.ru закрыт HTTP Basic Auth.
|
- Весь https://md2vk.nixg.ru закрыт HTTP Basic Auth.
|
||||||
- Пользователь: **estorozhenko**; пароль — у пользователя, в конфиге только bcrypt-хэш.
|
- Пользователь: **estorozhenko**; пароль — у пользователя, в конфиге только bcrypt-хэш.
|
||||||
- Хэш в Caddyfile: `admin $2a$14$T5gAji7gmg1t3Ifxw0CJy.jat9vpFfOiJIE8j5bnHynEVH1q.QTPS`
|
- Хэш в Caddyfile: `estorozhenko $2a$14$uzlQxJdcRZJ5gDpGG5B26OXnudIme1IYe7xh0WtDwFtbB6XiiDYTS`
|
||||||
(генерируется `docker exec caddy caddy hash-password --plaintext '<пароль>'`).
|
(сгенерирован 2026-09-18 `docker exec caddy caddy hash-password --plaintext '<пароль>'`).
|
||||||
- Без basic auth сервис наружу не отдаётся.
|
- Без basic auth сервис наружу не отдаётся.
|
||||||
|
|
||||||
### 2. Внутренний — API-ключ (FastAPI)
|
### 2. Внутренний — API-ключ (FastAPI)
|
||||||
@@ -20,15 +20,13 @@
|
|||||||
|
|
||||||
## Блокировка после 5 неудачных попыток (fail2ban на vps02)
|
## Блокировка после 5 неудачных попыток (fail2ban на vps02)
|
||||||
|
|
||||||
- На vps02 установлен fail2ban, jail `md2vk`:
|
- На vps02 установлен fail2ban (v1.0.2), jail `md2vk`:
|
||||||
- следит за логами Caddy (JSON: `/var/log/caddy/access.log`),
|
- следит за site-логом Caddy: `/opt/caddy/caddy_data/logs/md2vk.access.log` (JSON, все запросы включая 401),
|
||||||
- фильтр: basic auth failure (`401` с `"err"`, absence of `"user_id"`):
|
- фильтр `filter.d/caddy-md2vk.conf`: `^.*"request":\{"remote_ip":"<HOST>"[^}]*"host":"md2vk\.nixg\.ru"[^}]*\}.*"status":401`,
|
||||||
```
|
- правило: ban IP после **5 неудачных попыток** (`maxretry=5`, `findtime=600`; на 6-й срабатывает),
|
||||||
^.*"status":401.*
|
|
||||||
```
|
|
||||||
- правило: ban IP после **5 неудачных попыток подряд** (`maxretry=5`, `findtime=600`),
|
|
||||||
- наказание: **ban до ручного снятия** (`bantime = -1`),
|
- наказание: **ban до ручного снятия** (`bantime = -1`),
|
||||||
- действие: `iptables-allports` (ban на уровне ядра, DROP).
|
- действие: `iptables-allports` (цепочка `f2b-md2vk`, REJECT; на Debian 12 транслируется в nftables),
|
||||||
|
- ignoreip: `127.0.0.1/8 ::1 10.8.0.0/24` (не баним bigbox/vps02).
|
||||||
- **Ручная разблокировка** (на vps02, sudo):
|
- **Ручная разблокировка** (на vps02, sudo):
|
||||||
```bash
|
```bash
|
||||||
fail2ban-client -c /etc/fail2ban unban <ip>
|
fail2ban-client -c /etc/fail2ban unban <ip>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
|
|
||||||
## Открытые задачи
|
## Открытые задачи
|
||||||
|
|
||||||
|
- [ ] **БЛОКЕР: VK API недоступен для физлиц.** По состоянию на 2026-09-20 доступ к VK API (создание приложения, токены) — только для юрлиц, ИП и самозанятых (VK ID, «подтвердите профиль бизнеса»). У пользователя статуса нет → публикация/тест поста невозможны до изменения условий VK. Кабинет приложений: https://id.vk.ru/about/business/go/accounts/465683/apps (типы приложений: Web/Android/iOS; Standalone убран). Когда доступ откроют: см. `docs/vk-token-howto.md`.
|
||||||
- [ ] Планировщик отложенных постов (status=scheduled → wall.post). Модель готова, worker нет.
|
- [ ] Планировщик отложенных постов (status=scheduled → wall.post). Модель готова, worker нет.
|
||||||
- [ ] OAuth-флоу VK ID (кнопка «Прикрепить аккаунт»); нужны client_id/secret VK-приложения
|
- [ ] OAuth-флоу VK ID (кнопка «Прикрепить аккаунт»); нужны client_id/secret VK-приложения
|
||||||
- [ ] Web UI (форма поста + превью)
|
- [ ] Web UI (форма поста + превью)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Как получить VK access_token и опубликовать первый пост (шпаргалка)
|
||||||
|
|
||||||
|
## 1. Создать VK-приложение (один раз, вручную, на сайте VK)
|
||||||
|
|
||||||
|
1. Открой https://id.vk.com/about/business → «VK ID» → «Мои приложения» (или https://dev.vk.com).
|
||||||
|
2. «Создать приложение» → тип **Standalone-приложение**.
|
||||||
|
3. После создания будет `client_id` (число, например `123456789`).
|
||||||
|
Адрес приложения/redirect можно оставить `https://oauth.vk.com/blank.html`.
|
||||||
|
|
||||||
|
## 2. Получить токен (открыть ссылку в браузере, войти в VK)
|
||||||
|
|
||||||
|
```
|
||||||
|
https://oauth.vk.com/authorize?client_id=<client_id>&scope=wall,offline&redirect_uri=https://oauth.vk.com/blank.html&response_type=token
|
||||||
|
```
|
||||||
|
|
||||||
|
После подтверждения браузер перейдёт на
|
||||||
|
`https://oauth.vk.com/blank.html#access_token=<TOKEN>&expires_in=0&user_id=<ID>`
|
||||||
|
(expires_in=0 — бессрочный, благодаря scope=offline).
|
||||||
|
|
||||||
|
- `user_id` — ID твоей страницы (нужен для `vk_user_id`).
|
||||||
|
- Токен — длинная строка, права `wall` (пост на стену), `offline` (долгоживущий).
|
||||||
|
|
||||||
|
## 3. Добавить аккаунт в md2vk (через Swagger)
|
||||||
|
|
||||||
|
Открой https://md2vk.nixg.ru/docs (введя пароль), найди `POST /api/v1/accounts`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "<API-ключ из /opt/md2vk/secrets/estorozhenko_api_key.txt>",
|
||||||
|
"vk_user_id": <user_id из шага 2>,
|
||||||
|
"display_name": "<имя, например моя страница>",
|
||||||
|
"access_token": "<TOKEN из шага 2>",
|
||||||
|
"token_type": "user"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответ вернёт `"id": 1` — это `vk_account_id`.
|
||||||
|
|
||||||
|
## 4. Опубликовать пост
|
||||||
|
|
||||||
|
`POST /api/v1/publish`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "<тот же ключ>",
|
||||||
|
"vk_account_id": 1,
|
||||||
|
"message_md": "Привет, мир! Это тестовый пост через md2vk."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответ: `"success": true, "url": "https://vk.com/wall<user_id>_<post_id>"`.
|
||||||
|
|
||||||
|
## Альтернатива: curl из terminal (без Swagger)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
API_KEY=$(cat /opt/md2vk/secrets/estorozhenko_api_key.txt)
|
||||||
|
# добавить аккаунт
|
||||||
|
curl -s -u estorozhenko:<пароль> -X POST https://md2vk.nixg.ru/api/v1/accounts \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"api_key":"'$API_KEY'","vk_user_id":<user_id>,"display_name":"my","access_token":"<TOKEN>","token_type":"user"}'
|
||||||
|
# опубликовать
|
||||||
|
curl -s -u estorozhenko:<пароль> -X POST https://md2vk.nixg.ru/api/v1/publish \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"api_key":"'$API_KEY'","vk_account_id":1,"message_md":"Привет, мир! Это тестовый пост через md2vk."}'
|
||||||
|
```
|
||||||
|
|
||||||
|
> Пароль md2vk.nixg.ru — у пользователя (в конфиг/репо не пишем).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#0d1117"/><text x="16" y="22" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#58a6ff" text-anchor="middle">m</text></svg>
|
||||||
|
After Width: | Height: | Size: 248 B |
Reference in New Issue
Block a user