Repository navigation
fix(cli): warn when manage gets the password as an argument - #849
Conversation
extenddb manage takes the password from --password or, when the flag is
absent, from the EXTENDDB_PASSWORD environment variable. A password
given with the flag appears in process listings, which other users of
the host can read, and in the history of an interactive shell. The
command now prints one line on stderr when the password came from the
flag:
Warning: --password is visible in process listings and shell history. Omit it and set EXTENDDB_PASSWORD instead.
Nothing else changes. The flag still takes precedence when both are
set, a password from the variable prints nothing, and the output on
stdout is the same. The choice of source is a pure function,
choose_password, with unit tests for the flag, the variable, both and
neither. resolve_password moves from manage_http.rs to its only caller,
cmd_manage.rs, so that manage_http.rs stays under 500 lines. The
--password help text now names the exposure and the variable, and the
S-2 entry in docs/technical-debt.md records the warning.
The management API takes passwords and secret access keys in request
bodies, and Basic auth puts the password in the Authorization header of
every request. The server has no HTTP trace layer and no request
logging middleware, and no log call in the management or console code
formats a body, a header map or a form. To check whether any log level
writes these values, a SQLite build of the server ran with
logging.level = "trace" and the log_level and sqlx_log_level settings
at trace. It served create-admin, change-admin-password, create-user
with a console password, change-user-password and import-access-key, a
wrong Basic auth password for an admin and for an IAM user, and a
failed and a successful console login, each with a marker secret. The
whole log held 4,888 lines, 1,929 of them at TRACE. Each marker, the
admin password, each base64 Basic credential, "Authorization" and
"Basic " appeared 0 times.
At trace level, axum also logs the parse error of a refused JSON body.
For the bodies that carry a secret, that error quotes a value in two
cases: a field that must hold a string holds a number or a boolean, for
example a password sent as a JSON number, or the whole body is a bare
JSON string, number or boolean. Neither the CLI nor the console sends
such a body.
Signed-off-by: Lee Hannigan <29015211+LeeroyHannigan@users.noreply.github.com>
CodeQL reported cleartext logging at the eprintln that prints the --password warning. choose_password returned the password and the warning in one tuple, so the analysis traced the printed warning to the password. The printed text was always the fixed warning. choose_password now returns the password only. A new function, flag_warning, returns the warning when the command line has --password and None otherwise, and resolve_password calls it with cli_password.is_some(). The flag still takes precedence, so the warning appears exactly when the password came from the flag, and the output does not change. The four choose_password tests stay without their warning checks, and a fifth test covers flag_warning. Signed-off-by: Lee Hannigan <29015211+LeeroyHannigan@users.noreply.github.com>
hitesh96db
left a comment
There was a problem hiding this comment.
The code does what the PR says, and I found no bugs in it. Two things should change before merge: the tests do not check the warning itself, and the docs still steer users to the flag, with one note naming the wrong variable.
I checked out 7e64215 and ran cargo test -p extenddb-app cmd_manage (5 passed). cargo clippy -p extenddb-app --all-targets -- -D warnings and cargo fmt --all -- --check pass on 1.97.0. I built a SQLite binary and ran manage ... --endpoint 127.0.0.1:1 list-admins:
- With
--password: the warning prints once, then the connection error. - With
EXTENDDB_PASSWORD, or withEXTENDDB_PASSWORD=(empty): no warning. - With neither: the same "No password provided" error as before.
manage --helpshows the new flag text on one line.
Findings
-
Major,
crates/app/src/cmd_manage.rs:40: no test would catch the warning being removed. The five tests callchoose_passwordandflag_warning, which wrapOption::orandbool::then_some. No test callsresolve_password, the function that passes the real flag to them and callseprintln!. I changedflag_warning(cli_password.is_some())toflag_warning(false), which turns the warning off, and all five tests still passed. Deleting theeprintln!would pass too. The crate has no CLI test harness, so the cheapest fix is to giveresolve_passworda writer, e.g.resolve_password_with(flag, env, &mut impl Write), and test that the flag writes the warning and the variable writes nothing. -
Major, docs: the docs contradict the warning.
docs/getting-started.md:799says to useEXTENDDB_ADMIN_PASSWORDinstead of--password.managenever reads that variable; onlyinitand the SQLite bootstrap do. A user who follows the note gets "No password provided". The PR is about pointing users atEXTENDDB_PASSWORD, so this one-line fix belongs here.- About 100 examples run
manage --user admin --password <pw>: about 55 indocs/getting-started.md, about 32 indocs/manuals/05-admin-guide.mdand 7 indocs/manuals/04-quickstart-setup-guide.md, plusREADME.md,AGENTS.md,.agents/skills/extenddb/references/setup/06-iam-first-user.mdand the design docs. Each of them now prints "Omit it and set EXTENDDB_PASSWORD instead". The.agentsskill references matter most, because agents run them as written, with real passwords on the command line. Suggestread -rs EXTENDDB_PASSWORD; export EXTENDDB_PASSWORDonce per section and dropping--passwordfrom the examples. If that is too big for this PR, fix line 799 here and open an issue for the rest.
-
Minor: the subcommand secret flags get no warning, and no issue tracks them.
--admin-password,--new-password,--user-passwordand--secret-access-keycarry a new admin's password or an imported secret key, so they are arguably the worse exposure.manage --password X create-admin --admin-password Ywarns aboutXand says nothing aboutY. The S-2 row records this. Please open an issue for it as well, so it is not tracked only in a table cell. -
Minor,
crates/app/src/cmd_manage.rs:31: say whyflag_warningexists. It came in with 7e64215, after CodeQL'srust/cleartext-loggingflagged theeprintln!(alert 173). Without that context, the constant, the two helpers and the "pick the warning before the password" order look like over-engineering, and a later tidy-up that inlines them would bring the alert back. One comment line would cover it: the printed value depends only on whether the flag was given, so it never holds the password, and CodeQL can see that. -
Minor,
crates/app/src/cmd_manage.rs:21: an emptyEXTENDDB_PASSWORDsends an empty password. WithEXTENDDB_PASSWORD=the CLI sends the request instead of answering "No password provided". This predates the PR, butchoose_passwordis now the function that decides "no password". An empty password counts as a failed login, so a script whose variable is unset can lock that principal out from its address after five runs. Consider treating an empty value as unset, with a test. -
Nits.
- The S-2 row in
docs/technical-debt.mdnow holds a paragraph inside a table cell. Its locationapp/cmd_manage.rsshould becrates/app/src/cmd_manage.rs, and the file's "Last updated: 2026-09-16" was not changed. - "set EXTENDDB_PASSWORD instead" keeps the password out of process listings, but
EXTENDDB_PASSWORD=x extenddb manage ...typed on one line still lands in shell history. Theread -rssuggestion in finding 2 covers this. - Once a test of
resolve_passwordexists,only_the_flag_gets_the_warningand the precedence tests ofchoose_passwordadd little. The error-text test is worth keeping.
- The S-2 row in
What works
- The precedence, the error text, stdout and the exit status are unchanged, and the warning goes to stderr only, so scripts that parse stdout are not affected.
- Moving
resolve_passwordnext to its only caller and making it private is the right cleanup. - The trace-level log check in the PR body found something real: when the rate limiter blocks a principal,
rate_limit.rslogs the attempted user name at WARN, so a password typed into the user field reaches the log at the default level. That deserves its own issue. - The branch is 74 commits behind main, but main has not touched the four changed files, and it merges cleanly.
I would ask for findings 1 and 2 before merge: a test that fails without the warning, and at least the line 799 fix. Finding 4 is one comment line. Findings 3 and 5 can be follow-up issues.
The tests called choose_password and flag_warning, but not the function that prints the warning, so a change that removed the warning still passed them. resolve_password_with now takes the EXTENDDB_PASSWORD value and a writer for the warning, and resolve_password passes it the variable and stderr. The tests check what each password source writes. The flag writes the warning once, without the password. The variable writes nothing, and so does the error when neither is set. A failed write of the warning no longer stops the command, where eprintln! panicked. flag_warning gets a comment on why it exists. CodeQL's rust/cleartext-logging query traces printed text back to the password when both come from one function result. Two notes in docs/getting-started.md and one line of docs/manuals/10-security-model.md named EXTENDDB_ADMIN_PASSWORD for manage. Only init and the SQLite bootstrap read that variable. All three now name EXTENDDB_PASSWORD, the variable that manage reads. The S-2 row of docs/technical-debt.md gets its file path and a link to #916, and the file gets a new date. Signed-off-by: Lee Hannigan <29015211+LeeroyHannigan@users.noreply.github.com>
|
Changes in 6c1b3aa:
#918 tracks the rate limiter, which logs the attempted principal at WARN. |
#842 rewrote the three notes that this branch changed: two in docs/getting-started.md and one in docs/manuals/10-security-model.md. Main's wording already names EXTENDDB_PASSWORD, and its two getting-started notes give the read -rs form, so the merge keeps it. The first getting-started note now also says that manage prints a warning on stderr when it gets --password, which this branch adds. The security model gets the same fact as a list item of its own. Signed-off-by: Lee Hannigan <29015211+LeeroyHannigan@users.noreply.github.com>
|
I merged main in d62e2ed, because #842 rewrote the same three password notes and the branch conflicted. The merge keeps main's text for all three. The first note in |
hitesh96db
left a comment
There was a problem hiding this comment.
Approving at d62e2ed. The changes since my review at 7e64215 address every finding.
- Test of the warning:
resolve_password_withnow holds both theflag_warning(cli_password.is_some())call and thewriteln!, andresolve_passwordpasses it the realEXTENDDB_PASSWORDvalue and stderr. The four tests compare the exact text each password source writes, so the two flag tests depend on that call and that write. - Docs: at this head, the three password notes (
docs/getting-started.mdlines 803 and 942,docs/manuals/10-security-model.md) nameEXTENDDB_PASSWORD, and two of them now say thatmanagewarns on stderr. #915 tracks the remaining--passwordexamples, the.agentsreferences among them. The--password $EXTENDDB_ADMIN_PASSWORDin13-event-ticketing-demo.mdis a script variable, and #915 lists it. - Subcommand secret flags: #916 tracks them, and the S-2 row links to it.
flag_warning: its doc comment now explains the CodeQLrust/cleartext-loggingreason. Alert 173 shows as fixed on the PR ref, and the CodeQL check passes.- Empty
EXTENDDB_PASSWORD: #917 tracks it. - Nits: S-2 has the full path, the date is updated, and the redundant
choose_password/flag_warningtests are gone. The error-text test stays.
#918 for the rate limiter logging the attempted principal is a good catch to have tracked.
What I checked: the code and docs diff against main, the four issues, the CodeQL alert state, and the 56 CI checks on this head, which all pass, run-tests (1.88.0) included. This round I did not run the tests, the mutants or the binary myself.
What
extenddb managenow prints one line on stderr when the password comes from--password:EXTENDDB_PASSWORDare set. A password from the variable prints no warning. The output on stdout and the exit status do not change. With neither, the error text is the same as before.choose_password, incrates/app/src/cmd_manage.rs.resolve_passwordmoves there frommanage_http.rs, becausecmd_manage.rsis its only caller. A second pure function,flag_warning, gives the warning. It takes only whether the command line has--password, so the printed text never depends on the password.--passwordhelp text names the exposure and the variable. The S-2 entry indocs/technical-debt.mdrecords the warning. It also says that the subcommand flags--admin-password,--new-password,--user-passwordand--secret-access-keyhave the same exposure, no environment variable and no warning.Why
A password given with the flag appears in process listings, which other users of the host can read. It also appears in the history of an interactive shell.
extenddb managealready readsEXTENDDB_PASSWORDwhen the flag is absent.Testing done
4 new unit tests cover the flag, the variable, both and neither. Each one also checks whether the warning prints. On main at e681b8b,
cargo test -p extenddb-app: 54 passed.cargo fmt --checkandcargo clippy -p extenddb -p extenddb-server -p extenddb-app --all-targets -- -D warningspass.End to end: with the flag, and with both, the warning prints once. With the variable, and with neither, no warning prints. The output on stdout is the same with the flag and with the variable.
Log check. The management API takes passwords and secret access keys in request bodies, and Basic auth puts the password in the
Authorizationheader of each request. The server has no HTTP trace layer and no request logging middleware. No log call in the management or console code formats a body, a header map or a form. To check the log itself, I ran a SQLite build withlogging.level = "trace", and with thelog_levelandsqlx_log_levelsettings attrace. It served these requests, each with a marker secret:The log had 4,888 lines, and 1,929 of them were at TRACE. Each marker, the admin password, each base64 Basic credential,
AuthorizationandBasicappeared 0 times.Not fixed here:
crates/server/src/rate_limit.rslogs the attempted principal at WARN. So a password typed into the user name field reaches the log at the default level.Checklist
cargo test --workspace)cargo fmt --check)cargo clippy -- -W clippy::pedantic)Storagetrait, auth model, on-diskformat, or public CLI surface, an RFC has been accepted or is linked
below. Otherwise, an ADR captures the decision (link below).
ADR / RFC: n/a. The change adds one line on stderr. The flags, the output on stdout and the exit status do not change.
Merging with #140
#140 rewrites the S-1 row of
docs/technical-debt.md, the row above the S-2 row that this PR changes. The PR that merges second keeps both rows.By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache License 2.0 and I agree to the Developer Certificate of
Origin (DCO). See CONTRIBUTING.md for details.