Administrator recovery
Use this procedure when the first admin account has lost its password, authenticator, and recovery codes. For a user's normal password or TOTP change, use Account and team.
Use this procedure when the first admin account has lost its password, authenticator, and recovery codes. For a user's normal password or TOTP change, use Account and team.
Reset operations
RESET_TOKEN authorizes two operations with very different impact:
| Endpoint | Intended use | Effect |
|---|---|---|
POST /api/auth/reset-via-token |
Recover the fixed admin account |
Changes its password, removes TOTP and recovery codes, and increments its authentication version so older admin sessions are rejected |
POST /api/auth/force-reset |
Destructive instance reset | Deletes application data and returns OrbitPage to first-run state |
Use reset-via-token for account recovery. force-reset deletes instance data. Create and verify an infrastructure backup before enabling RESET_TOKEN; see Maintenance.
The shared reset limiter permits only two attempts per hour per source IP. Generate a strong token, use the exact request once, and do not test guessed values repeatedly.
Configure or rotate it on an installer-managed host
The installer stores runtime secrets in /etc/orbitpage/orbitpage.env. The following script replaces any prior RESET_TOKEN with a new 256-bit value without printing it or placing it in process arguments:
sudo bash <<'EOF'
set -euo pipefail
env_file=/etc/orbitpage/orbitpage.env
test -f "$env_file"
umask 077
tmp="$(mktemp "${env_file}.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
line=''
while IFS= read -r line || test -n "$line"; do
case "$line" in
RESET_TOKEN=*) continue ;;
esac
printf '%s\n' "$line" >> "$tmp"
done < "$env_file"
printf 'RESET_TOKEN=%s\n' "$(openssl rand -hex 32)" >> "$tmp"
chmod 0600 "$tmp"
chown root:root "$tmp"
mv "$tmp" "$env_file"
trap - EXIT
EOF
Recreate the container so it receives the changed environment, then wait for health:
sudo docker compose \
--project-name orbitpage \
--project-directory /opt/orbitpage \
--file /opt/orbitpage/compose.yaml \
up -d --force-recreate
sudo orbitpage start
orbitpage restart and docker restart reuse the old container environment; they do not load an edited env file. A recreation or platform redeploy is required.
Other deployment models
- Manual Docker or Compose: keep
RESET_TOKENin the same root-owned0600env file as the other runtime settings, recreate the container with--env-fileor Composeenv_file, and never pass the value with-eon the command line. - Managed container platform: create a secret in the platform secret store, map it to
RESET_TOKEN, and deploy a new revision with exactly one OrbitPage replica. - Source/service deployment: use the service manager's credential or environment-file support. Restrict the file to the service account and restart the service through that manager.
Users with root or Docker-daemon access can inspect container environment variables. Restrict that access even when a protected env file or secret store is used.
Recover the admin account safely
Use a trusted root console on the OrbitPage host or guest. Avoid a shared shell, terminal recording, debug tracing, and public HTTP.
Configure or rotate
RESET_TOKEN, recreate the container, and confirm/healthis successful.Run the following from an installer-managed host. It reads the recovery secret from the protected env file, prompts for the password through the terminal, and sends both values to the container over standard input. Neither value is placed in command arguments or printed.
The new password must contain at least eight characters, including uppercase, lowercase, a number, and a special character.
sudo bash <<'EOF' set -euo pipefail RESET_TOKEN="$(awk -F= '$1 == "RESET_TOKEN" { print substr($0, index($0, "=") + 1); exit }' /etc/orbitpage/orbitpage.env)" ORBITPAGE_CONTAINER_NAME="$(awk -F= '$1 == "ORBITPAGE_CONTAINER_NAME" { print substr($0, index($0, "=") + 1); exit }' /opt/orbitpage/.env)" : "${RESET_TOKEN:?RESET_TOKEN is not configured}" : "${ORBITPAGE_CONTAINER_NAME:?OrbitPage container name is unavailable}" trap 'unset RESET_TOKEN new_password confirm_password' EXIT IFS= read -r -s -p 'New admin password: ' new_password </dev/tty printf '\n' >/dev/tty IFS= read -r -s -p 'Confirm new admin password: ' confirm_password </dev/tty printf '\n' >/dev/tty test "$new_password" = "$confirm_password" || { printf 'Passwords do not match.\n' >&2 exit 1 } printf '%s\000%s' "$RESET_TOKEN" "$new_password" | docker exec -i "$ORBITPAGE_CONTAINER_NAME" node -e ' const chunks = []; process.stdin.on("data", (chunk) => chunks.push(chunk)); process.stdin.on("end", async () => { try { const input = Buffer.concat(chunks).toString("utf8"); const separator = input.indexOf("\0"); if (separator < 0) throw new Error("Recovery input was incomplete."); const token = input.slice(0, separator); const newPassword = input.slice(separator + 1); const port = process.env.PORT || "8080"; const response = await fetch(`http://127.0.0.1:${port}/api/auth/reset-via-token`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, newPassword }), }); const responseBody = await response.text(); console.log(responseBody); if (!response.ok) process.exitCode = 1; } catch (error) { console.error(error instanceof Error ? error.message : "Recovery request failed."); process.exitCode = 1; } });' EOFThe loopback request remains valid when a public
BASE_PATHis configured. For a manually named container, use that name and read the secret from its protected env file; do not copy the value into acurlargument or shell history.Require all of these checks before closing the recovery:
- the response reports a successful password and two-factor reset;
- an already-open browser session for
adminis rejected after refresh or its next protected API request; - the new password can log in without an old TOTP challenge;
- a different user account, if configured, is unaffected;
- TOTP is enrolled again and the new single-use recovery codes are stored securely.
The session-revocation check matters:
reset-via-tokeninvalidates existing sessions foradmin, not every additional dashboard user.Remove or rotate
RESET_TOKEN, recreate the container, and verify that the old recovery window is closed.
Do not rotate JWT_SECRET as part of this procedure. Changing JWT_SECRET invalidates all signed sessions and can make encrypted TOTP or dashboard-saved provider secrets unreadable.
Remove it after recovery
Unless an incident policy requires a continuously available recovery secret, remove it immediately after the new password and TOTP enrollment are verified:
sudo bash <<'EOF'
set -euo pipefail
env_file=/etc/orbitpage/orbitpage.env
test -f "$env_file"
umask 077
tmp="$(mktemp "${env_file}.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
line=''
while IFS= read -r line || test -n "$line"; do
case "$line" in
RESET_TOKEN=*) continue ;;
esac
printf '%s\n' "$line" >> "$tmp"
done < "$env_file"
chmod 0600 "$tmp"
chown root:root "$tmp"
mv "$tmp" "$env_file"
trap - EXIT
EOF
sudo docker compose \
--project-name orbitpage \
--project-directory /opt/orbitpage \
--file /opt/orbitpage/compose.yaml \
up -d --force-recreate
sudo orbitpage start
Verify that the new container no longer has the variable without displaying any environment values:
CONTAINER="$(sudo awk -F= '$1 == "ORBITPAGE_CONTAINER_NAME" { print substr($0, index($0, "=") + 1) }' /opt/orbitpage/.env)"
test -n "$CONTAINER"
sudo docker exec "$CONTAINER" node -e 'process.exit(process.env.RESET_TOKEN ? 1 : 0)'
Exit status 0 confirms removal. To keep recovery enabled, rotate it with the configuration procedure instead, recreate the container, and invalidate the old secret in the external secret manager or password vault.