Skip to content

feat(cli): connection flags + DX (quiet logging, random port, best-effort TLS) - #397

Open
coderdan wants to merge 5 commits into
mainfrom
dan/cli-connection-flags
Open

feat(cli): connection flags + DX (quiet logging, random port, best-effort TLS)#397
coderdan wants to merge 5 commits into
mainfrom
dan/cli-connection-flags

Conversation

@coderdan

@coderdan coderdan commented May 30, 2026

Copy link
Copy Markdown
Contributor

Implements CIP-3823, making the proxy usable as a plain CLI (for example, npx stash proxy) without hand-setting connection environment variables.

Connection flags

  • Add --database-url <URL> to parse a PostgreSQL connection string via tokio-postgres; individual --db-* flags take precedence.
  • Add --db-port (-P) and --db-password (-W).
  • Fix --db-user to set CS_DATABASE__USERNAME instead of the unused CS_DATABASE__USER.
  • Reject non-UTF-8 URL passwords instead of silently retaining an absent or stale password.

CLI behavior

  • Log errors only by default while always printing the bound listen address.
  • Preserve explicit --log-level and CS_LOG__LEVEL values, including an explicit info; an explicit level or config file takes precedence over --debug.
  • Remove informational configuration and override chatter from quiet startup.
  • If the implicit default listen port is occupied, bind an OS-assigned port and report it. Explicitly configured ports remain strict.
  • Treat invalid inbound TLS as best effort: fall back to plaintext with a notice. --tls makes TLS mandatory, while --no-tls forces plaintext.
  • Log PostgreSQL errors forwarded to clients concisely at debug level instead of dumping the full response at error level.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all-features -- -D warnings
  • cargo check -p cipherstash-proxy-integration
  • cargo test -p cipherstash-proxy database_url_sets_connection_env
  • cargo test -p cipherstash-proxy bind_falls_back_to_random_port_when_default_in_use
  • cargo test -p cipherstash-proxy log_level_preserves_whether_it_was_explicitly_set
  • Full cipherstash-proxy unit suite: 136/137 passed in parallel; the remaining environment-mutating config test passed in isolation.

The branch is rebased onto current origin/main, and all five commits include DCO sign-offs.

Copilot AI review requested due to automatic review settings May 30, 2026 00:48
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d86b056-bb43-4756-a278-754fbd8d2604

📥 Commits

Reviewing files that changed from the base of the PR and between e13f5b8 and c928bdf.

📒 Files selected for processing (2)
  • packages/cipherstash-proxy-integration/src/migrate/mod.rs
  • packages/cipherstash-proxy/src/config/tandem.rs

📝 Walkthrough

Walkthrough

Adds CLI DB and TLS flags; parses PostgreSQL URLs into CS_DATABASE__* environment variables with CLI overrides; honors --no-tls; enables bind fallback to an OS-assigned port when appropriate; threads a force-TLS flag through init and reloads; lowers some backend error logging; updates related tests.

Changes

CLI, config, connect, and init

