Skip to content

feat(gcp): tunable Cloud NAT ports and Cloud SQL private IP#369

Open
Cre-eD wants to merge 2 commits into
mainfrom
feat/egress-nat-tuning-and-cloudsql-private-ip
Open

feat(gcp): tunable Cloud NAT ports and Cloud SQL private IP#369
Cre-eD wants to merge 2 commits into
mainfrom
feat/egress-nat-tuning-and-cloudsql-private-ip

Conversation

@Cre-eD

@Cre-eD Cre-eD commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

Two opt-in GCP capabilities, defaults unchanged:

1. Tunable Cloud NAT (externalEgressIp)

  • minPortsPerVm, maxPortsPerVm — NAT source-port budget per VM.
  • dynamicPortAllocation — let a VM scale ports between min and max on demand.
  • endpointIndependentMapping — toggle EIM (must be off to use dynamic allocation).

2. Cloud SQL private IP (gcp-cloudsql-postgres)

  • privateNetwork — give the instance a private IP on a VPC (requires Private Services Access).
  • publicIpEnabled — toggle the public IPv4 (default true).
  • The in-cluster cloud-sql-proxy (runtime sidecar and init-job) automatically dials --private-ip whenever privateNetwork is set.

Why

The Cloud NAT created for externalEgressIp is hardcoded to 64 min ports per VM with endpoint-independent mapping on and no dynamic port allocation. Workloads that open many concurrent outbound connections (e.g. per-task DB connections plus outbound API calls) can exhaust NAT source ports; the dropped SYNs surface downstream as dial ... i/o timeout on new connections and, for a proxied Cloud SQL, as intermittent server closed the connection unexpectedly in clients. These knobs let operators raise the port budget / enable dynamic allocation, or remove the public egress path for the database entirely by moving the proxy to private IP.

Backwards compatibility

Every new field is a pointer and defaults to the prior behaviour (64 min ports, EIM on, dynamic allocation off, public IPv4 on). A nil/unset config produces the exact same NAT and IP configuration as before, so existing clusters and instances are untouched until they opt in. The only wire-level change on re-provision with default config is enableDynamicPortAllocation going from unset to an explicit false, which is a no-op in GCP.

Validation

  • dynamicPortAllocation requires endpointIndependentMapping: false and power-of-two port bounds (GCP constraints), enforced in ExternalEgressIpConfig.Validate().
  • publicIpEnabled: false requires privateNetwork (otherwise the instance would be unreachable), enforced in the Postgres provisioner.

Tests

  • resolveNatPortSettings: defaults preserved for nil/empty config; overrides applied.
  • NAT validation: valid dynamic-allocation config, and rejections for EIM-on-with-DPA, non-power-of-two bounds, out-of-range and inverted bounds.
  • ipConfiguration: nil when unset (leaves instance IP config untouched), requireSsl-only unchanged, private network keeps public on by default, publicIpEnabled: false disables IPv4.
  • proxy command args: --private-ip emitted only when requested, for both runtime and init-job proxies.

go build ./..., go vet, and go test ./pkg/clouds/... all pass.

Add opt-in Cloud NAT port/mapping tuning to externalEgressIp (minPortsPerVm,
maxPortsPerVm, dynamicPortAllocation, endpointIndependentMapping) and Cloud SQL
private-IP support (privateNetwork, publicIpEnabled). The in-cluster
cloud-sql-proxy dials --private-ip whenever a private network is configured.

All new fields are optional and default to the previous behaviour (64 min
ports, endpoint-independent mapping on, dynamic port allocation off, public
IPv4 on), so existing clusters and instances are unchanged until they opt in.

Rationale: high-egress workloads that open many concurrent outbound connections
can exhaust NAT source ports under the fixed 64-port, endpoint-independent
defaults; dropped SYNs then show up as dial i/o timeouts on new connections.
The new knobs allow raising the port budget and enabling dynamic port
allocation, or removing the public egress path for the database entirely via
private IP.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Semgrep Scan Results

Repository: api | Commit: eecfb61

Check Status Details
⚠️ Semgrep Warning 1 warning(s), 5 total

Scanned at 2026-07-22 11:49 UTC

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Security Scan Results

Repository: api | Commit: eecfb61

Check Status Details
✅ Secret Scan Pass No secrets detected
⚠️ Dependencies (Trivy) High 3 high, 7 total
⚠️ Dependencies (Grype) High 3 high, 7 total
📦 SBOM Generated 523 components (CycloneDX)

Scanned at 2026-07-22 11:50 UTC

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

📊 Statement coverage

Measured on the documented included set (see docs/TESTING.md → Coverage scope). Observe-only — no regression gate is enforced yet.

Scope This PR main baseline Δ
Included set (Gold-tier denominator) 90.4% 90.3% +0.1 pp
Full set (whole repo, transparency) 28.3% 28.2% +0.1 pp

Baseline: main @ 26ee13b

Addresses multi-model review of the NAT-tuning + Cloud SQL private-IP change:

- NAT validation now checks EFFECTIVE port bounds (defaults applied) against
  GCP's real range (floor 32, ceiling 65536) and requires max > min under
  dynamic port allocation, so a lone maxPortsPerVm below the default minimum or
  a sub-floor value is rejected up front instead of failing late at pulumi up.
- resolveNatPortSettings forces endpoint-independent mapping off whenever
  dynamic port allocation is on, keeping the resolved NAT args self-consistent
  regardless of validation order.
- EnableDynamicPortAllocation is emitted only when true, so the default path is
  byte-identical to the previous NAT resource (no spurious update on existing
  clusters).
- The in-cluster proxy switches to --private-ip only once the public IP is
  disabled (UsesPrivateIpProxy), not the instant privateNetwork is set, so
  adding a private IP while public stays on is a safe no-cutover step.
- publicIpEnabled:true is now a genuine no-op in ipConfiguration (was starting
  to manage the IP block and could wipe out-of-band authorized networks); empty
  privateNetwork strings are treated as unset.
- Cloud SQL config gains a Validate() (availabilityType, privateNetwork format,
  publicIpEnabled requires privateNetwork) plus HasPrivateNetwork/
  UsesPrivateIpProxy helpers; instance-create errors hint at the Private
  Services Access prerequisite.
- Shared port-bound constants; regenerated JSON schemas for the new fields;
  guide note on NAT port tuning. Added tests for the new validation branches,
  requireSsl=false, publicIpEnabled no-op, empty private network, the config to
  proxy derivation, and the container-args --private-ip path.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
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.

1 participant