--- 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.