Layer / File(s) Summary
CLI argument extensions
packages/cipherstash-proxy/src/cli/mod.rs
Added db_password: Option<String>, no_tls: bool, tls: bool, and debug: bool; refined database_url, db_host, and db_port clap metadata and help text.
Database URL parsing and env overrides
packages/cipherstash-proxy/src/config/tandem.rs
Added apply_database_url to parse Postgres URLs into CS_DATABASE__HOST, CS_DATABASE__PORT, CS_DATABASE__USERNAME, CS_DATABASE__PASSWORD, and CS_DATABASE__NAME; TandemConfig::build applies the URL then CLI overrides (including db_port and db_password); load honors --no-tls; includes unit tests validating env vars and non-UTF8 password rejection.
Listener bind retry and reporting
packages/cipherstash-proxy/src/connect/mod.rs
bind_with_retry gains allow_random_fallback to bind to host:0 when AddrInUse on implicit/default ports; adds report_listening helper and a test verifying fallback behavior.
Init, TLS validation, logging, and reload wiring
packages/cipherstash-proxy/src/main.rs
init signature gains force_tls: bool; logging defaults to error-only unless debug/loglevel/config present; TLS validated up-front and treated as fatal only when forced, else downgraded to plaintext; bind fallback enabled only when port not explicitly set; reloads pass args.tls.
Encrypt config logging change
packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs
Missing EncryptConfig table case now logs warn! instead of error! while continuing with an empty configuration.
Postgres backend logging tweaks
packages/cipherstash-proxy/src/postgresql/backend.rs
Imports ErrorResponseCode, trims tracing imports, and reduces forwarded DB error logging to debug! with extracted code/message.
ErrorResponse accessor and tests
packages/cipherstash-proxy/src/postgresql/messages/error_response.rs
Adds pub fn field(&self, code: ErrorResponseCode) -> Option<&str> and a unit test asserting returned values for Code/Message and None for missing Detail.
Integration test Args update
packages/cipherstash-proxy-integration/src/migrate/mod.rs
Updates test to initialize new Args fields (database_url, db_port, db_password, no_tls, tls, debug) to None/false to match CLI changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • tobyhede
  • freshtonic

Poem

🐰 I hop through flags and envs with care,
I parse the URL and set each pair,
If TLS says no, I gently fall back,
Ports that collide find a new track,
I munch on logs while tests stay fair.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding CLI connection flags (--database-url, --db-password, --db-port), improving quiet logging by default, implementing random port fallback, and enabling best-effort TLS configuration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dan/cli-connection-flags

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI 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.

Pull request overview

Adds CLI-only database connection configuration and inbound TLS override support for cipherstash-proxy, making the binary easier to run without preconfigured environment variables.

Changes:

  • Adds --database-url, --db-port, --db-password, and --no-tls CLI flags.
  • Applies database URL values into CS_DATABASE__* env vars before layered config loading.
  • Fixes --db-user to set CS_DATABASE__USERNAME instead of the unused CS_DATABASE__USER.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/cipherstash-proxy/src/cli/mod.rs Adds the new CLI arguments and help text.
