Repository navigation
fix(dev-mode): let operators drop the public example credential - #876
hitesh96db wants to merge 1 commit into
Conversation
The dev image binds 0.0.0.0 inside its container, serves plain HTTP, opens authorization, and always seeds AWS's public example key pair. Published with a bare `-p 18080:18080` instead of the documented `-p 127.0.0.1:18080:18080`, anyone who can reach the host's port can read, change and delete every table. The server cannot see the host's port mapping, so it cannot tell which case it is in. Add EXTENDDB_DEV_DISABLE_EXAMPLE_CREDENTIAL. Set to 1 or true, it drops the example pair, so only the EXTENDDB_DEV_* pair verifies. It requires that pair, so the server never starts with no credential, rejects any value other than 1, true, 0 or false, so a typo cannot leave the public key active, and removes an example key an earlier boot stored in a file-backed catalog, which seeding alone would leave verifying. The default is unchanged: tools that hardcode the example pair keep working. The dev banner said "plain HTTP on loopback" whatever the bind was. It now names the bind address and, when that is not loopback, prints a warning block: what exposure means, the loopback publish command, and the new switch. The non-loopback log warning names the switch too. Move the "local only, not for shared networks" warning to the top of the dev image's Docker Hub overview and docs/dev-image.md, and document the switch. Publish the MongoDB overview's example on loopback, as the text beneath it already says it is.
LeeroyHannigan
left a comment
There was a problem hiding this comment.
No test covers the catalog removal that makes the switch work on a file-backed database, and the docs overstate what the switch blocks.
This PR adds EXTENDDB_DEV_DISABLE_EXAMPLE_CREDENTIAL. Setting it stops the dev server from seeding AWS's example key pair and deletes a stored copy from the dev user. The PR also rewrites the non-loopback dev banner and moves the shared-network warning to the top of the dev docs.
I built the head with Rust 1.88.0 (sqlite,dev-mode), ran it on 127.0.0.1 against one SQLite file, and signed requests with botocore:
- Default start: the example key created a table and wrote an item.
- Restart with the switch and a team pair: the example key got
UnrecognizedClientExceptionand the team key read the item. The log showed the removal line and did not contain the team secret. - Second restart with
true: the server still refused the example key. - The switch with no pair, or set to
yes,TRUEoron: the server refused to start with the messages in the PR body. - A second server on the same file: the serve lock stopped it.
extenddb healthcheckwith the switch on: passed.
cargo test -p extenddb-server -p extenddb-app passes on 1.88.0, except five backup_store tests that reject my machine's temp directory. cargo fmt --check and cargo clippy -D warnings pass on 1.97.0. I put main's behaviour into the production code under the new function signatures and ran the new tests against it. Nine of the 13 new tests failed, and so did the extended describe_prints_builtin_secret_but_never_custom_secret. The four that passed check behaviour main already has or code the PR moved: disabling_the_example_pair_with_a_half_set_pair_names_the_missing_var, loopback_hosts_are_recognised, loopback_dev_banner_has_no_exposure_warning and dev_banner_keeps_the_ignored_aws_key_note. The three docs edits match the code, apart from findings 3 and 5.
Findings
-
Major,
crates/server/src/dev_credentials.rs:289: no test covers the catalog removal. I deleted lines 289 to 307, and only the fivebackup_storetests failed. A binary with that change accepted the example key after a restart with the switch, and its banner listed only the team key. The image keeps its database in a file, so a restart or a reused volume runs this code. Add a test that seeds one SQLite file twice, first with the example pair and then with a team pair only. Assert thatlookup_credential("AKIAIOSFODNN7EXAMPLE")returnsNone.ci/smoke-test-dev-container.shcan also restart its file-mode container once with the switch. -
Minor,
crates/server/src/dev_credentials.rs:294: the delete targets userdevin the default account and ignoresNotFound, so an example key on another user keeps working. With the switch on, I usedextenddb manageto import the example pair for a new useralice, asdocs/getting-started.mdshows. After a restart with the switch, the example key still listed tables and wrote an item. No removal line appeared, and the banner named only the team key. Find the owner withcredential_store.lookup_credential(EXAMPLE_ACCESS_KEY_ID). Delete the key from thataccount_idandprincipal_name. -
Minor,
docs/dev-image.md:78: "theEXTENDDB_DEV_*pair is the only credential the server accepts" does not match the code. I started with the switch and team key 1, then restarted with the switch and team key 2. Team key 1 still listed tables. Keys added withextenddb managealso keep working (finding 2). State that the switch stops seeding the example pair and deletes it from thedevuser. Add that keys stored by earlier starts keep working until you recreate the volume. -
Optional,
crates/app/src/cmd_serve.rs:129: thistracing::warn!runs beforeserve_innerinstalls a tracing subscriber, so it never prints. WithEXTENDDB_DEV_ALLOW_ANY_BIND=1andserver.bind_addr = 192.0.2.1, the only output was the bind error. The call predates this PR. The banner block now prints this warning. Delete the call, or emit it after logging starts. -
Optional,
docs/dev-image.md:81: the line says the server refuses a value other than the four it lists, but the server treats an empty value as unset. WithEXTENDDB_DEV_DISABLE_EXAMPLE_CREDENTIAL=in the environment, the server started and the example key listed tables. A CI line-e EXTENDDB_DEV_DISABLE_EXAMPLE_CREDENTIAL=$VARwithVARunset gives that result. Say that an empty value counts as unset, or refuse it. -
Optional,
docs/dev-image.md:39: the edit leftprints iton a line of its own. Rewrap the paragraph.
Not checked
- The non-loopback banner on a running server. Its two unit tests fail without the change.
- The
AWS_ACCESS_KEY_IDwarning incrates/server/src/serve.rswith the switch set. No test covers it. - The Docker image. I ran a local build.
- The PR body's clippy run for
-p extenddb --no-default-features --features sqlite,dev-mode.
What
Hardens the
extenddb-devimage against accidental network exposure. Nothing changes unless the new switch is set.EXTENDDB_DEV_DISABLE_EXAMPLE_CREDENTIAL=1(ortrue) drops AWS's public example key pair, so only theEXTENDDB_DEV_ACCESS_KEY_ID/EXTENDDB_DEV_SECRET_ACCESS_KEYpair verifies.1,true,0orfalse, so a typo such asyescannot leave the public key active.-p 127.0.0.1:PORT:PORT) and the new switch. The non-loopbacktracing::warn!and the ignored-AWS_ACCESS_KEY_IDadvisory mention the switch too.docker/dockerhub-overview-dev.md(it was the last section) and ofdocs/dev-image.md, which gains a "A shared host or CI runner" section for the switch.docker runexample published-p 18443:18443; it now publishes-p 127.0.0.1:18443:18443, matching the text beneath it (https://127.0.0.1:18443) and its certificate SANs.Why
The dev image serves plain HTTP with open authorization and seeds a public credential. That is right for a laptop, but the obvious
docker run -p 18080:18080publishes the port on every interface the host has, and then anyone who can reach it can read, change and delete every table. Keeping the in-container0.0.0.0bind is required (a container bound to its own loopback is unreachable even through-p 127.0.0.1:...), and the server cannot see the host's port mapping, so the fixes are: let shared hosts and CI runners remove the public credential, warn clearly at startup, and put the warning where people read first.The example pair stays on by default, so NoSQL Workbench, the
@extenddb/devlaunchers, CI, and docs that hardcode it are unaffected.Testing done
Unit tests:
cargo test -p extenddb-server -p extenddb-app(344 + 51 pass). New tests cover the switch (on/off values, typo rejection, missing replacement, half-set pair, key-id collision, the ignored-AWS_ACCESS_KEY_IDadvisory,describeoutput) and the banner (loopback vs non-loopback, with and without the example pair, port in the publish command, no custom secret printed).End to end, a
--features sqlite,dev-modebuild on loopback against one file-backed database:ListTablesOKUnrecognizedClientException; team key: OK; log:dev credential AKIAIOSFODNN7EXAMPLE: removed from the catalogEXTENDDB_DEV_*pairEXTENDDB_DEV_DISABLE_EXAMPLE_CREDENTIAL=yesmust be 1, true, 0 or false (got 'yes')The non-loopback banner was not exercised live, to avoid exposing a dev server with the public key; the unit tests cover its text.
cargo +1.97.0 fmt --all -- --check;cargo +1.97.0 clippy --all-targets -- -D warningsforextenddb-server/extenddb-appand for-p extenddb --no-default-features --features sqlite,dev-mode.Not covered: no integration-workflow job starts a dev server with the switch set; happy to add one if wanted.
Checklist
cargo test --workspace): ran the two changed crates, not the whole workspacecargo fmt --check)-D warningsas CI runs it)Storagetrait, 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. Adds an opt-in dev-mode environment variable; the default credential set and the auth model are unchanged.
Breaking changes
None. The two dev-mode startup error texts for an
EXTENDDB_DEV_ACCESS_KEY_IDthat is not AKIA-shaped, or that reuses the example key id, now say "seeded by default" / "reserved" instead of "always seeded".Follow-up (not in this PR)
Unsetting
EXTENDDB_DEV_ACCESS_KEY_IDdoes not remove a custom key an earlier boot stored in a file-backed catalog; it keeps verifying until the volume is recreated.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.