Skip to content

fix(dev-mode): let operators drop the public example credential - #876

Open
hitesh96db wants to merge 1 commit into
mainfrom
fix/dev-image-exposure-hardening
Open

hitesh96db wants to merge 1 commit into
mainfrom
fix/dev-image-exposure-hardening

Conversation

@hitesh96db

Copy link
Copy Markdown
Contributor

What

Hardens the extenddb-dev image against accidental network exposure. Nothing changes unless the new switch is set.

  • EXTENDDB_DEV_DISABLE_EXAMPLE_CREDENTIAL=1 (or true) drops AWS's public example key pair, so only the EXTENDDB_DEV_ACCESS_KEY_ID / EXTENDDB_DEV_SECRET_ACCESS_KEY pair verifies.
    • Requires that pair: without it the server refuses to start, so it never runs with no credential.
    • Rejects any value other than 1, true, 0 or false, so a typo such as yes cannot leave the public key active.
    • Removes an example key an earlier boot stored in a file-backed catalog. Seeding only adds keys, so without this the public key would keep verifying after the switch was set.
  • Accurate, louder dev banner. It said "plain HTTP on loopback" whatever the bind was. It now names the bind address and, when that is not loopback (the image's normal state), prints a warning block with the loopback publish command (-p 127.0.0.1:PORT:PORT) and the new switch. The non-loopback tracing::warn! and the ignored-AWS_ACCESS_KEY_ID advisory mention the switch too.
  • Docs. The "local development and CI only, not for shared networks" warning moves to the top of docker/dockerhub-overview-dev.md (it was the last section) and of docs/dev-image.md, which gains a "A shared host or CI runner" section for the switch.
  • MongoDB overview. Its docker run example 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:18080 publishes 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-container 0.0.0.0 bind 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/dev launchers, 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_ID advisory, describe output) 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-mode build on loopback against one file-backed database:

    Step Result
    Default start example key: ListTables OK
    Restart with the switch + a team key example key: UnrecognizedClientException; team key: OK; log: dev credential AKIAIOSFODNN7EXAMPLE: removed from the catalog
    Switch set, no EXTENDDB_DEV_* pair refuses to start, naming both variables
    EXTENDDB_DEV_DISABLE_EXAMPLE_CREDENTIAL=yes refuses to start: must 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 warnings for extenddb-server/extenddb-app and 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

  • I have read CONTRIBUTING.md
  • All tests pass (cargo test --workspace): ran the two changed crates, not the whole workspace
  • Code is formatted (cargo fmt --check)
  • Clippy is clean (-D warnings as CI runs it)
  • 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. 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_ID that 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_ID does 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.

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.
@hitesh96db hitesh96db added security v1 Needed for the 1.0 release release-blocker Must ship for its milestone's release labels Nov 6, 2026

@LeeroyHannigan LeeroyHannigan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 UnrecognizedClientException and 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, TRUE or on: 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 healthcheck with 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

  1. Major, crates/server/src/dev_credentials.rs:289: no test covers the catalog removal. I deleted lines 289 to 307, and only the five backup_store tests 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 that lookup_credential("AKIAIOSFODNN7EXAMPLE") returns None. ci/smoke-test-dev-container.sh can also restart its file-mode container once with the switch.

  2. Minor, crates/server/src/dev_credentials.rs:294: the delete targets user dev in the default account and ignores NotFound, so an example key on another user keeps working. With the switch on, I used extenddb manage to import the example pair for a new user alice, as docs/getting-started.md shows. 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 with credential_store.lookup_credential(EXAMPLE_ACCESS_KEY_ID). Delete the key from that account_id and principal_name.

  3. Minor, docs/dev-image.md:78: "the EXTENDDB_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 with extenddb manage also keep working (finding 2). State that the switch stops seeding the example pair and deletes it from the dev user. Add that keys stored by earlier starts keep working until you recreate the volume.

  4. Optional, crates/app/src/cmd_serve.rs:129: this tracing::warn! runs before serve_inner installs a tracing subscriber, so it never prints. With EXTENDDB_DEV_ALLOW_ANY_BIND=1 and server.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.

  5. 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. With EXTENDDB_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=$VAR with VAR unset gives that result. Say that an empty value counts as unset, or refuse it.

  6. Optional, docs/dev-image.md:39: the edit left prints it on 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_ID warning in crates/server/src/serve.rs with 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.

This branch has not been deployed

No deployments
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.

2 participants