packages/cipherstash-proxy/src/config/tandem.rs Applies CLI database overrides, disables inbound TLS when requested, and adds regression coverage for URL parsing/user env var behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cipherstash-proxy/src/config/tandem.rs Outdated
Comment on lines +423 to +427
if let Some(password) = pg.get_password() {
if let Ok(password) = std::str::from_utf8(password) {
env::set_var("CS_DATABASE__PASSWORD", password);
}
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/cipherstash-proxy/src/config/tandem.rs (1)

395-398: ⚡ Quick win

Doc comment contains stale text from another function.

Lines 395-396 ("Extracts a field name (if present)..." and "This is called in build...") describe extract_missing_field_and_key below, not apply_database_url. This appears to be left over from when the new function was inserted.

📝 Proposed fix
-/// Extracts a field name (if present) from a config::ConfigError string
-/// This is called in `build` if a ConfigError message contains the string `missing field`
 /// Parse a PostgreSQL connection string (e.g.
 /// "postgres://user:pass@host:5432/dbname") and set the corresponding
 /// CS_DATABASE__* env vars for each component present. Individual --db-* flags
 /// are applied after this and take precedence.
 fn apply_database_url(url: &str) -> Result<(), Error> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cipherstash-proxy/src/config/tandem.rs` around lines 395 - 398, The
doc comment above apply_database_url is stale and describes
extract_missing_field_and_key instead; update the comment for apply_database_url
to accurately describe that it parses a PostgreSQL connection string (e.g.
"postgres://user:pass@host:5432/dbname"), sets the CS_DATABASE__* environment
variables for components present, and notes that individual --db-* flags
(applied later) take precedence; remove or relocate the two sentences about
extracting field names and the build call so they belong with
extract_missing_field_and_key (or delete them if redundant).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cipherstash-proxy/src/config/tandem.rs`:
- Around line 423-427: The code currently drops non-UTF8 passwords silently;
update the pg.get_password() branch to log a warning when
std::str::from_utf8(password) returns Err instead of silently skipping—use your
project's logger (e.g., tracing::warn! or log::warn!) to emit a clear message
that the password contains non-UTF8 bytes and CS_DATABASE__PASSWORD was not set,
and include a safe representation of the raw bytes (hex or base64) for
debugging; keep the existing env::set_var("CS_DATABASE__PASSWORD", password)
path unchanged when from_utf8 succeeds.

---

Nitpick comments:
In `@packages/cipherstash-proxy/src/config/tandem.rs`:
- Around line 395-398: The doc comment above apply_database_url is stale and
describes extract_missing_field_and_key instead; update the comment for
apply_database_url to accurately describe that it parses a PostgreSQL connection
string (e.g. "postgres://user:pass@host:5432/dbname"), sets the CS_DATABASE__*
environment variables for components present, and notes that individual --db-*
flags (applied later) take precedence; remove or relocate the two sentences
about extracting field names and the build call so they belong with
extract_missing_field_and_key (or delete them if redundant).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 240c19cb-ffdf-4e10-b6ca-9aea6530acf7

📥 Commits

Reviewing files that changed from the base of the PR and between 100829b and 442c2cd.

📒 Files selected for processing (2)
  • packages/cipherstash-proxy/src/cli/mod.rs
  • packages/cipherstash-proxy/src/config/tandem.rs

Comment thread packages/cipherstash-proxy/src/config/tandem.rs
@coderdan coderdan changed the title fix(cli): add --database-url/--db-port/--db-password/--no-tls, fix --db-user feat(cli): connection flags + DX (quiet logging, random port, best-effort TLS) May 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cipherstash-proxy/src/connect/mod.rs`:
- Around line 67-82: The primary and fallback bind paths both format host:port
with naive string interpolation which breaks on IPv6 literals; update the code
so both TcpListener::bind calls use a proper socket-address builder or
consistently bracket IPv6 hosts (e.g. construct a SocketAddr via
ServerConfig::to_socket_address() or add logic that wraps an IPv6 literal in [ ]
before combining with port) so that the same bracket-handling used for the
primary path is applied to the fallback path (references: TcpListener::bind
calls and ServerConfig::to_socket_address()).

In `@packages/cipherstash-proxy/src/main.rs`:
- Around line 233-240: The reload logic rejects SIGHUP reloads because init may
have downgraded the in-memory TandemConfig (clearing config.tls and setting
config.server.require_tls = false) while a freshly loaded new_config still shows
TLS set, so has_network_config_changed sees a diff and forces a restart; fix by
normalizing both configs to the same effective/network representation before
comparing — e.g., extract the TLS-downgrade/normalization performed in init into
a helper (or call the same downgrade logic) and apply it to new_config inside
reload_application_config (before has_network_config_changed), or change
has_network_config_changed to compare “effective” TLS state instead of raw
config.tls/config.server.require_tls; target symbols: init, TandemConfig::load,
reload_application_config, has_network_config_changed, Proxy { config } and the
fields TandemConfig.tls and TandemConfig.server.require_tls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2214fe66-11bb-48f0-ae08-9fb700a83c51

📥 Commits

Reviewing files that changed from the base of the PR and between 442c2cd and e1c5fd6.

📒 Files selected for processing (4)
  • packages/cipherstash-proxy/src/cli/mod.rs
  • packages/cipherstash-proxy/src/config/tandem.rs
  • packages/cipherstash-proxy/src/connect/mod.rs
  • packages/cipherstash-proxy/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cipherstash-proxy/src/config/tandem.rs

Comment on lines +67 to +82
Err(err) if err.kind() == std::io::ErrorKind::AddrInUse && allow_random_fallback => {
match TcpListener::bind(format!("{}:0", server.host)).await {
Ok(listener) => {
println!(
"Port {} is already in use; listening on an OS-assigned port instead.",
server.port
);
report_listening(&listener);
return listener;
}
Err(err) => {
error!(msg = "Error binding connection", error = err.to_string());
std::process::exit(exitcode::CONFIG);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how ServerConfig::to_socket_address formats host:port (brackets for IPv6?)
ast-grep --pattern $'fn to_socket_address($$$) {
  $$$
}'
echo "----- raw search -----"
rg -nP -C3 '\bto_socket_address\b'

Repository: cipherstash/proxy

Length of output: 2830


Fix IPv6-literal concern: primary bind has the same formatting issue as the fallback

The fallback uses format!("{}:0", server.host), but ServerConfig::to_socket_address() (used by the retry/primary path) is also format!("{}:{}", self.host, self.port) with no IPv6-literal bracket handling. So the “primary presumably handles this” distinction doesn’t hold; if IPv6 literals are supported, both paths need consistent bracketed formatting (or a socket-address builder that handles IPv6 correctly).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cipherstash-proxy/src/connect/mod.rs` around lines 67 - 82, The
primary and fallback bind paths both format host:port with naive string
interpolation which breaks on IPv6 literals; update the code so both
TcpListener::bind calls use a proper socket-address builder or consistently
bracket IPv6 hosts (e.g. construct a SocketAddr via
ServerConfig::to_socket_address() or add logic that wraps an IPv6 literal in [ ]
before combining with port) so that the same bracket-handling used for the
primary path is applied to the fallback path (references: TcpListener::bind
calls and ServerConfig::to_socket_address()).

Comment on lines +233 to +240
(Some(_), Some(err), false) => {
eprintln!(
"TLS configuration is invalid ({err}); falling back to a plaintext listener. \
Pass --tls to make this fatal, or --no-tls to silence this warning."
);
config.tls = None;
config.server.require_tls = false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm has_network_config_changed compares tls/require_tls and is used on reload
rg -nP -C3 '\bhas_network_config_changed\b'
# Confirm reload uses the live (possibly downgraded) proxy.config
rg -nP -C3 'reload_application_config\s*\('

Repository: cipherstash/proxy

Length of output: 2326


🏁 Script executed:

#!/bin/bash
# Inspect TLS validation/downgrade logic around the reported lines
sed -n '200,260p' packages/cipherstash-proxy/src/main.rs

# Inspect reload + change-detection logic around has_network_config_changed usage
sed -n '280,360p' packages/cipherstash-proxy/src/main.rs

# Find where require_tls is set/cleared during init
rg -n --context 2 'require_tls' packages/cipherstash-proxy/src/main.rs

Repository: cipherstash/proxy

Length of output: 4552


🏁 Script executed:

#!/bin/bash
# Confirm SIGHUP handler passes &proxy.config into reload_application_config
sed -n '60,130p' packages/cipherstash-proxy/src/main.rs

# Locate Proxy struct definition and how it stores config (look for `config:` field)
rg -n "struct Proxy\b" packages/cipherstash-proxy/src -S
rg -n "\bconfig:\s*TandemConfig\b|\bconfig:\s*.*TandemConfig\b|proxy\.config\b" packages/cipherstash-proxy/src -S

# Locate Proxy::init to see what gets stored on success
rg -n "impl Proxy\b|fn init\(config:|Proxy::init" packages/cipherstash-proxy/src -S

Repository: cipherstash/proxy

Length of output: 7142


🏁 Script executed:

#!/bin/bash
# Check whether TandemConfig::load validates TLS (would affect whether new_config.tls stays Some)
rg -n "fn load\\b|impl .*TandemConfig" packages/cipherstash-proxy/src/config/tandem.rs
sed -n '1,260p' packages/cipherstash-proxy/src/config/tandem.rs
rg -n "check_cert|check_private_key|configure_server|tls::configure" packages/cipherstash-proxy/src/config/tandem.rs

Repository: cipherstash/proxy

Length of output: 9703


🏁 Script executed:

#!/bin/bash
# Locate ServerConfig definition + defaults for require_tls
rg -n "struct ServerConfig|require_tls" packages/cipherstash-proxy/src/config -S

# Show the portion that defines ServerConfig fields/defaults and how require_tls is derived
fd -e rs packages/cipherstash-proxy/src/config -a | head -n 20 >/dev/null 2>&1 || true
# Print candidate server config files around require_tls
rg -n "require_tls" packages/cipherstash-proxy/src/config -S --files-with-matches

Repository: cipherstash/proxy

Length of output: 966


🏁 Script executed:

#!/bin/bash
set -e

# Show ServerConfig struct + default for require_tls
sed -n '1,120p' packages/cipherstash-proxy/src/config/server.rs

# Find all assignments to require_tls during config build/load
rg -n "require_tls\s*=" packages/cipherstash-proxy/src/config -S
rg -n "require_tls" packages/cipherstash-proxy/src -S

# Inspect any TLS parsing/config wiring in tandem.rs (where tls Some(...) might imply require_tls)
rg -n "tls" packages/cipherstash-proxy/src/config/tandem.rs -S --context 2

Repository: cipherstash/proxy

Length of output: 5403


Non-fatal TLS downgrade can prevent SIGHUP reloads indefinitely (until TLS is removed/disabled or the process restarts).

        (Some(_), Some(err), false) => {
            eprintln!(
                "TLS configuration is invalid ({err}); falling back to a plaintext listener. \
                 Pass --tls to make this fatal, or --no-tls to silence this warning."
            );
            config.tls = None;
            config.server.require_tls = false;
        }

When inbound TLS is configured but invalid and --tls is not set, init downgrades in place by clearing config.tls and setting config.server.require_tls = false. That downgraded TandemConfig is what the running proxy stores (Proxy { pub config: Arc<TandemConfig> }). On SIGHUP, reload_application_config(&proxy.config, &args) loads a fresh new_config via TandemConfig::load(args) (which only disables TLS for --no-tls, and does not validate/downgrade invalid certs) and then rejects the reload before calling init if has_network_config_changed(config, &new_config) reports a change. Since has_network_config_changed compares both current.tls and current.server.require_tls, the downgraded current (tls = None) differs from new_config (tls = Some), so reload returns NetworkConfigurationChangeRequiresRestart and the SIGHUP handler keeps the old proxy (unwrap_or(proxy)).

Adjust the reload change detection to compare effective (post-downgrade/normalized) network config, or apply the same downgrade logic to new_config before calling has_network_config_changed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cipherstash-proxy/src/main.rs` around lines 233 - 240, The reload
logic rejects SIGHUP reloads because init may have downgraded the in-memory
TandemConfig (clearing config.tls and setting config.server.require_tls = false)
while a freshly loaded new_config still shows TLS set, so
has_network_config_changed sees a diff and forces a restart; fix by normalizing
both configs to the same effective/network representation before comparing —
e.g., extract the TLS-downgrade/normalization performed in init into a helper
(or call the same downgrade logic) and apply it to new_config inside
reload_application_config (before has_network_config_changed), or change
has_network_config_changed to compare “effective” TLS state instead of raw
config.tls/config.server.require_tls; target symbols: init, TandemConfig::load,
reload_application_config, has_network_config_changed, Proxy { config } and the
fields TandemConfig.tls and TandemConfig.server.require_tls.

@coderdan

coderdan commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Review feedback addressed (pushed in c928bdf)

Thanks for the reviews. Status:

Fixed in this PR

  • CI failure (the red Test jobs): root cause was a compile error, not test logic — the new Args fields weren't added to the struct literal in cipherstash-proxy-integration/src/migrate/mod.rs, so the integration crate failed to build (E0063). Added the six missing fields.
  • Non-UTF8 password (Copilot + CodeRabbit, tandem.rs): now returns a clear InvalidParameter error instead of silently dropping the password (which would connect with no/stale credentials). Went with Copilot's error-out approach over warn-and-continue, since the password is required downstream. Added a regression test (database_url_rejects_non_utf8_password) — the branch is reachable because tokio-postgres stores the URL password as raw bytes without UTF-8 validation.
  • Stale doc comment (Copilot + CodeRabbit, tandem.rs): removed the leftover extract_missing_field_and_key text above apply_database_url.

Deferred to follow-up issues (real, but pre-existing / behavioural and out of scope for this CLI-feature PR)

Local validation: cargo fmt --all --check, cargo clippy --all --all-features --tests -- -D warnings, and the affected unit tests all pass.

coderdan and others added 5 commits August 18, 2026 16:45
…db-user

Makes the proxy usable as a plain CLI (e.g. `npx stash proxy`) without
hand-setting env vars:

- --database-url: parse a full postgres:// connection string (host, port,
  user, password, dbname) via tokio-postgres' parser; individual flags override.
- --db-port / --db-password: the missing scalar connection flags.
- --no-tls: listen for client connections in plaintext, overriding any CS_TLS__*
  env/config (inbound only; does not affect the proxy->database connection).
  Avoids a hard startup error when a stray CS_TLS__* points at a missing cert.

Fixes a bug: --db-user set CS_DATABASE__USER, but the config field is
`username` (CS_DATABASE__USERNAME), so --db-user was silently a no-op.

Adds a regression test for the URL parsing + the USERNAME fix. Validated
locally: `npx stash proxy --database-url postgres://.../db --no-tls` connects
and listens.

Signed-off-by: James Sadler <james@cipherstash.com>
…t TLS

DX improvements for running proxy as a CLI (e.g. npx stash proxy):

- Quiet by default: log errors only unless --debug, an explicit --log-level /
  CS_LOG__LEVEL, or a config file is present (production/observability via env
  or config is unchanged). The listen address always prints.
- Random-port fallback: if the listen port is the default (no CS_SERVER__PORT,
  no config file) and is already in use, bind an OS-assigned port and report it
  instead of failing. An explicitly configured port is still respected.
- Best-effort inbound TLS: a TLS misconfiguration now falls back to a plaintext
  listener with a notice instead of exiting. --tls forces TLS (fatal if it
  can't be set up); --no-tls forces plaintext.

Adds a regression test for the random-port fallback.

Relates to BUG-300-adjacent CLI usability work.

Signed-off-by: James Sadler <james@cipherstash.com>
A PostgreSQL error forwarded to the client is normal proxy operation -- the
client already surfaces it -- but the backend logged every one at ERROR level as
the full Debug-formatted ErrorResponse struct. That is unreadable and, with
quiet-by-default logging, was the only thing showing on a failed query (and it
bled into --psql sessions).

Log it at debug instead, concisely (SQLSTATE code + message) via a new
ErrorResponse::field() accessor. Nothing on a SQL error by default; a clean
one-line entry under --debug. Adds a unit test for the accessor.

Signed-off-by: James Sadler <james@cipherstash.com>
- Add the six new Args fields (database_url, db_port, db_password, no_tls,
  tls, debug) to the struct literal in the migrate integration test. The
  proxy crate compiled, but the integration crate's literal was stale, so
  the full-workspace test build failed E0063 — this is the CI failure.
- Reject a non-UTF8 password from --database-url with a clear
  InvalidParameter error instead of silently dropping it (which would
  connect with no/stale credentials). Add a regression test; the branch is
  reachable because tokio-postgres stores the URL password as raw bytes
  without UTF-8 validation.
- Remove a stale doc comment left over from another function above
  apply_database_url.

Signed-off-by: James Sadler <james@cipherstash.com>
Signed-off-by: James Sadler <james@cipherstash.com>
@freshtonic
freshtonic force-pushed the dan/cli-connection-flags branch from c928bdf to fde3ad2 Compare August 18, 2026 06:46
@tobyhede

Copy link
Copy Markdown
Contributor

Reviewed at fde3ad22. Each item below is anchored to code I read on the branch head. Note that fde3ad22 landed after CodeRabbit paused, so items 1 and 5 are in unreviewed code.

Fix before merge

1. main.rs:30 — the quiet default discards the whole log config, not just the level.

LogConfig::with_level() (config/log.rs:99-111) constructs from Default and copies nothing from the config that was just loaded. So when neither CS_LOG__LEVEL nor a config file is present, format, output, ansi_enabled, slow_statements and every per-target level are dropped. CS_LOG__MAPPER_LEVEL=debug — the recipe in CLAUDE.md and the logging docs — then produces no mapper output at all, and CS_LOG__OUTPUT=stderr still writes to stdout. main did log::init(config.log.clone()), so this is a regression for env-only deployments (the container ships no toml).

Suggest cloning config.log and overriding only level / targets.

2. main.rs:239 — an invalid certificate clears an explicitly configured require_tls.

The fallback arm sets config.tls = None and config.server.require_tls = false. require_tls is enforced at exactly one site (postgresql/handler.rs:215, via context/mod.rs:880), so clearing it removes the only check that rejects a client which skipped SSLRequest. An operator who set CS_SERVER__REQUIRE_TLS=true and then loses the cert (rotation, bad mount, unreadable file — all reach this arm via config/tls.rs) gets a proxy that starts, exits 0, and accepts plaintext sessions. On main the same input exited exitcode::CONFIG.

Two further notes: this arm emits only eprintln!, no tracing event, so nothing reaches structured logs or alerting; and init() also runs on the SIGHUP path (main.rs:330), so this can downgrade a proxy that is already serving traffic.

The plaintext fallback itself is a reasonable DX default. Discarding an explicit require_tls = true is the part that should stay fatal.

3. cli/mod.rs:65-69--tls does not require TLS.

The help text reads "Require inbound (client-facing) TLS", but args.tls only reaches init as force_tls, where the valid-cert arm (main.rs:224) matches it as _. A repo-wide grep for require_tls shows three writes, all false (config/server.rs:42, config/tandem.rs:108, main.rs:239) — nothing ever sets it true. So cipherstash-proxy --tls with a valid certificate still serves a client that omits SSLRequest. --no-tls correctly writes the config at tandem.rs:108; --tls needs the symmetric write.

4. connect/mod.rs:67 — the random-port fallback pre-empts the retry loop.

The fallback guard arm is matched before the retry arm at :83 and returns on the first AddrInUse, so the backoff loop is unreachable for that error kind whenever fallback is on. allow_random_fallback is !port_configured (main.rs:74), which is true for any run with no CS_SERVER__PORT and no config file — including the reference docker-compose.yml service. If anything else already holds 6432 (double-start, stale process, unrelated service), the proxy binds a random port and reports success where main exited non-zero after ~1.5s of retries. The fallback port is printed, but exit code is 0 and nothing is emitted at error!/warn!.

Suggest falling back only once retry_count > MAX_RETRY_COUNT.

Question

5. main.rs:33-39 — should file existence outrank --debug?

log_explicitly_set includes config_file_path.exists() and is tested before args.debug, so --debug is a no-op whenever a cipherstash-proxy.toml is present, even when that file sets no [log] level at all. cli/mod.rs:71-72 documents this, so I take it as intentional — but the comment at main.rs:26 lists --debug first, and an explicit flag losing to the mere presence of a file is surprising. Worth confirming which behaviour you want, and aligning the two comments either way.

Minor

  • postgresql/backend.rs:330-332 — the doc comment still says "All errors are logged at ERROR level for debugging and recorded in monitoring systems" while the body now logs at debug!. The demotion looks right (proxy-origin errors are already logged at error! in frontend.rs), but the comment is now false.
  • config/tandem.rs:391 — a Unix-socket URL yields no Host::Tcp, so CS_DATABASE__HOST is left alone while CS_DATABASE__PORT is still set from the same URL; multi-host URLs silently take the first. Leaving unspecified components to env/config is documented and matches libpq, so this is only about components the URL does contain.
  • No CHANGELOG entries for the quiet-by-default level, the error-log demotion, or the random-port fallback — all three are operator-visible.

Already tracked

main.rs:322 — the reload guard runs before init at :330, so after a TLS fallback the running tls: None never matches a freshly loaded tls: Some(..) and every SIGHUP fails. This is CodeRabbit's comment, deferred to #403. One thing to add there: it is not just the cert-fix reload that fails — after a fallback, all reloads fail (log level, encrypt config) until the process restarts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants