Skip to content

fix(rds): flip StorageEncrypted default to true (CIS-AWS RDS.3)#240

Open
Cre-eD wants to merge 1 commit intomainfrom
fix/rds-encryption-secure-by-default
Open

fix(rds): flip StorageEncrypted default to true (CIS-AWS RDS.3)#240
Cre-eD wants to merge 1 commit intomainfrom
fix/rds-encryption-secure-by-default

Conversation

@Cre-eD
Copy link
Copy Markdown
Contributor

@Cre-eD Cre-eD commented May 9, 2026

Summary

PR #239 added the StorageEncrypted *bool field as opt-in (nil → false → unencrypted), preserving pre-2026.5 behaviour. That left every customer who hadn't read the new field's docs on unencrypted RDS, violating CIS-AWS Foundations RDS.3.

This 1-line-each change flips the resolver from `lo.FromPtr(*ptr)` to `lo.FromPtrOr(*ptr, true)` at both call sites, making encryption the secure default while preserving every existing customer's data.

State Before After
nil (omitted from YAML) false (unencrypted) true (encrypted)
&true (explicit) true true
&false (explicit) false false

Why existing DBs are safe — IgnoreChanges already in place

PR #239 added `sdk.IgnoreChanges([]string{"storageEncrypted"})` on the resource opts. Pulumi's IgnoreChanges semantics: "treat the desired value as the recorded state value for the diff."

Scenario State New spec Diff Outcome
Existing unencrypted, never set field false true (default flip) IgnoreChanges silences No replacement, no data loss
Existing encrypted, set true previously true true no change unchanged
Customer explicitly sets false in YAML * false IgnoreChanges silences honoured, no second-guessing
Brand-new stack none true initial create encrypted ✓

Customers who genuinely want to migrate an existing unencrypted RDS to encrypted still need the out-of-band path AWS forces (snapshot → encrypted-copy → restore → re-import). The IgnoreChanges deliberately blocks SC from attempting that migration via destructive replacement.

Tests

pkg/clouds/aws/rds_storage_encrypted_test.go updated:

  • omit-case description: "legacy default, encryption off" → "secure default, encryption on"
  • resolver: `lo.FromPtr(cfg.StorageEncrypted)` → `lo.FromPtrOr(cfg.StorageEncrypted, true)`
  • expected: `wantSet && wantVal` → `!wantSet || wantVal`

Locally: `go build ./...`, `go vet ./...`, `TestReadRds{Mysql,Postgres}Config_StorageEncrypted` all pass.

Companion

simple-container-com/actions#9 tightens the `go-aws-rds-no-storage-encryption` Semgrep rule to whitelist only the `lo.FromPtrOr(_, true)` shape — bare `lo.FromPtr(*ptr)` and `lo.FromPtrOr(*ptr, false)` both still fire the rule.

After both land:

  • This api PR's Semgrep status passes (post-fix code uses the whitelisted shape).
  • Any regression to bare `lo.FromPtr(*bool)` on a security-critical default gets flagged at PR time across SC + every consumer repo using the ruleset.

Test plan

  • go build ./... + go vet ./... locally
  • TestReadRds{Mysql,Postgres}Config_StorageEncrypted green
  • CI green
  • (after actions#9 merges) re-scan: 0 findings of go-aws-rds-no-storage-encryption
  • Validate via SC preview build against an existing PAY-SPACE staging stack — confirm pulumi preview shows no destructive replacement on the existing unencrypted RDS instances

PR #239 added the `StorageEncrypted *bool` field to MysqlConfig /
PostgresConfig as opt-in (nil → false → unencrypted), preserving
pre-2026.5 SC behaviour. That keeps every customer who hasn't read
the new field's docs on unencrypted RDS, violating CIS-AWS Foundations
RDS.3.

This change flips the resolution helper from `lo.FromPtr(*bool)` to
`lo.FromPtrOr(*bool, true)` at both call sites:

  pkg/clouds/pulumi/aws/rds_mysql.go:137
  pkg/clouds/pulumi/aws/rds_postgres.go:121

Resolution table:
  before:                       after:
  nil   → false (unencrypted)   nil   → true  (encrypted, secure default)
  &true → true                  &true → true  (unchanged)
  &false → false                &false → false (unchanged, explicit opt-out)

Why this is safe for existing instances
=======================================

PR #239 already added `IgnoreChanges([]string{"storageEncrypted"})` on
the resource opts for exactly this scenario. Pulumi's IgnoreChanges
diffing rule: "treat the desired value as the recorded state value
for the field." So:

  Existing unencrypted instance, customer never set the field in YAML:
    state.storageEncrypted = false
    new spec.storageEncrypted = true   (from lo.FromPtrOr(nil, true))
    diff: IgnoreChanges → no change registered → no operation
    ↳ existing DB stays as-is, NOT replaced. No data loss.

  Existing encrypted instance, customer set true previously:
    state.storageEncrypted = true
    new spec.storageEncrypted = true
    diff: trivially no change
    ↳ unchanged.

  Customer explicitly sets `storageEncrypted: false` in YAML:
    state.storageEncrypted = false (or whatever it was)
    new spec.storageEncrypted = false (lo.FromPtrOr(&false, true) = false)
    diff: IgnoreChanges → no change
    ↳ honoured; no second-guessing.

  Brand-new stack, customer never sets the field:
    no prior state — IgnoreChanges has no effect on initial create
    new spec.storageEncrypted = true
    ↳ AWS creates an encrypted instance. ✓ secure default.

Out-of-band migration
=====================

Customers who actually want to migrate an existing unencrypted RDS to
encrypted still have to do it the AWS-immutable-field way: snapshot →
encrypted-copy → restore → re-import. The IgnoreChanges silences SC's
spec from triggering Pulumi to attempt that migration via destructive
replacement. Documented on the struct field comments.

Tests
=====

`pkg/clouds/aws/rds_storage_encrypted_test.go` updated:
- "omitted → nil (legacy default, encryption off)" →
  "omitted → nil (secure default, encryption on)"
- resolution helper changed from `lo.FromPtr(cfg.StorageEncrypted)` to
  `lo.FromPtrOr(cfg.StorageEncrypted, true)`
- expected value changed from `wantSet && wantVal` to
  `!wantSet || wantVal`
- assertion message updated accordingly.

Build, vet, and TestRead{Mysql,Postgres}Config_StorageEncrypted all
pass locally.

Companion: simple-container-com/actions PR #9 (open) tightens
`go-aws-rds-no-storage-encryption` to whitelist only the
`lo.FromPtrOr(_, true)` shape — bare `lo.FromPtr(*ptr)` and
`lo.FromPtrOr(*ptr, false)` both still fire the rule, so this
codebase will pass the rule going forward and any regression to the
old shape gets flagged at PR time.

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

github-actions Bot commented May 9, 2026

Semgrep Scan Results

Repository: api | Commit: a69585f

Check Status Details
🚨 Semgrep ERROR 11 error(s), 73 warning(s), 108 total

Scanned at 2026-05-09 08:17 UTC

Cre-eD added a commit to simple-container-com/actions that referenced this pull request May 9, 2026
…ure default

Earlier draft of this PR whitelisted any `<ptr>.Bool(lo.FromPtr(*ptr))`
shape — but `lo.FromPtr(nil)` returns the type's zero value (false for
*bool), so that suppression accepted code that defaults to UNENCRYPTED
when a customer hasn't set the field. That permanently lowers the rule
below CIS-AWS RDS.3 and was the wrong call.

