镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Merged
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- **Connection flags**: `--database-url` sets the database host, port, user, password and name from one PostgreSQL connection string, and `--db-port` (`-P`) and `--db-password` (`-W`) join the existing `--db-*` flags, which override matching parts of the URL. A URL sets only the parts it contains. Connection strings with any other option, such as `sslmode`, or with a Unix socket host or multiple hosts, are rejected, because Proxy cannot apply them. A password on the command line is exposed in shell history and the process list, so prefer `CS_DATABASE__PASSWORD`.

- **Inbound TLS flags**: `--tls` requires TLS from clients, as `CS_SERVER__REQUIRE_TLS=true` does, and fails startup if TLS is not configured. `--no-tls` forces a plaintext listener and overrides `CS_SERVER__REQUIRE_TLS`.

- **Application-generated EQL values in statements**: SQL literals and bound parameters may now carry EQL v3 storage payloads or query-only SEM operands produced by an application. Proxy authenticates every stored ciphertext, requires its authenticated descriptor to name the inferred destination column, and independently re-derives every SEM term before forwarding it without double encryption. Query-only operands contain no ciphertext to authenticate, so Proxy instead validates their version, identifier, term shape, column capabilities, and syntactic query role; they are rejected in storage positions. This includes bare SteVec selector hashes matching `^[0-9a-f]{32}$`: in JSON selector query positions a match is treated as already hashed, while a non-match remains plaintext and is encrypted normally. A matching plaintext selector is inherently ambiguous and is intentionally treated as already hashed. Invalid payloads fail closed with one generic, transaction-aborting error so validation details cannot be used as an oracle.

