diff --git a/src/containers/portainer/update.sh b/src/containers/portainer/update.sh index 2658bd2..39b30e2 100755 --- a/src/containers/portainer/update.sh +++ b/src/containers/portainer/update.sh @@ -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}" - ADMIN_PW="$(vm "sudo cat ${ADMIN_SECRET}" | tr -d '\r')" +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=". @@ -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;