Files
admin/.wiki/concepts/compose-bcrypt-escape-trap.md

117 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.