diff --git a/README.md b/README.md index 7576376..030cd40 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,41 @@ For OpenAI-compatible SDKs set `baseURL` to `https://proxy.bildme.ru/deepseek` a --- +## Troubleshooting + +### `403 {"error":"invalid_token"}` on every path + +The proxy answered, not the upstream — `X-Proxy-Token` is missing or wrong. The body is the proxy's own format; a real upstream error looks different (DeepSeek: `{"error":{"message":...,"type":...}}`). + +The guard applies to **every** prefix. Before v0.2.0 `/openrouter/` was the one route without a token check — that exception is gone, so clients that relied on it must send the header too. + +```bash +curl -H "X-Proxy-Token: $PROXY_SECRET" \ + -H "Authorization: Bearer $DEEPSEEK_API_KEY" \ + https://proxy.bildme.ru/deepseek/user/balance +``` + +For OpenAI-compatible SDKs put it in the default headers, next to `baseURL`: + +```go +option.WithBaseURL("https://proxy.bildme.ru/deepseek"), +option.WithHeader("X-Proxy-Token", os.Getenv("PROXY_SECRET")), +``` + +### `403 {"error":"ip_not_allowed"}` + +The caller's source IP is not in `ALLOWED_CIDR`. Check the effective list: + +```bash +docker exec api-proxy cat /etc/caddy/matchers.caddy +``` + +### Upstream errors pass through unchanged + +`401`, `404` or `5xx` with an upstream-shaped JSON body means the proxy did its job and the upstream rejected the request — check the API key, not the proxy. + +--- + ## Monitoring ```bash diff --git a/swarm-report/caddy-letsencrypt-2026-09-20.md b/swarm-report/caddy-letsencrypt-2026-09-20.md new file mode 100644 index 0000000..ba1984b --- /dev/null +++ b/swarm-report/caddy-letsencrypt-2026-09-20.md @@ -0,0 +1,54 @@ +# Переход nginx → Caddy + Let's Encrypt (proxy.bildme.ru) + +Дата: 2026-09-20 · Профиль: refactoring · Репозиторий: `pub/elevenlabs.io` + +## Задача +Заменить nginx на Caddy, оставить порт 8082 как есть, параллельно открыть 443 с сертификатом Let's Encrypt на `proxy.bildme.ru`. Предыстория — таймаут `api.deepseek.com` у клиента, из-за чего в прокси был добавлен маршрут `/deepseek/`. + +## Аудит прода (62.84.98.250) +- 80/443 занимал хостовый nginx 1.18 (systemd): заглушка `/var/www/html` + vhost `tg.bildme.ru`, чей DNS уже указывал на `45.151.70.121`, сертификат истёк 14.07.2026 → потерь нет. +- Контейнер `api-proxy` из `/srv/elevenlabs` (не git), проброс `8082:8080`, образ из `git.realmanual.ru`. +- `docker-compose` 1.29.2 + Docker 28.2.2 → известная несовместимость (`KeyError: 'ContainerConfig'`). + +## Изменения в репозитории +| Файл | Что | +|------|-----| +| `Caddyfile` | новый конфиг: сниппеты `(scrub)`, `(api)`, `(routes)`; маршруты в `route {}` — guard'ы отрабатывают до `handle_path` | +| `docker-entrypoint.sh` | генерация `matchers.caddy` / `guard.caddy` из `ALLOWED_CIDR` и `PROXY_SECRET`; обязательные `DOMAIN`, `ACME_EMAIL`; `caddy validate` перед стартом | +| `Dockerfile` | `caddy:2.11.4-alpine`, EXPOSE 80/443/8080 | +| `docker-compose.yml` | порты `80`, `443` (tcp+udp), `8082:8080`; тома `caddy_data`, `caddy_config` | +| `scripts/migrate-host-nginx.sh` | бэкап `/etc/nginx` + `/etc/letsencrypt`, отключение nginx и `certbot.timer`, проверка портов | +| `scripts/deploy.sh` | реальный прод-путь `/srv/elevenlabs`, приоритет `docker compose` v2 | +| `nginx/`, `ssl/` | удалены | +| `VERSION` | 0.2.0 | + +Коммиты: `d71001a`, `e90d673`. + +## Выполнено на сервере +1. Бэкап хостового nginx → `/root/nginx-backup-20260920-093758` (`nginx-T.dump`, `etc-nginx.tar.gz`, `letsencrypt.tar.gz`). +2. `systemctl disable --now nginx`, `systemctl disable --now certbot.timer`. +3. Установлен `docker-compose-v2` 2.40.3 (v1 ломался на Docker 28). +4. `/srv/elevenlabs/docker-compose.yaml` и `.env` обновлены (бэкапы `.bak-2026-09-20`), добавлены `DOMAIN=proxy.bildme.ru`, `ACME_EMAIL=vasyakrg@gmail.com`. +5. Контейнер пересоздан из образа `0.2.0` / `latest`. + +## Проверки (прод) +| Проверка | Результат | +|----------|-----------| +| Сертификат | `CN=proxy.bildme.ru`, issuer Let's Encrypt YE1, до 19.12.2026 | +| `https://proxy.bildme.ru/health` | 200 | +| `http://62.84.98.250:8082/health` | 200 | +| `http://proxy.bildme.ru/health` | 308 → https | +| Без `X-Proxy-Token` | 403 `invalid_token` | +| IP вне `ALLOWED_CIDR` | 403 `ip_not_allowed` | +| `/deepseek/chat/completions` | 401 от DeepSeek (проброс работает, исходный i/o timeout снят) | +| `/telegram/`, `/openrouter/`, `/elevenlabs/` | 401 / 200 / 404 от апстримов — проброс работает | +| Неизвестный префикс | 404 `unknown_upstream` | +| Пересоздание контейнера | сертификат переиспользован из тома, повторного выпуска не было | +| Healthcheck | `healthy` | + +## Риски и открытые пункты +- **8082 остаётся plain HTTP** — токен и ключи апстримов идут открытым текстом. Клиентов нужно перевести на `https://proxy.bildme.ru`, после чего порт закрыть (ufw на VM сейчас `inactive`). +- **Том `caddy_data`** содержит сертификаты и ACME-аккаунт. Его удаление = повторный выпуск и риск упереться в лимиты Let's Encrypt. +- **Хостовый nginx остановлен и отключён**, конфиги в бэкапе. Если на VM появятся новые сайты — их конфиг переносится в `Caddyfile`, а не обратно в nginx. +- **`certbot.timer` отключён** — старый серт `tg.bildme.ru` больше не обновляется (домен и так живёт на другом сервере). +- Локальный каталог `ssl/` с `tls.crt`/`tls.key` удалён — эти файлы не были в git и на проде не монтировались.