Skip to content

fix(bindings): resolve cloud Postgres passwords in Rust (ALIEN-334) - #207

Open
lilienblum wants to merge 11 commits into
mainfrom
dan/alien-334-cloud-postgres-binding
Open

fix(bindings): resolve cloud Postgres passwords in Rust (ALIEN-334)#207
lilienblum wants to merge 11 commits into
mainfrom
dan/alien-334-cloud-postgres-binding

Conversation

@lilienblum

@lilienblum lilienblum commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Containers and Daemons with a Postgres binding could not connect on AWS, GCP, or Azure. Cloud bindings carry a pointer to the password secret, but neither the Rust provider nor the public bindings package resolved it. This PR resolves the secret with the workload identity and exposes complete connection details through the Rust and TypeScript APIs.

Approach

Postgres remains connection-only: Alien resolves binding fields, the password, and the TLS policy, then the application connects with its own driver. Resolution lives in alien-bindings so Rust and TypeScript share one provider implementation.

Changes

Cloud password resolution (ALIEN-334)

  • Added Aurora, Cloud SQL, and Flexible Server Postgres providers.
  • Each provider performs one secret read using the workload's existing postgres/data-access permission and returns PostgresConnectionParams.
  • Secret-read failures use retryable POSTGRES_SECRET_RESOLUTION_FAILED; malformed bindings fail before the secret read with non-retryable BINDING_CONFIG_INVALID.
  • Added the Postgres napi handle and the public postgres(name).connection() factory.
  • Connection strings use the shared Rust encoder and preserve RFC 3986 userinfo escaping.

Managed TLS verification (ALIEN-337)

  • Aurora uses verify-full with the official Amazon RDS global root bundle.
  • Azure Flexible Server uses verify-full with Microsoft's currently recommended roots.
  • Cloud SQL uses verify-ca with its per-instance CA because the Private Service Connect endpoint is an IP that is not present in the server certificate.
  • The napi boundary carries the resolved CA certificates. TypeScript produces node-postgres TLS options with rejectUnauthorized: true; no managed path uses rejectUnauthorized: false.
  • Invalid or contradictory TLS data fails closed with INVALID_POSTGRES_TLS_CONFIG.

Unambiguous BYO TLS (ALIEN-339)

  • External Postgres now has an explicit sslMode: verify-full (default) or disable (explicit plaintext opt-out).
  • Removed prefer; node-postgres cannot represent libpq's TLS-then-plaintext fallback in a static ssl config.
  • BYO verify-full uses the runtime's system trust store.

For node-postgres, pass the individual connection fields and ssl object rather than only connectionString; URL TLS parameters can replace the explicit TLS object and its provider roots.

The password is resolved when the binding handle loads. Calling the factory again creates a fresh handle and picks up a rotated password.

Validation

Check Result
Focused Rust cloud/BYO provider tests 24 passed
Rust napi Postgres mapping tests 2 passed
Rust core Postgres binding tests 5 passed
TypeScript factory boundary tests 16 passed
Real napi addon Postgres tests 4 passed
Package layout and packed import checks 28 passed
TypeScript typecheck and Biome on changed files passed
Focused Rust clippy for alien-bindings and the napi crate passed; only pre-existing dependency/workspace warnings
Embedded CA parsing/checksums 108 AWS roots and 3 Azure roots; documented checksums match

Tests cover exact connection strings, every accepted TLS mode, CA transport, Cloud SQL's hostname-verification exception, explicit plaintext opt-out, unknown-mode rejection, invalid TLS configuration, secret-read failures, and password redaction.

Deployment follow-up

Cloud SQL binding producers must populate serverCaCertificates with the instance CA roots, including both roots during a rotation. The public resolver deliberately fails closed when that field is absent or unresolved.

Not validated here

Real cloud end-to-end still requires deployed Aurora, Cloud SQL, and Flexible Server instances reachable from same-stack workloads. Verify SELECT 1, certificate validation, hostname validation where applicable, and the expected retryable error after revoking secret-read permission.