Replaced with a tighter whitelist that requires the literal-true
fallback:

  StorageEncrypted:\s*\w+\.Bool\(lo\.FromPtrOr\([^()\n]+,\s*true\s*\)\)

What still fires:
  - bare `StorageEncrypted: false` and `StorageEncrypted: Bool(false)`
  - bare `lo.FromPtr(*ptr)` (defaults to false → unencrypted)
  - `lo.FromPtrOr(*ptr, false)` (explicit insecure default)
  - any other arbitrary wrapper around a runtime expression

What's suppressed:
  - `StorageEncrypted: true` (literal)
  - `<ptr>.Bool(true)` (canonical Pulumi truthy)
  - `<ptr>.Bool(lo.FromPtrOr(*ptr, true))` (secure-by-default
    runtime config)

The runtime-config form must be paired with
`IgnoreChanges([]string{"storageEncrypted"})` on the resource opts to
keep the default-flip safe for pre-existing unencrypted instances.
The pairing isn't regex-verified; the wrapper shape + literal-true
fallback are the design signal.

Tests
=====

`semgrep-scan/tests/go_cases.go` updated:
- Renamed positive cases to `rdsRuntimeSecure{Sdk,Pulumi}` (was
  `rdsRuntimeOptIn{Sdk,Pulumi}`) and switched to the
  `lo.FromPtrOr(_, true)` shape.
- Added `// ruleid:` for `lo.FromPtr(*ptr)` (bare-FromPtr-still-fires)
  and `lo.FromPtrOr(*ptr, false)` (explicit-insecure-fallback).
- Kept arbitrary-wrapper guard.

Total go_cases.go: 35/35 passes; full ruleset 129/129 across all
fixtures.

Companion: simple-container-com/api#240 flips the call sites in
rds_{mysql,postgres}.go from `lo.FromPtr(*ptr)` to
`lo.FromPtrOr(*ptr, true)`, satisfying both this rule and CIS-AWS
RDS.3 going forward.

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

github-actions Bot commented May 9, 2026

Security Scan Results

Repository: api | Commit: a69585f

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

Scanned at 2026-05-09 08:18 UTC

@Cre-eD
Copy link
Copy Markdown
Contributor Author

Cre-eD commented May 10, 2026

✅ Empirical validation passed

Ran sc provision --preview --diff --skip-refresh against an existing unencrypted `aws-rds-postgres` in `Integrail/devops/.sc/stacks/integrail/server.yaml` (staging environment), with SC built from this PR's branch (`v2026.5.7-pre.7436b6b-preview.7436b6b`).

Plan summary

Action Count
create 0
update 2
replace 2
delete 0
unchanged 463 ← includes `aws-rds-postgres`

The 2 replacements + 2 updates are unrelated cloudtrail-security-helpers `docker:index/remoteImage:RemoteImage` rebuilds (new SC version → new helper image digest). No RDS, RDS-cluster, EC2, VPC, or related resource appears in the diff.

What this proves

  • Spec resolved to `storageEncrypted: true` on the new code (otherwise diff couldn't have been suppressed — would've trivially matched state).
  • Cloud-actual is `storage_encrypted: false` on the existing instance.
  • `sdk.IgnoreChanges([]string{"storageEncrypted"})` (added in fix(rds): set StorageEncrypted=true on mysql + postgres instances #239) correctly silenced the spec-state drift — Pulumi registered the resource as "unchanged" rather than "replace".
  • No data-loss path. Default-flip is safe to merge.

Validation PR (closed, branch deleted): Integrail/devops#364

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