Skip to content

Commit 636c647

Browse files
wz-gsaOpenCode Agent
andcommitted
feat(oracle19c): FedRAMP-Moderate TLS (SSL option group, TCPS 2484) + review fixes (#519,#538)
Implements encryption-in-transit for Oracle SE2 (design ratified 7/7 by consensus). On RDS Oracle, TLS requires a separate TCPS listener via an SSL option group — pg/ mysql serve TLS on their standard port, so Oracle's port divergence is unavoidable AWS architecture, not a broker inconsistency. - baselines/oracle19c/options.yml: SSL option — Port=2484, SQLNET.SSL_VERSION=1.2, SQLNET.CIPHER_SUITE=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (FedRAMP+FIPS+RSA-CA compatible), FIPS.SSLFIPS_140=TRUE; ssl_port + ca_cert_family metadata. - baselines.go: typed SSL settings + cipherSuiteFor helper. - engine.go/engine_baselines.go: RDSBaseline.BaselineOptions builds the SSL OptionConfiguration referencing the instance SG, with a FAIL-CLOSED cipher/CA guard (reject ECDSA-only cipher on an RSA CA before the AWS call). oracleSSLPort helper. pg/mysql return nil (create path unchanged). - option_group.go: new ProvisionBaselineOptionGroup — create+attach an option group at CREATE time (existing path was upgrade-only); fail-closed; no-op for empty baselines. - create_worker.go: provision the baseline option group and set OptionGroupName (Oracle only). - credentials.go: Oracle binding now publishes TLS — port 2484, TCPS DESCRIPTION connect string (uri + jdbcUrl) with SSL_SERVER_CERT_DN, ssl_server_dn_match=true, protocol=tcps, and the GovCloud RDS CA bundle URL. ssl_required=true is now truthful (backed by the SSL option group). Boundary (honest): the broker attaches the SSL option + expresses 2484 intent, but CANNOT make the connection TLS-only — opening 2484 / denying 1521 is a platform security-group (cg-provision) change. Plan stays not-customer-ready until that lands (tracked; ENABLE_ORACLE gate holds it). Review fixes (@markdboyd): trimmed stale/archaeology comments in smoke main.go + README (context belongs in git history); dropped epic ref from cleanup header; rewrote ci/cleanup-smoke-service.sh to be ENGINE-AGNOSTIC (matches smoke-tests.* $SERVICE_PLAN across RDS/Elasticache/Elasticsearch; cf delete-service cascades to bindings, dropping ~15 lines of per-app unbind/curl/jq). Tests: TestOracleSSLOptionBaseline, TestOracleBaselineOptionsBuild (SG ref + pg/mysql nil), updated TestGetCredentials oracle-se2 to the TCPS payload, mock ProvisionBaselineOptionGroup, create-worker mocks wired. go test ./... + cmd/tasks green; go vet + pre-commit (shellcheck/shfmt) green. Co-authored-by: OpenCode Agent <agent@gsa.gov>
1 parent c79af91 commit 636c647

14 files changed

Lines changed: 420 additions & 132 deletions

ci/cleanup-smoke-service.sh

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,32 @@
11
#!/bin/bash
22
# cleanup-smoke-service.sh — best-effort teardown of leftover smoke-test service
3-
# instances + apps for a given SERVICE_PLAN, run from a Concourse `ensure:` step so
4-
# a mid-run failure of the smoke test cannot leak a live (and, for Oracle EE,
5-
# expensive) RDS instance + its backups. Epic #519.
3+
# instances (and their bound apps) for a given SERVICE_PLAN, run from a Concourse
4+
# `ensure:` step so a mid-run smoke-test failure cannot leak a live billed
5+
# instance + its backups.
66
#
7-
# The smoke runners use randomized instance names
8-
# (rds-smoke-tests[-db-update]-$SERVICE_PLAN-$TEST_ID); this sweeps every service
9-
# whose name contains the plan and force-deletes it plus any bound apps. Never
10-
# fails the build (teardown is best-effort): the smoke task's own on_failure/
11-
# on_success is what reports the actual result.
7+
# Engine-agnostic: works for any smoke-tested service (RDS / Elasticache /
8+
# Elasticsearch). Smoke runners name instances <prefix>-<SERVICE_PLAN>-<id>;
9+
# this deletes every service whose name contains the plan, which cascades to its
10+
# bindings. Never fails the build teardown is best-effort; the smoke task's own
11+
# on_success/on_failure reports the real result.
1212
set -uo pipefail
1313

14-
# ci-utils.sh is provided at runtime by the Concourse `aws-broker-app` input; it is
15-
# not resolvable at lint time. This is the same pattern the sibling ci/*.sh scripts
16-
# use.
14+
# ci-utils.sh is provided at runtime by the Concourse `aws-broker-app` input.
1715
# shellcheck disable=SC1091
1816
. aws-broker-app/ci/ci-utils.sh
1917

2018
login || true
2119

2220
: "${SERVICE_PLAN:?SERVICE_PLAN must be set}"
2321

24-
# Find service instances belonging to this plan's smoke tests and delete them.
25-
# `cf services` output is best-effort parsed; guard everything so we never exit
26-
# nonzero from the ensure step.
27-
mapfile -t services < <(cf services 2>/dev/null | awk 'NR>3 {print $1}' | grep -E "rds-smoke-tests.*${SERVICE_PLAN}" || true)
28-
29-
for svc in "${services[@]}"; do
30-
[ -n "$svc" ] || continue
31-
echo "cleanup: deleting leftover smoke service ${svc} (and its bindings)"
32-
# Unbind/delete any apps still bound, then delete the service.
33-
mapfile -t apps < <(cf curl "/v3/service_instances?names=${svc}" 2>/dev/null | jq -r '.resources[0].guid // empty' | while read -r guid; do
34-
[ -n "$guid" ] && cf curl "/v3/service_credential_bindings?service_instance_guids=${guid}" 2>/dev/null | jq -r '.resources[].relationships.app.data.guid // empty'
35-
done)
36-
for app_guid in "${apps[@]}"; do
37-
[ -n "$app_guid" ] || continue
38-
app_name=$(cf curl "/v3/apps/${app_guid}" 2>/dev/null | jq -r '.name // empty')
39-
[ -n "$app_name" ] && cf delete -f "$app_name" || true
22+
# `cf delete-service -f` cascades to bindings/keys, so no per-app teardown is
23+
# needed. Match smoke-test instances for this plan and delete each.
24+
cf services 2>/dev/null | awk 'NR>3 {print $1}' | grep -E "smoke-tests.*${SERVICE_PLAN}" |
25+
while read -r svc; do
26+
[ -n "$svc" ] || continue
27+
echo "cleanup: deleting leftover smoke service ${svc}"
28+
cf delete-service -f "$svc" || true
4029
done
41-
cf delete-service -f "$svc" || true
42-
done
4330

4431
echo "cleanup: done (best-effort) for plan ${SERVICE_PLAN}"
4532
exit 0

ci/smoke-tests/aws-rds/README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,4 @@ proves the service is usable.
1919
- MySQL: `github.com/go-sql-driver/mysql`
2020
- Oracle: `github.com/sijms/go-ora/v2` — a **pure-Go** driver (registers as
2121
`oracle`). It needs **no** Oracle Instant Client / native libraries, so the app
22-
builds and runs on a plain `go_buildpack` with `CGO_ENABLED=0`. (This replaced
23-
`gopkg.in/goracle.v2`, which was cgo and required `libclntsh` at runtime plus
24-
vendored Oracle binaries under `include/` and `git lfs` — that path had rotted
25-
and the smoke test could never connect. Epic #519.)
22+
builds and runs on a plain `go_buildpack` with `CGO_ENABLED=0`.

ci/smoke-tests/aws-rds/main.go

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ import (
1313
"github.com/go-sql-driver/mysql"
1414
_ "github.com/go-sql-driver/mysql"
1515
_ "github.com/lib/pq"
16-
// Pure-Go Oracle driver (registers as "oracle"). Replaces gopkg.in/goracle.v2,
17-
// which was cgo + required the Oracle Instant Client (libclntsh) at runtime —
18-
// unavailable in the go_buildpack rootfs and not vendored, so the Oracle smoke
19-
// test could never connect (epic #519). go-ora needs no native client.
16+
// Pure-Go Oracle driver (registers as "oracle"); needs no Oracle Instant Client.
2017
_ "github.com/sijms/go-ora/v2"
2118
)
2219

@@ -43,20 +40,11 @@ func main() {
4340
}
4441

4542
switch {
46-
// NOTE: use strings.Contains(dbType, keyword) — the args were previously
47-
// reversed (strings.Contains("oracle", dbType)), which asks whether the
48-
// literal "oracle" contains e.g. "oracle-ee" → false, so the DB test silently
49-
// never ran and the smoke test passed without touching the database. Oracle
50-
// plans pass DB_TYPE=oracle-ee (epic #519), which exposed the bug.
5143
case strings.Contains(dbType, "postgres"):
5244
openAndTest("postgres", svc.Credentials["uri"].(string))
5345
case strings.Contains(dbType, "mysql"):
5446
openAndTest("mysql", fmtMysql(svc))
5547
case strings.Contains(dbType, "oracle"):
56-
// go-ora registers as "oracle" and accepts the oracle:// URI the broker
57-
// emits directly (oracle://user:pass@host:port/service_name) — no Instant
58-
// Client, no cgo. Build the URL from the discrete binding fields so
59-
// special characters are properly escaped (epic #519).
6048
openAndTest("oracle", fmtOracle(svc))
6149
default:
6250
panic("unsupported DB_TYPE: " + dbType)
@@ -90,8 +78,7 @@ func fmtMysql(svc *cfenv.Service) string {
9078

9179
// fmtOracle builds the oracle:// URL that go-ora accepts, from the broker's
9280
// Oracle binding fields. Uses net/url so any special characters in the password
93-
// are percent-escaped (defensive: broker passwords are alnum today, but do not
94-
// rely on that here). Epic #519.
81+
// are percent-escaped.
9582
func fmtOracle(svc *cfenv.Service) string {
9683
user, ok := svc.CredentialString("username")
9784
if !ok {

services/rds/baselines.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,39 @@ type oracleLogExportsFile struct {
3535
Supported []string `yaml:"supported"`
3636
}
3737

38-
// oracleOptionsFile lists broker-managed option-group options.
38+
// oracleOptionsFile lists broker-managed option-group options + TLS metadata.
3939
type oracleOptionsFile struct {
40-
Options []oracleOption `yaml:"options"`
40+
SSLPort int32 `yaml:"ssl_port"`
41+
CACertFamily string `yaml:"ca_cert_family"`
42+
Options []oracleOption `yaml:"options"`
4143
}
4244

4345
type oracleOption struct {
44-
Name string `yaml:"name"`
45-
StigIntent string `yaml:"stig_intent"`
46-
Notes string `yaml:"notes"`
46+
Name string `yaml:"name"`
47+
Port int32 `yaml:"port"`
48+
StigIntent string `yaml:"stig_intent"`
49+
Notes string `yaml:"notes"`
50+
Settings []oracleOptionSetting `yaml:"settings"`
51+
}
52+
53+
type oracleOptionSetting struct {
54+
Name string `yaml:"name"`
55+
Value string `yaml:"value"`
56+
}
57+
58+
// cipherSuiteFor returns the SQLNET.CIPHER_SUITE value from the SSL option, if any.
59+
func (f *oracleOptionsFile) cipherSuiteFor(optionName string) (string, bool) {
60+
for _, o := range f.Options {
61+
if o.Name != optionName {
62+
continue
63+
}
64+
for _, s := range o.Settings {
65+
if s.Name == "SQLNET.CIPHER_SUITE" {
66+
return s.Value, true
67+
}
68+
}
69+
}
70+
return "", false
4771
}
4872

4973
// loadOracleParameters parses baselines/oracle19c/parameters.yml.
Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,58 @@
1-
# Oracle Database 19c — broker-managed option-group options (epic #519, WS6 #526).
1+
# Oracle Database 19c — broker-managed RDS OPTION-GROUP baseline (epic #519, WS6 #526).
22
#
3-
# RDS Oracle uses OPTION GROUPS (distinct from parameter groups) for certain
4-
# features. This file lists the options the broker WOULD attach to an Oracle
5-
# baseline option group.
6-
#
7-
# CURRENT STATE (#519 review H3): the create path does NOT yet provision an Oracle
8-
# option group — it only creates the parameter group. Because this list is empty,
9-
# there is nothing to attach today, so the gap is latent. Wiring option-group
10-
# provisioning at create (gated on a non-empty list) is tracked in #526; do NOT
11-
# assume adding an option here takes effect until that lands.
3+
# Unlike parameter groups (born-hardened, always applied), option-group options are
4+
# listed here and provisioned at CREATE time. This file now carries the SSL option
5+
# that enables FedRAMP-Moderate encryption-in-transit (SC-8 / SC-8(1) / SC-13) —
6+
# see #538 and ADR-0004.
127
#
138
# HONESTY / SCOPE:
14-
# - Every option below MUST be verified as available in AWS GovCloud for the
15-
# RDS Oracle 19c engine before a live deploy (WS15, gated). GovCloud option
16-
# availability differs from commercial regions.
17-
# - Do NOT enable attack-surface options (e.g. Oracle XML DB HTTP listener,
18-
# external procedures / extproc, Java VM, APEX) unless explicitly required and
19-
# justified (#535). None are enabled here.
20-
# - The authoritative STIG mapping lives in the overlay control->layer map
21-
# (aws_rds_option_group layer, #530).
22-
#
23-
# NOTE: several Oracle security controls that would be options on self-managed
24-
# Oracle (e.g. Native Network Encryption) are configured differently or are
25-
# AWS-managed on RDS; those are validated by the overlay as inherited/NA, not set
26-
# here. This list is intentionally conservative for the first iteration and may be
27-
# empty until the dev proof confirms which options RDS Oracle 19c in GovCloud both
28-
# supports and requires for the STIG posture.
9+
# - RDS Oracle serves TLS on a SEPARATE TCPS listener port (2484), provisioned via
10+
# the SSL option below — unlike postgres/mysql which negotiate TLS on their
11+
# standard port. This port divergence is an unavoidable AWS/Oracle architecture
12+
# fact, not a broker inconsistency.
13+
# - The broker attaches this option group + expresses the SSL intent (Port 2484,
14+
# the plan security group), but it CANNOT make the connection TLS-only: opening
15+
# 2484 ingress and DENYING plaintext 1521 is an EC2 security-group change owned
16+
# by the platform (cg-provision/Terraform), outside the broker's IAM. Leaving
17+
# 1521 open alongside 2484 is an SC-8 assessor finding, so the plan is NOT
18+
# customer-ready until the platform SG rule lands (#538 / platform issue).
19+
# - TLS version ceiling: RDS Oracle SSL supports TLS 1.2 only (no 1.3). 1.2 is
20+
# acceptable for FedRAMP Moderate; documented for the SSP.
21+
# - Cipher/CA compatibility: the default RDS CA is RSA (rds-ca-rsa2048-g1), which
22+
# is compatible with the ECDHE_RSA cipher below. ECDSA-only ciphers would
23+
# require the ECC CA and are rejected fail-closed before the AWS call.
24+
# - Every value MUST be verified on the live RDS Oracle SE2 19c engine in GovCloud
25+
# before customer exposure (WS15, gated).
26+
# - NO attack-surface options (XML DB HTTP, external procedures, Java VM, APEX).
27+
28+
# The SSL/TCPS listener port RDS Oracle exposes for TLS connections.
29+
ssl_port: 2484
30+
31+
# The RDS CA family the instance uses. Governs cipher compatibility: an "rsa" CA is
32+
# compatible with TLS_ECDHE_RSA_* suites; an ECDSA-only suite would need "ecc".
33+
ca_cert_family: rsa
2934

30-
options: []
31-
# Example shape (verify availability + necessity before enabling):
32-
# - name: NATIVE_NETWORK_ENCRYPTION
33-
# stig_intent: "Encrypt Oracle client/server traffic (SRG-APP-000441/442)."
34-
# notes: "Confirm GovCloud availability + settings; may overlap RDS TLS."
35+
options:
36+
# SSL option — certificate-based TLS on a TCPS listener (SC-8 / SC-13).
37+
# Provisioned + attached at create; the binding publishes port 2484 + a TCPS URL.
38+
- name: SSL
39+
port: 2484
40+
stig_intent: >-
41+
Encryption-in-transit via certificate-based TLS (SC-8/SC-8(1)); FIPS-validated
42+
crypto (SC-13). DISA Oracle 19c STIG V-270579 (SC-8(2)) + V-270571 (SC-13).
43+
notes: >-
44+
TLS 1.2 (RDS Oracle ceiling); FedRAMP+FIPS AEAD cipher; FIPS mode on. Requires
45+
the plan security group to allow 2484 and (for TLS-only) deny 1521 — platform
46+
dependency (#538).
47+
settings:
48+
# TLS 1.2 only. RDS Oracle does not support 1.3 via the SSL option.
49+
- name: SQLNET.SSL_VERSION
50+
value: "1.2"
51+
# FedRAMP-compliant + FIPS + RSA-CA-compatible AEAD suite (AES-256 preferred),
52+
# AES-128 GCM as a negotiable fallback (also FedRAMP + FIPS + RSA-compatible).
53+
- name: SQLNET.CIPHER_SUITE
54+
value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
55+
# Force the RDS Oracle crypto module into FIPS 140 mode (writes fips.ora
56+
# SSLFIPS_140=TRUE). STIG V-270571 checks for this.
57+
- name: FIPS.SSLFIPS_140
58+
value: "TRUE"

services/rds/baselines_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package rds
22

33
import (
44
"slices"
5+
"strings"
56
"testing"
7+
8+
"github.com/aws/aws-sdk-go-v2/aws"
69
)
710

811
// baselines_test.go — the embedded Oracle 19c baseline files must parse and carry
@@ -109,6 +112,84 @@ func TestOracleOptionsBaselineHasNoAttackSurfaceOptions(t *testing.T) {
109112
}
110113
}
111114

115+
func TestOracleSSLOptionBaseline(t *testing.T) {
116+
f, err := loadOracleOptions()
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
if f.SSLPort != 2484 {
121+
t.Errorf("ssl_port = %d, want 2484", f.SSLPort)
122+
}
123+
if f.CACertFamily != "rsa" {
124+
t.Errorf("ca_cert_family = %q, want rsa", f.CACertFamily)
125+
}
126+
// The SSL option must carry TLS 1.2, a FedRAMP+FIPS+RSA-compatible cipher, and FIPS on.
127+
var ssl *oracleOption
128+
for i := range f.Options {
129+
if f.Options[i].Name == "SSL" {
130+
ssl = &f.Options[i]
131+
}
132+
}
133+
if ssl == nil {
134+
t.Fatal("SSL option missing from oracle options baseline")
135+
}
136+
if ssl.Port != 2484 {
137+
t.Errorf("SSL option port = %d, want 2484", ssl.Port)
138+
}
139+
want := map[string]string{
140+
"SQLNET.SSL_VERSION": "1.2",
141+
"SQLNET.CIPHER_SUITE": "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
142+
"FIPS.SSLFIPS_140": "TRUE",
143+
}
144+
got := map[string]string{}
145+
for _, s := range ssl.Settings {
146+
got[s.Name] = s.Value
147+
}
148+
for k, v := range want {
149+
if got[k] != v {
150+
t.Errorf("SSL setting %s = %q, want %q", k, got[k], v)
151+
}
152+
}
153+
// The chosen cipher must be an ECDHE_RSA suite (RSA-CA compatible), not ECDSA-only.
154+
if strings.Contains(got["SQLNET.CIPHER_SUITE"], "_ECDSA_") {
155+
t.Errorf("SSL cipher %q is ECDSA-only; incompatible with the RSA CA", got["SQLNET.CIPHER_SUITE"])
156+
}
157+
}
158+
159+
func TestOracleBaselineOptionsBuild(t *testing.T) {
160+
b, ok := baselineFor("oracle-se2")
161+
if !ok {
162+
t.Fatal("oracle-se2 baseline missing")
163+
}
164+
i := &RDSInstance{DbType: "oracle-se2"}
165+
i.SecGroup = "sg-abc123"
166+
opts, err := b.BaselineOptions(i)
167+
if err != nil {
168+
t.Fatalf("BaselineOptions error: %v", err)
169+
}
170+
if len(opts) != 1 {
171+
t.Fatalf("expected 1 baseline option (SSL), got %d", len(opts))
172+
}
173+
ssl := opts[0]
174+
if aws.ToString(ssl.OptionName) != "SSL" {
175+
t.Errorf("option name = %q, want SSL", aws.ToString(ssl.OptionName))
176+
}
177+
if aws.ToInt32(ssl.Port) != 2484 {
178+
t.Errorf("option port = %d, want 2484", aws.ToInt32(ssl.Port))
179+
}
180+
if len(ssl.VpcSecurityGroupMemberships) != 1 || ssl.VpcSecurityGroupMemberships[0] != "sg-abc123" {
181+
t.Errorf("SSL option must reference the instance SG, got %v", ssl.VpcSecurityGroupMemberships)
182+
}
183+
// pg/mysql have no baseline options.
184+
for _, eng := range []string{"postgres", "mysql"} {
185+
pgb, _ := baselineFor(eng)
186+
o, err := pgb.BaselineOptions(&RDSInstance{DbType: eng})
187+
if err != nil || len(o) != 0 {
188+
t.Errorf("%s BaselineOptions = (%v,%v), want (nil,nil)", eng, o, err)
189+
}
190+
}
191+
}
192+
112193
func TestOracleBaselineWiredIntoEngine(t *testing.T) {
113194
b, ok := baselineFor("oracle-se2")
114195
if !ok {

services/rds/create_worker.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ func (w *CreateWorker) prepareCreateDbInput(
127127
params.DBParameterGroupName = aws.String(i.ParameterGroupName)
128128
}
129129

130+
// Provision + attach the engine's baseline option group at create time
131+
// (Oracle SE2: the SSL/TCPS option for FedRAMP-Moderate TLS — #519/#538).
132+
// No-op for engines with an empty baseline (postgres/mysql). Fails closed.
133+
if err = w.optionGroupClient.ProvisionBaselineOptionGroup(i, rdsTags); err != nil {
134+
return nil, err
135+
}
136+
if i.OptionGroupName != "" {
137+
params.OptionGroupName = aws.String(i.OptionGroupName)
138+
}
139+
130140
return params, nil
131141
}
132142

0 commit comments

Comments
 (0)