cargo fmt --all -- --check still reports pre-existing rustfmt drift in untouched workspace files; every Rust file changed by this PR is fmt-clean.

Containers and Daemons with a Postgres binding could not connect on
AWS/GCP/Azure. The cloud binding variants carry only a pointer to the
password secret (Secrets Manager ARN / Secret Manager name / Key Vault
URI); the workload must resolve it with its own identity, which the
postgres/data-access permission set already grants. Nothing did: the Rust
provider rejected cloud bindings as "resolved by the workload SDK", and
that SDK resolver no longer exists.

Resolve them in alien-bindings instead, which fixes Rust and TypeScript
together:

- New aurora / cloud_sql / flexible_server providers read the password
  from their cloud secret store (reusing the in-repo Secrets Manager,
  Secret Manager, and Key Vault clients) and build the connection
  parameters. load_postgres dispatches to them behind the usual
  aws/gcp/azure feature gates.
- Aurora connects to the cluster endpoint; every cloud backend uses
  sslmode=require. Connection strings stay byte-identical to the existing
  resolver, including percent-encoding the RFC 3986 sub-delims.
- A failed secret read reports the new retryable
  POSTGRES_SECRET_RESOLUTION_FAILED; a malformed or unresolved binding
  stays non-retryable BINDING_CONFIG_INVALID and never reaches the secret
  store. The resolved password is still kept out of Debug output.
- Expose the binding through the napi addon and a postgres() factory in
  @alienplatform/bindings, re-exported from the SDK facade, with ssl
  derived from sslmode for drivers that take one.
- Point the comprehensive-typescript e2e handler at the real binding
  instead of its inlined copy of the deleted resolver, and correct the
  docs that pointed the two sides at each other.
@lilienblum lilienblum self-assigned this Jul 25, 2026
@lilienblum
lilienblum requested a review from alongubkin July 25, 2026 23:18
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

This PR completes secure Postgres binding support across Rust and TypeScript.

  • Resolves managed-cloud database passwords through each provider’s secret store while keeping cloud handles uncached for password rotation.
  • Adds provider-specific verified TLS policies and transports CA certificates through the napi and TypeScript boundaries.
  • Replaces ambiguous external Postgres prefer behavior with verified TLS by default and an explicit plaintext opt-out.
  • Exposes Postgres connection details through the public bindings and SDK packages.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains in the previously reported TLS or password-rotation paths.

Important Files Changed

Filename Overview
crates/alien-bindings/src/providers/postgres/runtime.rs Routes Postgres variants, caches only inline-password bindings, and re-resolves managed-cloud passwords on each load.
crates/alien-bindings/src/providers/postgres/mod.rs Defines validated Postgres TLS policies, including provider roots and fail-closed CA handling.
crates/alien-bindings/src/provider.rs Delegates Postgres loading to the dedicated runtime without reintroducing the generic binding cache.
crates/alien-bindings-node/src/postgres.rs Carries resolved connection fields, TLS mode, and CA certificates across the napi boundary.
packages/bindings/src/factories.ts Maps verified Postgres TLS modes to node-postgres options and creates a fresh lazy handle for each factory invocation.
crates/alien-core/src/bindings/postgres.rs Makes external Postgres use verify-full by default with an explicit disable mode.
packages/bindings/src/types.ts Adds discriminated public connection types for disabled, verify-ca, and verify-full TLS policies.

Sequence Diagram

sequenceDiagram
  participant App as Rust/TypeScript App
  participant Bindings as Public Bindings API
  participant Provider as Rust Postgres Provider
  participant Secrets as Cloud Secret Store
  App->>Bindings: postgres(name)
  Bindings->>Provider: resolve binding
  alt Managed cloud binding
    Provider->>Secrets: read password with workload identity
    Secrets-->>Provider: current password
  end
  Provider-->>Bindings: connection fields + TLS policy + CA roots
  Bindings-->>App: connection()
Loading

