Skip to content

fix(cli): warn when manage gets the password as an argument - #849

Merged
hitesh96db merged 4 commits into
mainfrom
fix/manage-password-flag-warning
Nov 10, 2026
Merged

hitesh96db merged 4 commits into
mainfrom
fix/manage-password-flag-warning

Conversation

@LeeroyHannigan

@LeeroyHannigan LeeroyHannigan commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

What

extenddb manage now prints one line on stderr when the password comes from --password:

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 the flag and EXTENDDB_PASSWORD are 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.
  • The choice of source is a pure function, choose_password, in crates/app/src/cmd_manage.rs. resolve_password moves there from manage_http.rs, because cmd_manage.rs is 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.
  • The --password help text names the exposure and the variable. The S-2 entry in docs/technical-debt.md records the warning. It also says that the subcommand flags --admin-password, --new-password, --user-password and --secret-access-key have 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 manage already reads EXTENDDB_PASSWORD when 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 --check and cargo clippy -p extenddb -p extenddb-server -p extenddb-app --all-targets -- -D warnings pass.

  • 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 Authorization header 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 with logging.level = "trace", and with the log_level and sqlx_log_level settings at trace. It served these requests, each with a marker secret:

    • 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
    • a failed and a successful console login

    The log had 4,888 lines, and 1,929 of them were at TRACE. Each marker, the admin password, each base64 Basic credential, Authorization and Basic appeared 0 times.

  • Not fixed here:

    • At trace level, axum 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. The CLI and the console never send such a body.
    • When the rate limiter refuses a principal after five failures, crates/server/src/rate_limit.rs logs the attempted principal at WARN. So a password typed into the user name field reaches the log at the default level.

Checklist

  • I have read CONTRIBUTING.md
  • All tests pass (cargo test --workspace)
  • Code is formatted (cargo fmt --check)
  • Clippy is clean (cargo clippy -- -W clippy::pedantic)
  • I have added or updated tests for new functionality
  • I have updated documentation if behavior changed
  • Breaking changes are noted below (if any)
  • If this changes the wire protocol, Storage trait, auth model, on-disk
    format, 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.

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>
Comment thread crates/app/src/cmd_manage.rs Fixed
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>
@jcshepherd jcshepherd added security release-blocker Must ship for its milestone's release labels Nov 9, 2026
@hitesh96db hitesh96db self-assigned this Nov 9, 2026

@hitesh96db hitesh96db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with EXTENDDB_PASSWORD= (empty): no warning.
  • With neither: the same "No password provided" error as before.
  • manage --help shows the new flag text on one line.

Findings

  1. Major, crates/app/src/cmd_manage.rs:40: no test would catch the warning being removed. The five tests call choose_password and flag_warning, which wrap Option::or and bool::then_some. No test calls resolve_password, the function that passes the real flag to them and calls eprintln!. I changed flag_warning(cli_password.is_some()) to flag_warning(false), which turns the warning off, and all five tests still passed. Deleting the eprintln! would pass too. The crate has no CLI test harness, so the cheapest fix is to give resolve_password a writer, e.g. resolve_password_with(flag, env, &mut impl Write), and test that the flag writes the warning and the variable writes nothing.

  2. Major, docs: the docs contradict the warning.

    • docs/getting-started.md:799 says to use EXTENDDB_ADMIN_PASSWORD instead of --password. manage never reads that variable; only init and the SQLite bootstrap do. A user who follows the note gets "No password provided". The PR is about pointing users at EXTENDDB_PASSWORD, so this one-line fix belongs here.
    • About 100 examples run manage --user admin --password <pw>: about 55 in docs/getting-started.md, about 32 in docs/manuals/05-admin-guide.md and 7 in docs/manuals/04-quickstart-setup-guide.md, plus README.md, AGENTS.md, .agents/skills/extenddb/references/setup/06-iam-first-user.md and the design docs. Each of them now prints "Omit it and set EXTENDDB_PASSWORD instead". The .agents skill references matter most, because agents run them as written, with real passwords on the command line. Suggest read -rs EXTENDDB_PASSWORD; export EXTENDDB_PASSWORD once per section and dropping --password from the examples. If that is too big for this PR, fix line 799 here and open an issue for the rest.
  3. Minor: the subcommand secret flags get no warning, and no issue tracks them. --admin-password, --new-password, --user-password and --secret-access-key carry a new admin's password or an imported secret key, so they are arguably the worse exposure. manage --password X create-admin --admin-password Y warns about X and says nothing about Y. The S-2 row records this. Please open an issue for it as well, so it is not tracked only in a table cell.

  4. Minor, crates/app/src/cmd_manage.rs:31: say why flag_warning exists. It came in with 7e64215, after CodeQL's rust/cleartext-logging flagged the eprintln! (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.

  5. Minor, crates/app/src/cmd_manage.rs:21: an empty EXTENDDB_PASSWORD sends an empty password. With EXTENDDB_PASSWORD= the CLI sends the request instead of answering "No password provided". This predates the PR, but choose_password is 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.

  6. Nits.

    • The S-2 row in docs/technical-debt.md now holds a paragraph inside a table cell. Its location app/cmd_manage.rs should be crates/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. The read -rs suggestion in finding 2 covers this.
    • Once a test of resolve_password exists, only_the_flag_gets_the_warning and the precedence tests of choose_password add little. The error-text test is worth keeping.

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_password next 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.rs logs 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>
@LeeroyHannigan

