portainer: gate the admin bootstrap on being able to log in

The previous guard was 'does the password file exist'. The first lab1 install
wrote the password, then failed init on the missing setup token — leaving a file
that made every later run skip the bootstrap and then fail to authenticate, so
OIDC never got configured. Ask Portainer instead: try /api/auth, and bootstrap
only when that yields no token. The JWT is then reused for the settings PUT
rather than logging in twice.
This commit is contained in:
Lars Rossen 2026-08-24 20:58:22 +02:00
parent 77f361120a
commit 1c48eb4634

View file

@ -123,16 +123,28 @@ for _ in $(seq 1 24); do
done
[[ "${UP}" -eq 1 ]] || die "Portainer did not answer on :9443"
# ── 4. Bootstrap the local admin (once) ──────────────────────────────
# Portainer refuses admin creation after a timeout window, so this must happen
# promptly after first start. The password is kept on the VM for break-glass
# access — OIDC is the everyday path, this is the account that survives an
# Authentik outage.
# ── 4. Bootstrap the local admin ─────────────────────────────────────
# The password is kept on the VM for break-glass access — OIDC is the everyday
# path, this is the account that survives an Authentik outage.
#
# The guard is "can we log in?", NOT "does the password file exist": a failed
# init leaves a password file behind, and keying off the file makes every later
# run skip the bootstrap forever (seen on the first lab1 install).
if ! vm "sudo test -s ${ADMIN_SECRET}" 2>/dev/null; then
info " Bootstrapping the break-glass admin account..."
vm "sudo install -d -m 0700 \$(dirname ${ADMIN_SECRET})"
vm "openssl rand -base64 24 | sudo tee ${ADMIN_SECRET} >/dev/null && sudo chmod 0600 ${ADMIN_SECRET}"
fi
ADMIN_PW="$(vm "sudo cat ${ADMIN_SECRET}" | tr -d '\r')"
portainer_jwt() {
vm "curl -sk -X POST https://localhost:9443/api/auth \
-H 'Content-Type: application/json' \
-d '{\"Username\":\"admin\",\"Password\":\"${ADMIN_PW}\"}'" 2>/dev/null | jq -r '.jwt // empty'
}
JWT="$(portainer_jwt)"
if [[ -z "${JWT}" ]]; then
info " Bootstrapping the break-glass admin account..."
# Portainer >= 2.39 prints a one-time setup token at startup and refuses
# admin creation without it (HTTP 403). The log line wraps the value in ANSI
# colour codes, so match the 64-hex token rather than "setup_token=<value>".
@ -144,18 +156,18 @@ if ! vm "sudo test -s ${ADMIN_SECRET}" 2>/dev/null; then
-d '{\"Username\":\"admin\",\"Password\":\"${ADMIN_PW}\"}'" || echo 000)"
case "${init_code}" in
200|204) info " admin created (password in ${ADMIN_SECRET} on the VM)" ;;
409) info " admin already existed — keeping it" ;;
409) warn " an admin exists but ${ADMIN_SECRET} does not match it — reset it by hand" ;;
*) warn " admin init returned HTTP ${init_code}; configure the admin by hand at ${UI_URL}" ;;
esac
JWT="$(portainer_jwt)"
else
ADMIN_PW="$(vm "sudo cat ${ADMIN_SECRET}" | tr -d '\r')"
info " break-glass admin already provisioned"
fi
# ── 5. Point Portainer at TAPPaaS identity (OIDC) ────────────────────
# identity:identity wrote these three values; if they are missing the module is
# still usable with the local admin, so warn rather than fail.
if [[ -n "${ADMIN_PW:-}" ]] && vm "sudo test -s ${SECRETS_ENV}" 2>/dev/null; then
if [[ -n "${JWT:-}" ]] && vm "sudo test -s ${SECRETS_ENV}" 2>/dev/null; then
info " Configuring OIDC login against Authentik..."
CLIENT_ID="$(vm "sudo sh -c '. ${SECRETS_ENV}; printf %s \"\$OIDC_CLIENT_ID\"'" | tr -d '\r')"
CLIENT_SECRET="$(vm "sudo sh -c '. ${SECRETS_ENV}; printf %s \"\$OIDC_CLIENT_SECRET\"'" | tr -d '\r')"
@ -169,9 +181,6 @@ if [[ -n "${ADMIN_PW:-}" ]] && vm "sudo test -s ${SECRETS_ENV}" 2>/dev/null; the
USER_URI="$(jq -r '.userinfo_endpoint // empty' <<<"${WELL_KNOWN}")"
if [[ -n "${AUTH_URI}" && -n "${TOKEN_URI}" && -n "${USER_URI}" ]]; then
JWT="$(vm "curl -sk -X POST https://localhost:9443/api/auth \
-H 'Content-Type: application/json' \
-d '{\"Username\":\"admin\",\"Password\":\"${ADMIN_PW}\"}'" | jq -r '.jwt // empty')"
if [[ -n "${JWT}" ]]; then
# AuthenticationMethod 3 = OAuth. OAuthAutoCreateUsers lets a
# TAPPaaS identity log in without an admin pre-creating it;