fix(bindings): resolve cloud Postgres passwords in Rust (ALIEN-334) - #207
fix(bindings): resolve cloud Postgres passwords in Rust (ALIEN-334)#207lilienblum wants to merge 11 commits into
Conversation
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.
Greptile SummaryThis PR completes secure Postgres binding support across Rust and TypeScript.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains in the previously reported TLS or password-rotation paths.
|
| 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()
Reviews (14): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile
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.
16ae64d to
daff5a9
Compare
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.
|
@ItamarZand88 Good catch, and thanks for reading it that closely. Confirmed and fixed in 44749c5 — I checked the guard actually bites rather than just re-running it green: removing the export from 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; |
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.
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
e735809 to
15e91ce
Compare
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-bindingsso Rust and TypeScript share one provider implementation.Changes
Cloud password resolution (ALIEN-334)
postgres/data-accesspermission and returnsPostgresConnectionParams.POSTGRES_SECRET_RESOLUTION_FAILED; malformed bindings fail before the secret read with non-retryableBINDING_CONFIG_INVALID.postgres(name).connection()factory.Managed TLS verification (ALIEN-337)
verify-fullwith the official Amazon RDS global root bundle.verify-fullwith Microsoft's currently recommended roots.verify-cawith its per-instance CA because the Private Service Connect endpoint is an IP that is not present in the server certificate.rejectUnauthorized: true; no managed path usesrejectUnauthorized: false.INVALID_POSTGRES_TLS_CONFIG.Unambiguous BYO TLS (ALIEN-339)
sslMode:verify-full(default) ordisable(explicit plaintext opt-out).prefer; node-postgres cannot represent libpq's TLS-then-plaintext fallback in a staticsslconfig.verify-fulluses the runtime's system trust store.For node-postgres, pass the individual connection fields and
sslobject rather than onlyconnectionString; 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
alien-bindingsand the napi crateTests 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
serverCaCertificateswith 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 -- --checkstill reports pre-existing rustfmt drift in untouched workspace files; every Rust file changed by this PR is fmt-clean.