Copy link
Copy Markdown
Collaborator Author

Changes in 6c1b3aa:

  1. resolve_password_with takes the EXTENDDB_PASSWORD value and a writer for the warning, and resolve_password passes it the variable and stderr. The tests now check what each password source writes. With flag_warning(false) in place of flag_warning(cli_password.is_some()), two of the four tests fail. They also fail when the writeln! is deleted. A failed write of the warning is now ignored, where eprintln! panicked.
  2. Three notes named EXTENDDB_ADMIN_PASSWORD for manage: two in docs/getting-started.md, one of them in the import section, and one in docs/manuals/10-security-model.md. All three now name EXTENDDB_PASSWORD. docs: the manage examples pass the password with --password #915 tracks the 121 lines in 12 files that still show --password, the .agents references among them. Its last paragraph covers the shell history point of your nits.
  3. cli: manage gives no warning for the secret flags of its subcommands #916 tracks the subcommand secret flags, and the S-2 row links to it.
  4. flag_warning has a comment on why it exists.
  5. cli: manage sends an empty password when EXTENDDB_PASSWORD is empty #917 tracks the empty EXTENDDB_PASSWORD.
  6. The S-2 row has the full path, and the file has a new date. The row keeps its paragraph in the cell, as C-11 does. The choose_password and flag_warning tests are gone, because the four resolve_password_with tests cover them. The error-text check stays.

#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>
@LeeroyHannigan

Copy link
Copy Markdown
Collaborator Author

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 docs/getting-started.md also says that manage prints the warning, and docs/manuals/10-security-model.md gets that fact as a list item of its own. The code is the same as in 6c1b3aa.

@hitesh96db hitesh96db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at d62e2ed. The changes since my review at 7e64215 address every finding.

  1. Test of the warning: resolve_password_with now holds both the flag_warning(cli_password.is_some()) call and the writeln!, and resolve_password passes it the real EXTENDDB_PASSWORD value 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.
  2. Docs: at this head, the three password notes (docs/getting-started.md lines 803 and 942, docs/manuals/10-security-model.md) name EXTENDDB_PASSWORD, and two of them now say that manage warns on stderr. #915 tracks the remaining --password examples, the .agents references among them. The --password $EXTENDDB_ADMIN_PASSWORD in 13-event-ticketing-demo.md is a script variable, and #915 lists it.
  3. Subcommand secret flags: #916 tracks them, and the S-2 row links to it.
  4. flag_warning: its doc comment now explains the CodeQL rust/cleartext-logging reason. Alert 173 shows as fixed on the PR ref, and the CodeQL check passes.
  5. Empty EXTENDDB_PASSWORD: #917 tracks it.
  6. Nits: S-2 has the full path, the date is updated, and the redundant choose_password/flag_warning tests 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.

@hitesh96db
hitesh96db added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit e681b8b Nov 10, 2026
56 checks passed
@hitesh96db
hitesh96db deleted the fix/manage-password-flag-warning branch November 10, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-blocker Must ship for its milestone's release security v1 Needed for the 1.0 release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants