docs(.wiki,.tasks): vds-kzntsv-bootstrap done — 3 phases shipped
VDS Rusonyx 160 NVMe (89.253.255.94 / vds.kzntsv.site) активирован 2026-05-20. 3 фазы за ~6 часов: - Phase 1: harden + docker 29.5.1 + traefik v2.11 + portainer 2.21.5 - Phase 2: shared DB park (postgres 16 / mariadb 11.4 / mongo 7 / redis 7) via traefik raw TCP forward + self-signed TLS - Phase 3: миграция gitea (132 repos), verdaccio (2063 pkgs), registry fresh install (user accepted loss old images) + Joxit GUI Ingest 1 source + 1 entity (vds-kzntsv) + 6 concepts (traefik-tcp- passthrough-vs-starttls / portainer-2.21-admin-password-regression / db-tls-self-signed-via-traefik-raw-tcp / rusonyx-vps-onboarding-quirks / registry-gc-mount-and-modify-flag / compose-bcrypt-escape-trap). 3 follow-up ⚪ tasks: vds-gc-cron, vds-backup-rsync-kreknin, vds-ntfy-push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
116
compose-bcrypt-escape-trap.md
Normal file
116
compose-bcrypt-escape-trap.md
Normal file
@@ -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 <<COMPOSE
|
||||
services:
|
||||
app:
|
||||
command: --admin-password '$BCRYPT_ESCAPED'
|
||||
COMPOSE
|
||||
```
|
||||
|
||||
`docker compose config` покажет `$$2y$$05$$...` — это нормально, в runtime expand'ится в `$2y$05$...`.
|
||||
|
||||
**Но осторожно с кавычками**: YAML string form `command: "... '<hash>'"` — после 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.
|
||||
147
db-tls-self-signed-via-traefik-raw-tcp.md
Normal file
147
db-tls-self-signed-via-traefik-raw-tcp.md
Normal file
@@ -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)
|
||||
↓ <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 = `<db>.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
|
||||
- <password>
|
||||
```
|
||||
|
||||
## 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 <db> 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/<engine>/certs/`. Strong random hex32 пароли в `~/projects/.common/secrets/vds-kzntsv.env`.
|
||||
@@ -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.
|
||||
|
||||
85
portainer-2.21-admin-password-regression.md
Normal file
85
portainer-2.21-admin-password-regression.md
Normal file
@@ -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 "<bcrypt-hash>"` (документированный путь 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.
|
||||
115
registry-gc-mount-and-modify-flag.md
Normal file
115
registry-gc-mount-and-modify-flag.md
Normal file
@@ -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.
|
||||
92
rusonyx-vps-onboarding-quirks.md
Normal file
92
rusonyx-vps-onboarding-quirks.md
Normal file
@@ -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.
|
||||
66
traefik-tcp-passthrough-vs-starttls.md
Normal file
66
traefik-tcp-passthrough-vs-starttls.md
Normal file
@@ -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(<hostname>)` + `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.
|
||||
Reference in New Issue
Block a user