diff --git a/compose-bcrypt-escape-trap.md b/compose-bcrypt-escape-trap.md new file mode 100644 index 0000000..38cdd95 --- /dev/null +++ b/compose-bcrypt-escape-trap.md @@ -0,0 +1,116 @@ +--- +title: Docker Compose bcrypt `$` escape trap +type: concept +tags: [docker-compose, bcrypt, password, escape, gotcha] +sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md] +updated: 2026-05-20 +--- + +# Docker Compose ест `$` в bcrypt hashes + +## Симптом + +```yaml +services: + app: + command: --admin-password '$2y$05$abc123...' +``` + +При `docker compose up` Compose выдаёт warning: +``` +warning msg="The \"r9BLFM98iCLTskjgj7Y3teOACut3Nxk\" variable is not set. Defaulting to a blank string." +``` + +И в контейнер передаётся пустая строка вместо bcrypt hash. + +## Root cause + +Compose interpolates `${VAR}` и `$VAR` syntax из env variables **в YAML values**. Bcrypt hash начинается с `$2y$` или `$2a$` — выглядит как `$NAME` шаблоны для compose'а: + +- `$2y` → попытка expand env var `2y` → не определена → empty string +- `$05` → expand env var `05` → empty string +- Остаток до точки/конца строки → имя var → empty +- Точки/слэши прерывают имя var + +Результат — обрезанный/пустой hash. + +## Решение 1 — double-escape `$` → `$$` + +В YAML literal compose treats `$$` как escape для `$`. Нужно sed-replace перед записью: + +```bash +BCRYPT=$(htpasswd -nbB user pass | cut -d':' -f2) +BCRYPT_ESCAPED=$(echo "$BCRYPT" | sed 's/\$/\$\$/g') +cat > docker-compose.yml <'"` — после tokenization shell видит **literal** одинарные кавычки внутри значения. Argument приходит как `'$2y$05$...'` (с кавычками вокруг hash) → bcrypt verify fails из-за паразитных `'` char'ов. + +## Решение 2 — YAML list form + +```yaml +command: + - "--admin-password" + - "$$2y$$05$$abc123..." +``` + +Каждый list-element — отдельный argv element, без shell tokenization. Кавычки не утекают. + +**Но опять же** — некоторые версии Compose (2024+) могут иметь регрессии где YAML list form **не unescape'ит** `$$` обратно в `$`. Проверять через `docker compose config | grep command` после написания. + +## Решение 3 — `docker run` напрямую, без compose + +Bypass проблему. Shell escape'ы (одинарные кавычки) предсказуемы: + +```bash +BCRYPT=$(htpasswd -nbB user pass | cut -d':' -f2) +docker run -d --name app \ + -v ... \ + app/app:tag \ + --admin-password "$BCRYPT" # bash interpolation одного слоя +``` + +Bash `"$BCRYPT"` expand'ится один раз. Внутри expansion'а `$` уже не парсится. Container получает чистый hash. + +Trade-off: лишаемся compose файла → нет declarative restart, нужно `docker run --restart unless-stopped`. Управляемо. + +## Решение 4 — env file with single var + +```yaml +services: + app: + env_file: ./bcrypt.env + command: --admin-password ${BCRYPT} +``` + +`bcrypt.env`: +``` +BCRYPT=$2y$05$abc123... +``` + +Env file читается raw, без compose interpolation. Тогда `${BCRYPT}` в command expand'ится из этого env. Работает. + +Но менее transparent — debugger должен смотреть в env file. + +## Похожие traps + +Любые значения с `$`: +- Postgres password `secret$pass$word` +- API keys с `$` (rare) +- Cron syntax в command (`$1` etc.) +- Bash variable substitutions внутри command + +## Где применено + +`portainer/portainer-ce:2.21.5` на [`vds-kzntsv`](../entities/vds-kzntsv.md) — финальный путь = решение 3 (`docker run` direct). Хотя в этой же сессии оказалось что `--admin-password` flag broken в Portainer 2.21+ regardless escape — см. [`portainer-2.21-admin-password-regression`](portainer-2.21-admin-password-regression.md). + +## Ссылки + +- Compose docs: [Variable interpolation](https://docs.docker.com/compose/compose-file/12-interpolation/) +- Сессия: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 1 cont. diff --git a/db-tls-self-signed-via-traefik-raw-tcp.md b/db-tls-self-signed-via-traefik-raw-tcp.md new file mode 100644 index 0000000..d71eb97 --- /dev/null +++ b/db-tls-self-signed-via-traefik-raw-tcp.md @@ -0,0 +1,147 @@ +--- +title: DB TLS через traefik raw TCP с self-signed certs +type: concept +tags: [traefik, tls, postgres, mariadb, mongo, redis, self-signed, pattern] +sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md] +updated: 2026-05-20 +--- + +# Self-signed DB TLS через Traefik raw TCP + +Pattern для выставления нескольких DB-контейнеров наружу (public internet) через traefik с минимумом сложности cert management. Self-signed на старте, LE-extraction позже. + +Сочетается с [`traefik-tcp-passthrough-vs-starttls`](traefik-tcp-passthrough-vs-starttls.md) — там объяснено почему `HostSNI(*)` + raw TCP, не SNI passthrough. + +## Архитектура + +``` +Internet client + ↓ TLS handshake (порт зависит от DB) + ↓ .vds.kzntsv.site:5432/3306/27017/6379 +Traefik (raw TCP forward, без TLS inspection) + ↓ docker network `proxy` +DB container (терминирует TLS своим self-signed cert) + ↓ docker network `shared-dbs` +Other VDS containers (могут ходить без TLS по dns name `postgres`/`mariadb`/...) +``` + +Каждая DB: +- Подключена к двум networks: `proxy` (для traefik) и `shared-dbs` (для других контейнеров VDS). +- Имеет свой self-signed cert (CN = `.vds.kzntsv.site`) в `./certs/`. +- Конфигурируется для TLS-required. +- Объявляет traefik label с `HostSNI(\`*\`)` на dedicated entrypoint port. + +Traefik static config: +```yaml +entryPoints: + postgres: { address: ":5432" } + mariadb: { address: ":3306" } + mongo: { address: ":27017" } + redis: { address: ":6379" } +``` + +ufw: allow на эти 4 порта. + +## Cert generation + +```bash +for db in postgres mariadb mongo redis; do + openssl req -x509 -newkey rsa:2048 \ + -keyout /opt/stacks/databases/$db/certs/server.key \ + -out /opt/stacks/databases/$db/certs/server.crt \ + -days 3650 -nodes \ + -subj "/CN=$db.vds.kzntsv.site/O=kzntsv.site/C=RU" \ + -addext "subjectAltName=DNS:$db.vds.kzntsv.site" + chmod 600 /opt/stacks/databases/$db/certs/server.key +done +``` + +## DB-specific quirks + +### Postgres 16 (non-alpine, uid 999) + +Postgres key file перм-checks: must be 600 + owned by postgres user OR root. Postgres alpine использует **uid 70**, не 999. Не-alpine Debian — **uid 999**. Если используем alpine — `chown -R 70:70 certs/server.key`; если debian — `chown 999:999`. Лучше debian (`postgres:16`) для совместимости с прочими стеками. Команда: + +```yaml +command: + - postgres + - -c + - ssl=on + - -c + - ssl_cert_file=/etc/postgres-certs/server.crt + - -c + - ssl_key_file=/etc/postgres-certs/server.key +``` + +### MariaDB 11.4 + +```yaml +command: + - --ssl-cert=/etc/mariadb-certs/server.crt + - --ssl-key=/etc/mariadb-certs/server.key + - --require-secure-transport=ON # форсит TLS для всех клиентов +``` + +### MongoDB 7 + +Cert + key должны быть **в одном PEM-файле** (`cat server.crt server.key > server.pem`). Плюс Mongo 7 enforce'ит "chain of trust" — нужен `--tlsCAFile` (для self-signed — указываем server.crt сам как CA). `--tlsAllowConnectionsWithoutCertificates` нужен иначе сервер требует client cert. + +```yaml +command: + - --tlsMode=requireTLS + - --tlsCertificateKeyFile=/etc/mongo-certs/server.pem + - --tlsCAFile=/etc/mongo-certs/server.crt + - --tlsAllowConnectionsWithoutCertificates + - --bind_ip_all +``` + +### Redis 7 alpine + +```yaml +command: + - redis-server + - --port + - "0" # disable non-TLS port + - --tls-port + - "6379" + - --tls-cert-file + - /etc/redis-certs/server.crt + - --tls-key-file + - /etc/redis-certs/server.key + - --tls-auth-clients + - "no" # без mTLS + - --requirepass + - +``` + +## Client connection examples + +```bash +# Postgres (sslmode=require, не verify-full — self-signed) +psql "postgresql://postgres:$PG_PASS@postgres.vds.kzntsv.site:5432/postgres?sslmode=require" + +# MariaDB (--ssl + --ssl-verify-server-cert=0) +mariadb -h mariadb.vds.kzntsv.site -P 3306 -u root -p$MARIA_PASS --ssl --ssl-verify-server-cert=0 + +# Mongo (tls=true + tlsAllowInvalidCertificates) +mongosh "mongodb://root:$MONGO_PASS@mongo.vds.kzntsv.site:27017/admin?tls=true&tlsAllowInvalidCertificates=true" + +# Redis (--tls --insecure) +redis-cli --tls --insecure -h redis.vds.kzntsv.site -p 6379 -a $REDIS_PASS +``` + +## Trade-off + +- ✔ Уровень входа: 5 минут на cert + 5 минут на compose. Никаких lego/cert-manager headache на старте. +- ✔ Каждый клиент явно отключает verify — predictable failure mode (если cert меняется случайно — клиент сразу пишет в логи). +- ✗ Клиенты должны помнить `verify=disable`. Production-tier клиенты обычно фейлят на self-signed по defaultу. +- ✗ Cert не ротируется автоматически. 10-year validity = временное обходное. +- ✗ Для каждой DB — отдельный cert (не wildcard). Расширяемо, но если будет mongo + mongo-readonly — у каждого свой. + +## Roadmap к LE certs + +Sidecar контейнер с lego (готовый container `goacme/lego` или собственный) который watch'ит traefik `acme.json` и каждый раз когда меняется (i.e. cert ротировался) — извлекает PEM-bundle на disk + триггерит `docker exec kill -HUP 1` для reload. Каждый DB получает auto-renewed LE cert. Это deferred — см. follow-up task в [`vds-kzntsv`](../entities/vds-kzntsv.md). + +## Где применено + +4 shared DBs на [`vds-kzntsv`](../entities/vds-kzntsv.md): postgres / mariadb / mongo / redis. Self-signed 10-year certs в `/opt/stacks/databases//certs/`. Strong random hex32 пароли в `~/projects/.common/secrets/vds-kzntsv.env`. diff --git a/future-resilient-architecture-goals.md b/future-resilient-architecture-goals.md index 0598204..f3e1637 100644 --- a/future-resilient-architecture-goals.md +++ b/future-resilient-architecture-goals.md @@ -98,3 +98,14 @@ MSSQL options: 5. Документировать runbooks. Связано со всеми остальными страницами этого wiki. + +## Прогресс 2026-05-20 + +✅ **Decouple инфра-сервисов от windows-recovery-host SPOF.** Поднят отдельный облачный VDS [[vds-kzntsv]] (Rusonyx 160 NVMe), туда мигрированы gitea / verdaccio / docker-registry + shared DB park (Postgres/MariaDB/Mongo/Redis). [[windows-recovery-host]] теперь хостит **только** production CMS. Это не полный multi-host resilience (VDS сам по себе SPOF), но critical infra decoupling сделан. + +Запланированы follow-up tasks (см. `.tasks/`): +- `vds-backup-rsync-kreknin` — ежедневный 05:00 MSK rsync VDS → kreknin с email-нотификацией. +- `vds-ntfy-push` — self-hosted push на Android для backup status и monitoring. +- `vds-gc-cron` — cron GC для verdaccio + registry чтобы не повторился incident 99G на registry. + +Следующая фаза по originally outlined ladder — добавить **cloud off-site backup target** (Backblaze B2 / Yandex Object Storage) поверх kreknin'а, и replicated MSSQL для production CMS. diff --git a/portainer-2.21-admin-password-regression.md b/portainer-2.21-admin-password-regression.md new file mode 100644 index 0000000..c47c987 --- /dev/null +++ b/portainer-2.21-admin-password-regression.md @@ -0,0 +1,85 @@ +--- +title: Portainer 2.21 `--admin-password` regression + min 12-char policy +type: concept +tags: [portainer, password, bcrypt, regression, gotcha] +sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md] +updated: 2026-05-20 +--- + +# Portainer 2.20+ admin-password regression + +## Симптом 1 — API init policy + +Portainer 2.20.0+ форсит **минимум 12 chars** на admin password при создании через API endpoint `POST /api/users/admin/init`. Короче — `{"message":"Password does not meet the requirements"}`. + +## Симптом 2 — CLI flag `--admin-password` broken + +CLI flag `--admin-password ""` (документированный путь init без API) в 2.21.5 ведёт себя странно: +- Лог сервера показывает «created admin user with the given password.» — то есть admin **записывается**. +- Login с паролем → `Invalid credentials`. Bcrypt verify fails. + +Пробовали и `$2y$` (htpasswd default), и `$2a$` (Go bcrypt), и YAML list form чтобы избежать compose env-interp escape (`$` → `$$`), и docker run direct без compose. Bcrypt hash в `docker inspect` Cmd корректный. Но login всё равно fails. + +Не покопали глубоко (возможно — flag тупо игнорируется и admin создаётся с auto-generated password, либо хеш re-hashится сервером). + +## Решение + +Bypass CLI flag. Использовать API init с **длинным паролем (12+ chars)**: + +```bash +# Run portainer без --admin-password (fresh data dir) +docker run -d --name portainer --network proxy \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $PWD/data:/data \ + portainer/portainer-ce:2.21.5 + +# API admin/init available 5 minutes after start +curl -sk -X POST https://portainer.vds.kzntsv.site/api/users/admin/init \ + -H 'Content-Type: application/json' \ + -d '{"Username":"vitya","Password":"Pryakhin9-VDS-2026"}' + +# Login → JWT +JWT=$(curl -sk -X POST https://portainer.vds.kzntsv.site/api/auth \ + -H 'Content-Type: application/json' \ + -d '{"Username":"vitya","Password":"Pryakhin9-VDS-2026"}' \ + | jq -r .jwt) + +# Generate API key +APIKEY=$(curl -sk -X POST https://portainer.vds.kzntsv.site/api/users/1/tokens \ + -H "Authorization: Bearer $JWT" \ + -H 'Content-Type: application/json' \ + -d '{"description":"automation","password":"Pryakhin9-VDS-2026"}' \ + | jq -r .rawAPIKey) +``` + +После init — local docker endpoint надо создать отдельным POST'ом: +```bash +curl -sk -X POST https://portainer.vds.kzntsv.site/api/endpoints \ + -H "X-API-Key: $APIKEY" \ + -F "Name=local" \ + -F "EndpointCreationType=1" # LocalDockerEnvironment +``` + +## Trade-off + +- ✗ Имя пользователя пароль user'а — приходится менять на «12+ chars» (`Pryakhin9-VDS-2026` вместо `Pryakhin9`). +- ✔ Single source of truth — admin live в DB только через API, всегда последовательное состояние. +- ✔ API token доступен сразу для дальнейшей автоматизации stacks через REST. + +## Compose эскейп bcrypt — separate gotcha + +При попытках использовать `--admin-password` в compose столкнулись с classic `$` escape trap: +- В YAML string form `command: "... --admin-password '$$2y$$05$$abc'"` — compose unwraps `$$` → `$`, передаёт правильный hash. +- В YAML list form `command: ["...", "--admin-password", "$$2y$$05$$abc"]` — compose unwraps аналогично, hash виден в `docker inspect` корректный. +- Кавычки `'...'` вокруг hash в string form **становятся literal частью value** (shell tokenize'ит, не shell-context'ит), bcrypt получает паразитные `'` → broken hash. + +Решение для подобных escape-проблем — `docker run` direct (нет compose env-interp) или `--admin-password-file` (passes plain text from file, no escape) — но last не bypass'ит policy, ждёт plain text 12+ chars. + +## Где применено + +Portainer на [`vds-kzntsv`](../entities/vds-kzntsv.md), запущен через `docker run` (не compose) чтобы зафиксировать конкретный набор labels. Кред live в `~/projects/.common/secrets/vds-kzntsv.env`. + +## Ссылки + +- Portainer changelog 2.20.0 — введён password policy + auth refactor (https://github.com/portainer/portainer/releases/tag/2.20.0). +- Сессия где упоролись: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 1 cont. diff --git a/registry-gc-mount-and-modify-flag.md b/registry-gc-mount-and-modify-flag.md new file mode 100644 index 0000000..06ca8ad --- /dev/null +++ b/registry-gc-mount-and-modify-flag.md @@ -0,0 +1,115 @@ +--- +title: Docker Registry garbage-collect mount layout + `-m` flag +type: concept +tags: [docker-registry, garbage-collect, gotcha, storage] +sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md] +updated: 2026-05-20 +--- + +# Registry GC — mount path и `-m` flag + +Распознаваемая пара ошибок при `registry garbage-collect` на offline-restored backup data. Один shoot — `-m` flag для реального удаления, второй — правильный mount path. + +## Симптом 1 — `Path not found: /docker/registry/v2/repositories` + +Запуск: +```bash +docker run --rm \ + -v /volume1/docker/infrastucture/registry/docker:/var/lib/registry \ + registry:2.8.3 garbage-collect /etc/docker/registry/config.yml +``` + +Ошибка: `failed to garbage collect: failed to mark: filesystem: Path not found: /docker/registry/v2/repositories`. + +### Root cause + +Default config файл `/etc/docker/registry/config.yml` в registry image имеет: +```yaml +storage: + filesystem: + rootdirectory: /var/lib/registry +``` + +Registry expect'ит файлы в `/var/lib/registry/docker/registry/v2/...`. Mounting только `docker/` subdir host'а к `/var/lib/registry/` положит данные в `/var/lib/registry/registry/v2/...` (один уровень потерян). + +### Fix — mount PARENT directory + +```bash +docker run --rm \ + -v /volume1/docker/infrastucture/registry:/var/lib/registry \ # <-- parent dir, не /docker + registry:2.8.3 garbage-collect /etc/docker/registry/config.yml +``` + +Теперь internal path = `/var/lib/registry/docker/registry/v2/...` ✅. + +(Original kreknin compose использовал `./:/var/lib/registry` — mount whole registry parent dir — что было корректно но для running registry, не для offline GC.) + +## Симптом 2 — GC ничего не удаляет, размер прежний + +После первого GC прохода видим лог: +``` +blob eligible for deletion: sha256:087b41... +time="..." level=info msg="Deleting blob: /docker/registry/v2/blobs/sha256/08/087b41..." +``` + +Лог говорит «Deleting» — но `du -sh` показывает **прежний 99G**. Размер не изменился. + +### Root cause + +`registry garbage-collect` без флагов работает в **dry-run mode** (incident-free). Лог «Deleting blob» — `info` level намерения, не actual unlink. + +Подобный intent vs action разделение типично для batch tools (`apt-get -s`, `git rm --dry-run`, etc.) но в registry CLI нет attention-grabbing `--dry-run` flag — а опция «реально делать» named cryptically. + +### Fix — `-m` (modify) flag + +```bash +docker run --rm \ + -v /volume1/docker/infrastucture/registry:/var/lib/registry \ + registry:2.8.3 garbage-collect -m /etc/docker/registry/config.yml +``` + +С `-m` после прохода размер реально уменьшается. На kreknin backup'е: **99G → 35G** (64G freed), 2-3 минуты обработки. + +## Полный рецепт offline GC restored backup data + +```bash +# 1. Pull registry image что соответствует production версии (compatibility) +docker pull registry:2.8.3 + +# 2. (Optional) dry-run для отчёта — что будет удалено +docker run --rm \ + -v /path/to/restored/registry:/var/lib/registry \ + registry:2.8.3 garbage-collect /etc/docker/registry/config.yml \ + > gc-dry-run.log 2>&1 + +# 3. Реальный GC +docker run --rm \ + -v /path/to/restored/registry:/var/lib/registry \ + registry:2.8.3 garbage-collect -m /etc/docker/registry/config.yml + +# 4. Measure delta +du -sh /path/to/restored/registry/docker +``` + +## Online GC (production live registry) — важные дополнения + +Если делаем GC на **running** registry — нужен read-only mode чтобы избежать race condition'ов (новый push во время GC может потерять blobs): + +```yaml +# Add to running registry config / env vars +storage: + maintenance: + readonly: + enabled: true +``` + +Затем restart registry, run GC `-m`, отключить read-only, restart again. Окно downtime — продолжительность GC (~10-30 min на средних объёмах). + +## Где применено + +[`vds-kzntsv`](../entities/vds-kzntsv.md) Phase 3.3 — GC kreknin'овского backup'а 99G → 35G. После того как user decided abandon миграцию и fresh install — GC оказался полезной экономией если бы tar/rsync'или (но в финале rsync не запустился). + +## Ссылки + +- Registry docs: [Garbage collection](https://distribution.github.io/distribution/about/garbage-collection/) +- Сессия: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 3.3. diff --git a/rusonyx-vps-onboarding-quirks.md b/rusonyx-vps-onboarding-quirks.md new file mode 100644 index 0000000..68c064e --- /dev/null +++ b/rusonyx-vps-onboarding-quirks.md @@ -0,0 +1,92 @@ +--- +title: Rusonyx VPS onboarding quirks (Astra Облако / myvm.rusonyx.ru) +type: concept +tags: [rusonyx, vds, bootstrap, onboarding, gotchas, vendor] +sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md] +updated: 2026-05-20 +--- + +# Rusonyx VPS onboarding quirks + +Quirks first-boot опыта на Rusonyx VDS (Astra Облако, https://myvm.rusonyx.ru) — для memory будущих сессий. Зафиксировано при активации [`vds-kzntsv`](../entities/vds-kzntsv.md) 2026-05-20. + +## 1. VNC console не открывается с первого раза + +В Управление сервером → Консоль HTML5 noVNC может не загружаться (зависание на индикаторе соединения). Помогает кнопка **«Остановить VNC»** в той же панели — force-disconnect stale attachment'а на hypervisor side. После клика подождать ~5 сек, открыть консоль заново. + +Если не помогает — ticket в support, без VNC реальной возможности зайти в box нет (см. quirk 2). + +## 2. Stale системный образ Ubuntu — обязательный upgrade через VNC + +Поставка содержит сотни pending updates, включая `openssh-server` и kernel. Письмо при активации **прямо рекомендует** последовательность: + +```bash +apt update +apt upgrade +apt --fix-broken install +apt upgrade +``` + +И ключевая часть: **«строго через VNC-консоль»**, потому что `openssh-server` upgrade'у переключают сервис, что разрывает SSH session и бьёт upgrade на половине. + +По дороге будет **5+ dpkg interactive prompts** (conffile conflicts). Ответы: + +| Prompt | Правильный ответ | Reasoning | +|---|---|---| +| `/etc/ssh/sshd_config` | **2** (keep local) | Rusonyx-modified sshd_config форсит `PermitRootLogin yes` + `PasswordAuthentication yes` для первого захода. Maintainer-версия дефолтит `prohibit-password` — потеряешь SSH-доступ если ключ ещё не залит. | +| `/etc/cloud/cloud.cfg` | **N** (keep current) | Rusonyx модифицировал под свой provisioning (network, ssh-key inject). Maintainer-версия может сломать их хуки. | +| `grub-pc /dev/vdaX target` | **1** (`/dev/vda` whole-disk MBR) | Опция 2 (на partition) — blocklist mechanism, less reliable. Опция 3 (skip) = box не загрузится. | +| `cloud-init local config` (если появится) | keep | По той же логике. | + +После завершения — `reboot` (или ждать пока apt сам запустит). + +## 3. SSH initial password — одноразовый + +Активационное письмо даёт `root` + одноразовый пароль. Первый шаг bootstrap'а (после VNC-upgrade) — push своего SSH key и harden sshd: + +``` +PermitRootLogin no +PasswordAuthentication no +PubkeyAuthentication yes +``` + +Через **drop-in file `/etc/ssh/sshd_config.d/00-hardening.conf`** (префикс `00-` чтобы выиграть first-match precedence над `50-cloud-init.conf` который форсит `PasswordAuthentication yes`). + +## 4. Подключение через SSH с password на Windows + +OpenSSH for Windows **не имеет** `-pw` flag (как `plink`/PuTTY) или `sshpass`. Для одноразового password-bootstrap: + +- **plink.exe** (`C:\Program Files\PuTTY\plink.exe`) — supports `-pw`, доступен если PuTTY установлен. +- Pipe `echo y | plink ...` для auto-accept first-time host key (plink кэширует в registry). +- После push pubkey → переключаемся на native OpenSSH `ssh -i` (plink больше не нужен). + +## 5. Hypervisor-side VNC ≠ guest-side VNC service + +В гостевой системе **нет** VNC service'а — VNC console работает на qemu/KVM hypervisor side. Поэтому request «зайди и передёрни vnc service из гостя» невозможен. Управляется только через Rusonyx web panel. + +## 6. Default firewall — open? + +Из наблюдений 2026-05-20: после reboot SSH:22 поднимался автоматом, не было видно guest-side ufw default-deny. Но **ICMP ping проходил до VNC-fix**, при том что **все TCP-порты были filtered**. Похоже, Rusonyx имеет perimeter firewall, который автоматически allow'ит TCP только когда VPS становится «active» в их учёте — корреляция с моментом первой VNC-сессии. Не воспроизводимо post-factum; для будущих VDS — стоит сначала открыть VNC, потом ждать что SSH станет доступен. + +## 7. Tariff именования + +«160 SSD» переименован в «160 NVMe» (та же цена, апгрейд по IOPS). Если в старой переписке/доках видите «160 SSD» — это actually 160 NVMe. Аналогично могут быть переименования для 80/220+ тарифов. + +## 8. Welcome-email рекомендации + +Содержит только команды apt upgrade, **не упоминает**: +- Что VNC может быть stale. +- Какие dpkg prompts и правильные ответы. +- Initial password — одноразовый, нужно сменить на ключ. + +См. также: support-ticket draft в [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) с конкретными suggestions для Rusonyx. + +## Когда применять + +- Любой новый Rusonyx VDS (Astra Облако). +- Возможно частично применимо к другим Russian VDS-providers с custom OS templates (REG.RU, FirstByte, RuVDS), но конкретные dpkg prompt'ы вендор-specific. + +## Ссылки + +- [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) — где впервые столкнулись. +- [`vds-kzntsv`](../entities/vds-kzntsv.md) — текущий live host. diff --git a/traefik-tcp-passthrough-vs-starttls.md b/traefik-tcp-passthrough-vs-starttls.md new file mode 100644 index 0000000..c368141 --- /dev/null +++ b/traefik-tcp-passthrough-vs-starttls.md @@ -0,0 +1,66 @@ +--- +title: Traefik TCP passthrough vs STARTTLS-protocols +type: concept +tags: [traefik, tls, tcp, sni, postgres, mariadb, mongo, redis, gotcha] +sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md] +updated: 2026-05-20 +--- + +# Traefik TCP passthrough не работает с STARTTLS + +## Симптом + +Traefik TCP router с `HostSNI()` + `tls.passthrough=true`. Mongo / Redis (TLS-from-start) — TLS handshake проходит, виден правильный cert. **Postgres / MariaDB — TLS connection hangs / timeout**, никакого handshake'а не происходит. + +Probe `openssl s_client -connect postgres.vds.kzntsv.site:5432 -servername postgres.vds.kzntsv.site -starttls postgres` → timeout 10s. + +## Root cause + +`tls.passthrough=true` означает: traefik не терминирует TLS, а маршрутизирует TCP-connection raw. Для маршрутизации по `HostSNI` traefik читает SNI **из TLS ClientHello** — первого TLS-сообщения, отправляемого клиентом сразу после TCP handshake. + +**Mongo / Redis** (TLS-from-start) — клиент шлёт TLS ClientHello как первое сообщение. SNI там присутствует. Traefik читает, маршрутизирует, остаток TCP forwarding'ом. Работает. + +**Postgres / MariaDB / MySQL** — используют **STARTTLS pattern**: +1. Клиент после TCP-handshake шлёт `SSLRequest` (plaintext, специфичный для протокола). +2. Сервер отвечает `'S'` (готов на TLS). +3. **Только после этого** клиент начинает TLS ClientHello. + +Первые байты от клиента — **plaintext protocol bytes**, не TLS ClientHello. Traefik ищет SNI в TLS ClientHello, не находит → не может смаршрутизировать → connection hangs (висит на чтении от клиента). + +## Решение — Raw TCP forward + HostSNI(*) + +```yaml +labels: + - traefik.enable=true + - "traefik.tcp.routers.postgres.rule=HostSNI(`*`)" + - traefik.tcp.routers.postgres.entrypoints=postgres + - traefik.tcp.routers.postgres.service=postgres + - traefik.tcp.services.postgres.loadbalancer.server.port=5432 + # NO tls.* — traefik forwards raw TCP без inspect +``` + +Каждая DB — на **своей dedicated entrypoint port** (`postgres:5432`, `mariadb:3306`, `mongo:27017`, `redis:6379`). Routing — по entrypoint, не по SNI. Traefik просто forwards bytes к backend без чтения TLS ClientHello. STARTTLS protocols договариваются о TLS напрямую с DB-контейнером. + +`HostSNI(\`*\`)` — единственное значение для TCP router'а **без** `tls.*` секции (traefik требует какое-то правило, wildcard catch-all уместен здесь). + +## Trade-off + +- ✔ Работает для всех 4 protocols (STARTTLS + TLS-from-start). +- ✔ Uniform config, не нужно ветвить compose под тип protocol'а. +- ✗ Каждая DB требует своего entrypoint:port в traefik static config. 4 DB → 4 entrypoints. Если хотим много инстансов одного движка — теряем muxing на один port через SNI. +- ✗ Traefik в этом случае не видит контент трафика — только TCP-bytes мимо. Невозможно сделать middleware (rate limit, IP allowlist на уровне traefik) — это нужно делать на DB side. + +## Альтернативный путь (отвергнут) + +Traefik TLS termination (без passthrough) + SNI muxing на 443 entrypoint. Не работает для PG/MariaDB по той же причине — STARTTLS встроен в protocol stack, и traefik терминирующий TLS отдаёт plaintext бэкенду, который ждёт SSLRequest и предлагает свой TLS handshake. Двойная TLS обёртка вокруг STARTTLS — broken. + +LE-cert provisioning при passthrough TCP — на DB side, не traefik. Сейчас self-signed; follow-up — lego sidecar extracting из traefik acme.json. + +## Где применено + +Все 4 DBs на [`vds-kzntsv`](../entities/vds-kzntsv.md): postgres:5432, mariadb:3306, mongo:27017, redis:6379. Traefik v2.11 LTS. См. также [`db-tls-self-signed-via-traefik-raw-tcp`](db-tls-self-signed-via-traefik-raw-tcp.md) — связанный паттерн self-signed cert provisioning'а. + +## Ссылки + +- Traefik docs: [TCP routers](https://doc.traefik.io/traefik/routing/routers/#configuring-tcp-routers) +- Сессия где обнаружили: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 2.