v1.3.11 — argos user reset-password CLI¶
Why this exists¶
Reported during dogfooding: an operator forgot the admin password after a deploy. The browser panel has no public reset endpoint -- by design, since "anyone can reset the admin password" is a worse failure mode than "operator must shell into the box". The OS-level /argos --help flag printed the server banner and bound-already-in-use error rather than a CLI surface, because the binary parsed unknown args silently and fell through to run(). Recovery required an out-of-band SQLite hack: stop the container, generate a bcrypt hash by hand with htpasswd, UPDATE users SET password_hash=... directly. Tedious and error-prone.
What's new¶
argos --help¶
Prints the actual CLI surface. Was previously silent.
$ docker compose exec argos /argos --help
argos 1.3.11 -- self-hosted edge gateway
Usage:
argos start the HTTP server (default)
argos server start the HTTP server (explicit)
argos user list list panel users
argos user reset-password <user> reset a user's password
argos disable-2fa --user <user> --yes remove TOTP for a locked-out user
argos migrate ... run / rollback DB migrations
argos restore ... stage a backup for restore
argos -h | --help show this help
argos user list¶
$ docker compose exec argos /argos user list
ID USERNAME TOTP PWD CREATED
--------------------------------------------------------------------------------
1 admin off yes 2026-04-24T16:34:10Z
PWD: yes means the row has a local password hash; OIDC-only rows show -. TOTP: on reflects the users.totp_enabled column.
argos user reset-password <user>¶
Two modes:
Interactive (default; recommended):
$ docker compose exec -it argos /argos user reset-password admin
New password: <hidden>
Confirm new password: <hidden>
password reset for user "admin" (user_id=1) at 2026-04-25T08:41:10Z
Echo is suppressed via golang.org/x/term.ReadPassword when stdin is a TTY (-it on docker compose exec). The prompt runs twice and the two reads must match -- prevents fat-finger resets.
Non-interactive (for scripts):
$ docker compose exec argos /argos user reset-password admin --password 'NewPassRotated1!'
password reset for user "admin" (user_id=1) at 2026-04-25T08:41:10Z
The --password flag leaks the value to the operator's shell history. Use only when an interactive operator isn't going to run it.
Both modes:
- Validate the user exists; bail out with a "run
argos user list" hint otherwise. - Enforce the same minimum length (8 chars) the panel API enforces -- via
auth.HashPasswordso the rule lives in one place. - Generate a bcrypt cost-12 hash (
auth.BcryptCostconstant). UPDATE users SET password_hash=? WHERE id=?in a single transaction.- Insert an
audit / password_resetrow intolog_entrieswithsource: cliso the operator can see the event in the panel Logs tab after logging back in. Audit failures are logged to stderr but don't fail the command -- the password is already changed. - SQLite WAL mode means the running panel keeps serving while the CLI writes; the next login attempt picks up the new hash. No container restart required.
Why this is a separate command from disable-2fa¶
The existing argos disable-2fa --user <user> --yes covers "locked out of TOTP". This release adds the matching "locked out of password" path. They share the same env contract (ARGOS_DB_PATH), the same audit-log shape, and deliberately don't compose into a single "reset everything" command -- the operator should know which credential is broken.
Bug found mid-smoke¶
First version of the command was tested with argos user reset-password admin --password X and parsed it as "username = admin, then 2 unexpected positional args (--password, X)". Go's flag.Parse stops at the first non-flag arg by default; the natural operator-typed shape "positional then flags" doesn't work without explicit handling.
Fixed by extracting the username manually as args[0] before constructing the FlagSet, then parsing the remainder. Verified both forms work and added TestRunUserResetPasswordParsesPositionalThenFlags to lock the behaviour in.
Smoke results (prod stack argos-prod-argos:v1.3.11)¶
$ docker exec argos-prod-panel /argos user list
ID USERNAME TOTP PWD CREATED
--------------------------------------------------------------------------------
1 admin off yes 2026-04-24T16:34:10Z
$ docker exec argos-prod-panel /argos user reset-password admin --password 'NewPassRotated1!'
password reset for user "admin" (user_id=1) at 2026-04-25T08:41:02Z
$ curl -X POST http://localhost:9180/api/auth/login \
-d '{"username":"admin","password":"NewPassRotated1!"}'
{"username":"admin"} # logged in
$ curl -X POST http://localhost:9180/api/auth/login \
-d '{"username":"admin","password":"OLD-password"}'
{"error":"invalid credentials"} # old hash now rejected
$ sqlite3 argos.db "SELECT message FROM log_entries WHERE raw LIKE '%password_reset%' ORDER BY id DESC LIMIT 1"
password reset via CLI
Restored the original prod password via the same command after the smoke run. The panel didn't restart at any point.
Tests¶
8 unit tests in backend/cmd/argos/cli_user_test.go:
TestResetPasswordNonInteractiveUpdatesHash-- new password validates via bcrypt; old password rejected.TestResetPasswordWritesAuditRow-- exactly onecli / password_resetrow inserted intolog_entries.TestResetPasswordRejectsShortPassword-- "must be at least 8 characters" surfaced verbatim fromauth.HashPassword, hash NOT touched on failure.TestResetPasswordUnknownUser-- error mentions the offending username.TestResetPasswordInteractiveMatchAndMismatch-- two sub-tests: matching prompts succeed and update; mismatching prompts return "passwords do not match" without touching the DB.TestResetPasswordRequiresDBPath-- missingARGOS_DB_PATH(and no--dbflag) errors cleanly.TestRunUserResetPasswordParsesPositionalThenFlags-- natural arg order works; bare--password X(no username) errors.TestResetPasswordRejectsBlankUsername-- whitespace-only username rejected before any DB connection is opened.
Files changed¶
backend/cmd/argos/main.go-- top-level dispatcher gainsuser,server,--help/-h/helpcases. NewprintRootUsagewriter.backend/cmd/argos/cli_user.go(new) -- 200 lines.runUserCommand,runUserResetPassword,runUserList,resetPasswordWithOpts(testable core),readPasswordFromTerm.backend/cmd/argos/cli_user_test.go(new) -- 8 tests.backend/go.mod/backend/go.sum--x/termpromoted to direct dep. Tidy also promoted go-oidc/v3 + oauth2 (already used directly elsewhere; metadata cleanup only).docs/operations/troubleshooting.md-- new "Forgot admin password" section with interactive / non-interactive / panel-down / sqlite3-fallback recovery paths.
Upgrade¶
The CLI is in the binary; no migration / config / volume work. Existing disable-2fa / migrate / restore subcommands are unchanged.
Not changed¶
- The HTTP API has no password-reset endpoint, by design. CLI only.
disable-2faretains its--yesconfirmation flag;reset-passworddoes NOT require a confirmation flag because the prompt-twice interactive flow already serves that role and the non-interactive flow is meant for non-confirmation scripts.- Audit-row schema unchanged; the new entries fit the existing
source / level / message / rawshape used bydisable-2faand the in-processlogs.Recorder.