4.7 KiB
title, type, tags, sources, updated
| title | type | tags | sources | updated | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Portainer 2.21 `--admin-password` regression + min 12-char policy | concept |
|
|
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):
# 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'ом:
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, запущен через 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-20Phase 1 cont.