Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **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.

- **Upstream TLS verification for client traffic**: Connections with `with_tls_verification` enabled now use a cached snapshot of the system root certificates, loaded once when Proxy starts. Unlike Proxy's background database connections, pg-proto client-traffic connections do not apply operating-system revocation checks or enterprise verification policy. Restart Proxy after changing the system trust store.

### Fixed

- **Extended-protocol execution lifecycle regressions**: statement duration and slow-statement metrics now describe each execution rather than the lifetime of its cached prepared statement; distinct portals keep isolated Bind measurements; suspended executions retain their metrics until completion; correlated stale responses and inaccessible connection protocol state close the connection instead of silently omitting metadata transitions; uncorrelated responses retain PostgreSQL passthrough behavior; decryption failures no longer report pending schema changes as successful; and disabling mapping no longer creates empty statement metrics.

- **Query cancellation through Proxy**: Cancellation requests now reach the matching PostgreSQL connection, and their routing entries are removed when the client connection exits. Previously cancellation requests arrived on a separate connection that could not find the original route; retaining those routes globally without cleanup could also leak memory and eventually reject a new connection if PostgreSQL reused a cancellation key.

- **Configured default keyset selection**: when a connection has not selected a keyset explicitly, Proxy now scopes encryption and decryption to `CS_DEFAULT_KEYSET_ID`. Previously it passed no keyset to ZeroKMS and could silently use the account default instead, deriving different searchable-encryption terms when the two defaults differed. Before upgrading, verify that the configured and account defaults are intentional; values written by an affected version under the unintended account default must be decrypted with that old keyset and re-encrypted under the configured default.
Expand Down
17 changes: 17 additions & 0 deletions packages/cipherstash-proxy-integration/src/schema_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,4 +455,21 @@ mod tests {
assert!(exists);
assert_ciphertext_at_rest(&encrypted, 1, "preserved batch secret").await;
}

#[tokio::test]
async fn multi_statement_simple_query_keeps_the_connection_usable() {
let client = connect_for_test(*PROXY).await;
let first = table("execution_lifecycle_first");
let second = table("execution_lifecycle_second");

client
.batch_execute(&format!(
"CREATE TABLE {first} (id bigint); CREATE TABLE {second} (id bigint)"
))
.await
.unwrap();

let value: i32 = client.query_one("SELECT 1", &[]).await.unwrap().get(0);
assert_eq!(value, 1);
}
}
6 changes: 4 additions & 2 deletions packages/cipherstash-proxy/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ A prepared statement bound to parameter values, ready to execute. Either `Encryp
carrying the analysed statement, or `Passthrough` when nothing in it is encrypted.

**Statement metrics scope**:
The measurement window around a single statement — opened at Parse or Query, closed when
the statement completes. Many of these occur per connection.
The measurement window around one statement execution occurrence. A simple Query owns one;
an extended-protocol Execute owns one from its first execution through any suspension and
resumption until completion or failure, while Parse timing belongs to the prepared Statement
and may be attributed to each execution occurrence.
_Avoid_: session (`start_session`, `SessionId` and the
`..._statements_session_duration_seconds` metric all use this sense and are misnamed).

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
status: accepted
---

# Context owns CipherStash protocol metadata transitions

`Context` is the connection seam for CipherStash metadata associated with PostgreSQL Statements,
Portals, operations, and statement metrics scopes. Frontend and Backend remain protocol adapters:
they interpret wire messages, while Context applies each correlated metadata transition atomically
and returns the knowledge or effects the adapter needs. pg-proto continues to own protocol ordering
and backpressure, and Schema middleware continues to own transactional schema state under ADR-0001.

The correlated protocol state is kept in one internal state model rather than independently locked
maps. Context never holds that state lock across asynchronous work, metrics emission, or Schema
middleware calls; a transition first changes protocol state and returns explicit effects, then
Context applies those effects. Passthrough and encrypted traffic use the same lifecycle seam, and
inaccessible, stale, or inconsistent state fails the connection closed rather than silently omitting
a transition.

Statement metrics scopes belong to execution occurrences, not prepared Statements. A suspended and
resumed execution retains one scope until completion or failure; distinct Portals and repeated
executions receive distinct scopes, while Parse timing remains knowledge of the prepared Statement
that may be attributed to each execution observation. Existing Prometheus metric names remain stable.
9 changes: 9 additions & 0 deletions packages/cipherstash-proxy/src/config/tandem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,15 @@ impl TandemConfig {
development: None,
}
}

#[cfg(test)]
pub fn disable_mapping_for_testing(&mut self) {
self.development = Some(DevelopmentConfig {
disable_mapping: true,
disable_database_tls: false,
enable_mapping_errors: false,
});
}
}

impl EncryptConfig {
Expand Down
15 changes: 15 additions & 0 deletions packages/cipherstash-proxy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ impl Error {

#[derive(Error, Debug)]
pub enum ContextError {
#[error("Operation has no in-flight Execute state")]
OperationWithoutExecute,

#[error("Connection protocol state is unavailable")]
ProtocolStateUnavailable,

#[error("Operation could not be found in connection protocol state")]
UnknownOperation,

#[error("Operation has no in-flight Describe state")]
UnknownDescribe,

#[error("Prepared statement has no metrics template")]
StatementMetricsUnavailable,

#[error("Portal could not be found in context")]
UnknownPortal,
}
Expand Down
Loading
Loading