Reviews (14): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread packages/bindings/src/factories.ts Outdated
Comment thread crates/alien-bindings/src/bindings.rs
@lilienblum
lilienblum requested a review from ItamarZand88 July 26, 2026 00:08
The `postgres()` doc promised that calling it again picks up a rotated
password, but `load_postgres` cached the resolved handle, so a rotation was
unreachable for the life of the process. Make the documented behavior true
instead of weakening the doc: only the inline-password variants (Local /
External) are cached — they are a pure env-var parse — while the three cloud
variants re-resolve, and therefore re-read their secret, on every call.
`postgres()` is a per-handle setup call, not a per-query one, so this is one
secret-store read per handle.

A new provider test pins it end to end against a fake Secrets Manager reached
through the client's endpoint override: two loads of the same Aurora binding
must perform two GetSecretValue calls, and the second must observe the
password rotated between them.

Also collapse `AuroraPostgres` / `CloudSqlPostgres` / `FlexibleServerPostgres`
— three types that differed in nothing — into one `CloudPostgres` holding the
single Debug/Binding/Postgres impl. Each cloud module now exposes only a
`resolve` function returning the connection parameters; the per-cloud secret
read and payload decoding are unchanged.

On the TypeScript side, an unrecognized `sslmode` from the addon now throws a
typed `UnknownPostgresSslModeError` (code `UNKNOWN_POSTGRES_SSLMODE`) rather
than a bare `Error` that `guard` flattened into the generic fallback code, and
the redundant `Postgres.connectionString()` accessor is dropped —
`connection().connectionString` already exposes it.
load_postgres skips the handle cache for cloud bindings so a rotated
password is picked up, but the handle was also the only thing holding the
secret-store client — so every postgres() call built a fresh
reqwest::Client. A discarded client keeps its pooled sockets for the full
90s idle timeout, so a workload reconnecting in a loop churns file
descriptors on a 1024-FD budget.

Cache the client per platform (and, on AWS, the credential provider it
wraps; on Azure, its token cache) while still resolving the password on
every load. Rotation and connection pooling both hold.

Also moves CloudPostgres and resolve_secret_locator into
providers/postgres/cloud.rs, gated once at the mod declaration instead of
six repeated cfg attributes.

Refs ALIEN-334
The knowledge base still describes the app-facing binding surface as
storage/kv/queue/vault. That stale enumeration is what dropped postgres
in the first place, so reviews of postgres binding code start from a
wrong premise. Records the accepted cloud TLS posture alongside it.
@lilienblum
lilienblum force-pushed the dan/alien-334-cloud-postgres-binding branch from 16ae64d to daff5a9 Compare July 26, 2026 06:53
@lilienblum

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread packages/bindings/src/factories.ts
@lilienblum

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread packages/bindings/src/factories.ts Outdated
PACKAGE_LAYOUT.md lists it as a pinned export and index.ts exports it,
but the fixture's BINDINGS_EXPORTS omitted it — so dropping the export
would have passed CI unnoticed.
@lilienblum

lilienblum commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

@ItamarZand88 Good catch, and thanks for reading it that closely.

Confirmed and fixed in 44749c5UnknownPostgresSslModeError is now in BINDINGS_EXPORTS. You were right that it was pinned in PACKAGE_LAYOUT.md (line 40) and exported from index.ts (line 34) while the fixture never listed it.

I checked the guard actually bites rather than just re-running it green: removing the export from index.ts now fails 2 of 28 checks (Bun and Node both report "missing pinned exports"), where before it passed clean. Restored after.

Worth noting this is the third instance of the same failure mode on this branch. The original bug was postgres being dropped from the app-facing binding enumeration with no test to catch it; container turned out to be missing from this same fixture list; and now this. Each time something left a list and nothing failed. Also why the --exclude -> --filter-expr change in #208 exists — same shape, different list.

sslmode=prefer maps to ssl:false because node-postgres has no prefer
mode, so it never attempts TLS. The doc described the mechanism but not
the consequence: credentials cross the wire unencrypted, and unlike the
managed clouds a BYO database is not necessarily on a private network.
@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@lilienblum
lilienblum force-pushed the dan/alien-334-cloud-postgres-binding branch from e735809 to 15e91ce Compare July 27, 2026 14:43
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.

2 participants