Compatibility note: on every encrypted column type, Proxy reserves three JSON object shapes for application-generated EQL: storage payloads with top-level `v` and `i` plus at least one of `c`, `h`, or `sv`; scalar query payloads with `v` and `i` plus at least one of `hm`, `bf`, `ob`, or `op`; and SteVec query payloads whose only top-level key is `sv`. An object matching one of these shapes is validated as EQL rather than encrypted as plaintext, and there is no opt-out. Query-only shapes are rejected in storage positions. Before upgrading, audit plaintext application writes for these key combinations; [Invalid encrypted value](docs/errors.md#encrypt-invalid-inbound-eql-payload) includes a `jsonb` scan predicate.
Expand All @@ -18,6 +22,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **Quiet by default in a terminal**: when run in a terminal with no `--log-level`, `CS_LOG__LEVEL` or configuration file, Proxy now logs errors only and prints its listen address. Pass `--debug` for debug logging. Other log settings from the environment, such as `CS_LOG__OUTPUT` and per-target levels like `CS_LOG__MAPPER_LEVEL`, still apply. Without a terminal, such as in a container run without `-t`, logging is unchanged.

- **Default port fallback**: if the default listen port is still in use after Proxy's bind retries, Proxy listens on an OS-assigned port and prints it. A port set with `CS_SERVER__PORT` or a configuration file is never replaced.

- **Database errors logged at debug level**: errors that PostgreSQL returns to a client are now logged concisely at `debug` level (`CS_LOG__PROTOCOL_LEVEL=debug` shows them), instead of in full at `error` level. Errors that Proxy itself generates are still logged.

- **Minimum supported Rust version**: building Proxy from source now requires Rust 1.89.0 or later, declared as `rust-version` in `Cargo.toml` so Cargo reports an older compiler clearly instead of failing deep in a dependency. CI builds the production binary on Rust 1.89.0 to keep that minimum from rising by accident. Development, tests, and release binaries use the Rust version pinned in `rust-toolchain.toml` (currently 1.99.0). See [Supported Rust versions](DEVELOPMENT.md#supported-rust-versions).

- **PostgreSQL execution lifecycle ownership**: Proxy now tracks prepared statements, portals, Describe operations, execution outcomes, and execution metrics as one connection-local protocol lifecycle. Suspended executions retain their original metrics scope until completion, repeated portal executions receive distinct scopes, and terminal errors replace upstream responses atomically while schema outcomes are reported exactly once.
Expand All @@ -37,6 +47,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **`--db-user`**: the flag now sets the database username. It previously set an unused variable, so it had no effect.

- **Unknown column error message**: the type-checking error for a column a table does not have now names the column and the table the right way round, for example `Column: email not found for table: users`. It previously swapped them.

- **Schema-qualified table names**: Proxy now resolves a schema-qualified name such as `public.users` to the table in that schema, so statements that use one are encrypted instead of failing type checking. This includes a table whose name is also used in another schema on the search path. Proxy keys encryption configuration by the unqualified table name, so when same-named tables in different schemas declare a column with different EQL domains, statements on that column are refused rather than encrypted with the other table's configuration. Same-named tables that declare the same domain share one configuration and key descriptor, so their ciphertexts and search terms are interchangeable.
Expand Down
22 changes: 22 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@

- Configuration errors:
- [Missing or invalid TLS configuration](#config-missing-or-invalid-tls)
- [Invalid database URL](#config-invalid-database-url)
- [Network configuration change requires restart](#config-network-change-requires-restart)

<!-- ---------------------------------------------------------------------------------------------------- -->
Expand Down Expand Up @@ -1196,6 +1197,27 @@ Encrypted jsonb entry has an invalid selector '{selector}'.
# Configuration errors


## Invalid database URL <a id='config-invalid-database-url'></a>

The connection string passed with `--database-url` cannot be applied.

### Error message

```
Invalid --database-url: {reason}.
```

### How to fix

`--database-url` sets only the user, password, host, port and database name.
Remove any other connection option from the URL, and use a single TCP host and port.
Proxy connects to the database with `sslmode=prefer`, so remove `sslmode` and other `ssl` options.
To verify the database certificate, set `CS_DATABASE__WITH_TLS_VERIFICATION=true`.
To connect to the database without TLS, set `CS_DEVELOPMENT__DISABLE_DATABASE_TLS=true`.
Percent-encode special characters in the password, and prefer `CS_DATABASE__PASSWORD` so the password is not exposed on the command line.

<!-- ---------------------------------------------------------------------------------------------------- -->

## Database <a id='config-missing-or-invalid-tls'></a>

There was a problem with the Tls configuration.
Expand Down
8 changes: 7 additions & 1 deletion packages/cipherstash-proxy-integration/src/migrate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ mod tests {

let args = Args {
config_file_path: "".to_string(),
log_level: LogLevel::Debug,
log_level: Some(LogLevel::Debug),
log_format: LogFormat::Pretty,
command: None,
database_url: None,
db_host: None,
db_port: None,
db_name: None,
db_user: None,
db_password: None,
no_tls: false,
tls: false,
debug: false,
};

let config = match TandemConfig::load(&args) {
Expand Down
65 changes: 63 additions & 2 deletions packages/cipherstash-proxy/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,25 @@ const DEFAULT_CONFIG_FILE: &str = "cipherstash-proxy.toml";
/// CipherStash Proxy keeps your sensitive data in PostgreSQL encrypted and searchable, with no changes to SQL.
///
pub struct Args {
/// Optional full PostgreSQL connection string, e.g.
/// "postgres://user:pass@host:5432/dbname".
/// Sets host, port, user, password and database name at once.
/// Individual flags below override matching parts of the URL.
/// A password in the URL is exposed in shell history and the process
/// list; prefer CS_DATABASE__PASSWORD.
#[arg(long, value_name = "URL", verbatim_doc_comment)]
pub database_url: Option<String>,

/// Optional database host to connect to.
/// Uses env or config file if not specified.
#[arg(short = 'H', long)]
pub db_host: Option<String>,

/// Optional database port to connect to.
/// Uses env or config file if not specified.
#[arg(short = 'P', long)]
pub db_port: Option<u16>,

/// Optional database name to connect to.
/// Uses env or config file if not specified.
#[arg(value_name = "DBNAME")]
Expand All @@ -36,6 +50,34 @@ pub struct Args {
#[arg(short = 'u', long)]
pub db_user: Option<String>,

/// Optional database password.
/// Uses env or config file if not specified.
/// Prefer CS_DATABASE__PASSWORD: a password on the command line, here or
/// in --database-url, is exposed in shell history and the process list.
#[arg(short = 'W', long, verbatim_doc_comment)]
pub db_password: Option<String>,

/// Disable inbound (client-facing) TLS: the proxy listens for client
/// connections in plaintext. Overrides any CS_TLS__* env / config and
/// CS_SERVER__REQUIRE_TLS.
/// Use for local development only. Does not affect the connection from the
/// proxy to the database.
#[arg(long, verbatim_doc_comment, conflicts_with = "tls")]
pub no_tls: bool,

/// Require inbound (client-facing) TLS, as CS_SERVER__REQUIRE_TLS does:
/// clients that do not use TLS are rejected, and startup fails if TLS is
/// not configured. Without this flag the proxy uses TLS when configured
/// and listens in plaintext otherwise.
#[arg(long, verbatim_doc_comment)]
pub tls: bool,

/// Enable verbose (debug) logging. Without it, the proxy run in a
/// terminal logs errors only. An explicit --log-level / CS_LOG__LEVEL
/// takes precedence.
#[arg(long, verbatim_doc_comment)]
pub debug: bool,

/// Optional path to a CipherStash Proxy configuration file.
///
/// Default is "cipherstash-proxy.toml".
Expand All @@ -47,8 +89,8 @@ pub struct Args {
///
/// Optional log level.
///
#[arg(short, long, value_enum, default_value_t = LogConfig::default_log_level(), env = "CS_LOG__LEVEL", global = true)]
pub log_level: LogLevel,
#[arg(short, long, value_enum, env = "CS_LOG__LEVEL", global = true)]
pub log_level: Option<LogLevel>,

///
/// Optional log format. Default level is "pretty" if running in a terminal session, otherwise "structured".
Expand Down Expand Up @@ -80,3 +122,22 @@ pub async fn run(args: Args, config: TandemConfig) -> Result<bool, Error> {
None => Ok(false),
}
}

#[cfg(test)]
mod tests {
use super::Args;
use crate::config::LogLevel;
use clap::Parser;

#[test]
fn log_level_preserves_whether_it_was_explicitly_set() {
temp_env::with_var_unset("CS_LOG__LEVEL", || {
let omitted = Args::try_parse_from(["cipherstash-proxy"]).unwrap();
let explicit =
Args::try_parse_from(["cipherstash-proxy", "--log-level", "info"]).unwrap();

assert_eq!(omitted.log_level, None);
assert_eq!(explicit.log_level, Some(LogLevel::Info));
});
}
}
98 changes: 98 additions & 0 deletions packages/cipherstash-proxy/src/config/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,39 @@ impl Default for LogConfig {
}

impl LogConfig {
/// The log config to use at startup.
///
/// `explicit_level` is true when a level was set with `--log-level` or
/// `CS_LOG__LEVEL`, and `interactive` when stdout is a terminal.
pub fn for_startup(
&self,
explicit_level: bool,
config_file_present: bool,
debug: bool,
interactive: bool,
) -> Self {
if explicit_level {
self.clone()
} else if debug {
self.with_default_level(LogLevel::Debug)
} else if config_file_present || !interactive {
self.clone()
} else {
self.with_default_level(LogLevel::Error)
}
}

/// This config with `level` as the default level, used when no log level
/// was set explicitly.
/// Every other setting, and any per-target level that was set, is kept.
pub fn with_default_level(&self, level: LogLevel) -> Self {
LogConfig {
level,
targets: self.targets.with_default_level(level),
..self.clone()
}
}

pub fn with_level(level: LogLevel) -> Self {
LogConfig {
format: LogConfig::default_log_format(),
Expand Down Expand Up @@ -194,4 +227,69 @@ mod tests {
});
});
}

#[test]
/// The quiet default replaces only the level: settings and explicit
/// per-target levels from env or config are kept.
fn with_default_level_keeps_loaded_settings() {
use crate::config::LogConfig;
let mut loaded = LogConfig {
output: LogOutput::Stderr,
format: LogFormat::Structured,
slow_statements: true,
..Default::default()
};
loaded.targets.mapper_level = LogLevel::Debug;

let config = loaded.with_default_level(LogLevel::Error);

assert_eq!(config.level, LogLevel::Error);
assert_eq!(config.output, LogOutput::Stderr);
assert_eq!(config.format, LogFormat::Structured);
assert!(config.slow_statements);
assert_eq!(config.targets.mapper_level, LogLevel::Debug);
assert_eq!(config.targets.protocol_level, LogLevel::Error);
}

#[test]
/// Containers run without a terminal, an explicit level or a config file,
/// so the quiet default must not apply there.
fn startup_keeps_loaded_level_when_not_interactive() {
use crate::config::LogConfig;
let config = LogConfig::default().for_startup(false, false, false, false);

assert_eq!(config.level, LogConfig::default_log_level());
assert_eq!(
config.targets.protocol_level,
LogConfig::default_log_level()
);
}

#[test]
fn startup_is_quiet_when_interactive() {
use crate::config::LogConfig;
let config = LogConfig::default().for_startup(false, false, false, true);

assert_eq!(config.level, LogLevel::Error);
}

#[test]
/// `--debug` is an explicit flag for this run, so it outranks a config
/// file that sets no level.
fn startup_debug_outranks_config_file() {
use crate::config::LogConfig;
let config = LogConfig::default().for_startup(false, true, true, true);

assert_eq!(config.level, LogLevel::Debug);
}

#[test]
fn startup_explicit_level_outranks_debug() {
use crate::config::LogConfig;
let loaded = LogConfig::with_level(LogLevel::Warn);

let config = loaded.for_startup(true, false, true, true);

assert_eq!(config.level, LogLevel::Warn);
}
}
2 changes: 1 addition & 1 deletion packages/cipherstash-proxy/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub use database::DatabaseConfig;
pub use log::{LogConfig, LogFormat, LogLevel, LogOutput};
use serde::Deserialize;
pub use server::ServerConfig;
pub use tandem::TandemConfig;
pub use tandem::{InboundTls, TandemConfig};
pub use tls::TlsConfig;
use vitaminc_protected::Protected;

Expand Down
Loading
Loading