Skip to content

Commit 58aa6ee

Browse files
authored
fix(s3vectors): read EncryptionConfiguration with the CFn SseType / KmsKeyArn spellings (#1397)
1 parent aa6489f commit 58aa6ee

9 files changed

Lines changed: 179 additions & 36 deletions

File tree

docs/_generated/integ-coverage.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3555,7 +3555,8 @@
35553555
"infra-security",
35563556
"kms-encryption",
35573557
"loggroup-kms-associate",
3558-
"propagation-races-2"
3558+
"propagation-races-2",
3559+
"s3-vectors"
35593560
],
35603561
"signals": {
35613562
"composite-stack": [
@@ -3582,6 +3583,9 @@
35823583
],
35833584
"propagation-races-2": [
35843585
"l1"
3586+
],
3587+
"s3-vectors": [
3588+
"l2"
35853589
]
35863590
}
35873591
},

docs/_generated/integ-last-run.tsv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ s3-lifecycle 2026-07-21T14:28:43Z PASS 58 verify.sh rc ok, orph clean
232232
s3-object-lock 2026-07-21T14:42:20Z PASS 53 verify.sh rc ok, orph clean
233233
s3-replication-and-filter 2026-07-21T14:43:45Z PASS 63 verify.sh rc ok, orph clean
234234
s3-tables 2026-07-27T16:53:47Z PASS 45 verify.sh issue #1270/#1272 post-review re-run; 5 del 0 err, 0 orphans
235-
s3-vectors 2026-07-26T18:49:34Z PASS 43 verify.sh 0727b sweep-b6 staleness re-run (rc=0); account clean
235+
s3-vectors 2026-08-09T03:50:21Z PASS 230 verify.sh #1385 CMK encryption asserted; destroy 0 errors, 0 orphans
236236
scheduled-task 2026-07-26T18:04:53Z PASS 44 standard 0727b sweep-b1 staleness re-run (rc=0); account clean
237237
scheduler-custom-group 2026-07-26T19:25:15Z PASS 103 verify.sh 0727b sweep-b10 staleness re-run (rc=0); account clean
238238
schema-v5-to-v6-migration 2026-07-24T10:27:22Z PASS 61 verify.sh 0724 P0 sweep: v5->v8 transparent upgrade verified; 1 del 0 err

docs/integ-coverage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ Registered without an integ fixture, with an explicit `// allow-no-integ: <ratio
124124
| `AWS::IAM::User` | [`iam-access-key`](../tests/integration/iam-access-key/) (l2)<br>[`infra-security`](../tests/integration/infra-security/) (l2) |
125125
| `AWS::IAM::UserToGroupAddition` | [`infra-security`](../tests/integration/infra-security/) (literal) |
126126
| `AWS::KMS::Alias` | [`composite-stack`](../tests/integration/composite-stack/) (literal)<br>[`infra-security`](../tests/integration/infra-security/) (literal)<br>[`kms-encryption`](../tests/integration/kms-encryption/) (literal) |
127-
| `AWS::KMS::Key` | [`composite-stack`](../tests/integration/composite-stack/) (literal)<br>[`drift-revert`](../tests/integration/drift-revert/) (l2)<br>[`dynamodb-streams`](../tests/integration/dynamodb-streams/) (l2)<br>[`ecr-scanning`](../tests/integration/ecr-scanning/) (l2)<br>[`infra-security`](../tests/integration/infra-security/) (l2)<br>[`kms-encryption`](../tests/integration/kms-encryption/) (l2,literal)<br>[`loggroup-kms-associate`](../tests/integration/loggroup-kms-associate/) (l2)<br>[`propagation-races-2`](../tests/integration/propagation-races-2/) (l1) |
127+
| `AWS::KMS::Key` | [`composite-stack`](../tests/integration/composite-stack/) (literal)<br>[`drift-revert`](../tests/integration/drift-revert/) (l2)<br>[`dynamodb-streams`](../tests/integration/dynamodb-streams/) (l2)<br>[`ecr-scanning`](../tests/integration/ecr-scanning/) (l2)<br>[`infra-security`](../tests/integration/infra-security/) (l2)<br>[`kms-encryption`](../tests/integration/kms-encryption/) (l2,literal)<br>[`loggroup-kms-associate`](../tests/integration/loggroup-kms-associate/) (l2)<br>[`propagation-races-2`](../tests/integration/propagation-races-2/) (l1)<br>[`s3-vectors`](../tests/integration/s3-vectors/) (l2) |
128128
| `AWS::Kinesis::Stream` | [`cache-streaming`](../tests/integration/cache-streaming/) (l2)<br>[`dynamodb-ondemand`](../tests/integration/dynamodb-ondemand/) (l2)<br>[`kinesis-esm-filter`](../tests/integration/kinesis-esm-filter/) (l2)<br>[`kinesis-stream-mode-switch`](../tests/integration/kinesis-stream-mode-switch/) (l2)<br>[`log-pipeline`](../tests/integration/log-pipeline/) (l2)<br>[`replacement-immutable-name`](../tests/integration/replacement-immutable-name/) (l2,literal)<br>[`sdk-ccapi-crossref`](../tests/integration/sdk-ccapi-crossref/) (l1,literal) |
129129
| `AWS::KinesisFirehose::DeliveryStream` | [`log-pipeline`](../tests/integration/log-pipeline/) (l1,literal) |
130130
| `AWS::Lambda::EventInvokeConfig` | [`lambda-destinations`](../tests/integration/lambda-destinations/) (literal)<br>[`lambda-event-invoke-config-update`](../tests/integration/lambda-event-invoke-config-update/) (literal) |

src/provisioning/providers/s3-vectors-provider.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@ export class S3VectorsProvider implements ResourceProvider {
254254
);
255255
}
256256

257+
// CFn spells the two sub-keys `SseType` / `KmsKeyArn` (verified against the
258+
// live `AWS::S3Vectors::VectorBucket` registry schema and CDK's
259+
// `CfnVectorBucket` renderer) — NOT `SSEType` / `KMSKeyArn`. Reading the
260+
// all-caps spellings made both lookups permanently `undefined`, so
261+
// `CreateVectorBucket` silently shipped the account-default encryption
262+
// instead of the requested KMS key (issue #1385).
257263
const encryptionConfiguration = properties['EncryptionConfiguration'] as
258264
| Record<string, unknown>
259265
| undefined;
@@ -271,8 +277,8 @@ export class S3VectorsProvider implements ResourceProvider {
271277
vectorBucketName,
272278
encryptionConfiguration: encryptionConfiguration
273279
? {
274-
sseType: encryptionConfiguration['SSEType'] as SseType | undefined,
275-
kmsKeyArn: encryptionConfiguration['KMSKeyArn'] as string | undefined,
280+
sseType: encryptionConfiguration['SseType'] as SseType | undefined,
281+
kmsKeyArn: encryptionConfiguration['KmsKeyArn'] as string | undefined,
276282
}
277283
: undefined,
278284
...(tags && Object.keys(tags).length > 0 ? { tags } : {}),
@@ -383,8 +389,8 @@ export class S3VectorsProvider implements ResourceProvider {
383389
*
384390
* Issues `GetVectorBucket` for the bucket name (the physical id) and
385391
* surfaces `VectorBucketName` and `EncryptionConfiguration` (re-shaping
386-
* the camelCase SDK response back to PascalCase CFn property names —
387-
* `sseType` → `SSEType`, `kmsKeyArn` → `KMSKeyArn`).
392+
* the camelCase SDK response back to the CFn property names —
393+
* `sseType` → `SseType`, `kmsKeyArn` → `KmsKeyArn`).
388394
*
389395
* Returns `undefined` when the bucket is gone (`NotFoundException` /
390396
* `NoSuchVectorBucket`).
@@ -415,17 +421,17 @@ export class S3VectorsProvider implements ResourceProvider {
415421
const enc: Record<string, unknown> = {};
416422
const sseType = bucket.encryptionConfiguration.sseType;
417423
if (sseType !== undefined) {
418-
enc['SSEType'] = sseType;
424+
enc['SseType'] = sseType;
419425
}
420-
// Class 1 guard (docs/provider-development.md § 3b): KMSKeyArn is
421-
// KMS-only — only valid when SSEType === 'aws:kms'. AWS will not
426+
// Class 1 guard (docs/provider-development.md § 3b): KmsKeyArn is
427+
// KMS-only — only valid when SseType === 'aws:kms'. AWS will not
422428
// return kmsKeyArn for AES256-encrypted buckets, but defend
423429
// against a future SDK that surfaces an account-default KMS key
424430
// ARN on AES256 responses (which would round-trip back via
425431
// `cdkd drift --revert` and AWS would reject as
426-
// "KMSKeyArn is only valid when SSEType is aws:kms").
432+
// "KmsKeyArn is only valid when SseType is aws:kms").
427433
if (sseType === 'aws:kms' && bucket.encryptionConfiguration.kmsKeyArn !== undefined) {
428-
enc['KMSKeyArn'] = bucket.encryptionConfiguration.kmsKeyArn;
434+
enc['KmsKeyArn'] = bucket.encryptionConfiguration.kmsKeyArn;
429435
}
430436
if (Object.keys(enc).length > 0) result['EncryptionConfiguration'] = enc;
431437
}

tests/integration/s3-vectors/lib/s3-vectors-stack.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as cdk from 'aws-cdk-lib';
22
import { Construct } from 'constructs';
3+
import * as kms from 'aws-cdk-lib/aws-kms';
34
import * as s3vectors from 'aws-cdk-lib/aws-s3vectors';
45

56
export class S3VectorsStack extends cdk.Stack {
@@ -28,14 +29,42 @@ export class S3VectorsStack extends cdk.Stack {
2829
{ key: 'team', value: 'platform' },
2930
];
3031

32+
// Issue #1385: the provider read the encryption sub-keys with the WRONG
33+
// CFn spellings (`SSEType` / `KMSKeyArn`), so both lookups were always
34+
// `undefined` and CreateVectorBucket silently shipped the account-default
35+
// encryption instead of this customer-managed key. `EncryptionConfiguration`
36+
// is create-only, so it is held CONSTANT across the CDKD_TEST_UPDATE branch
37+
// above (a change would force a replacement, not the Tags in-place update
38+
// that phase exercises).
39+
// Default key policy (account root) only — deliberately NO
40+
// `s3vectors.amazonaws.com` grant. This fixture proves the CONFIGURATION
41+
// reaches AWS (CreateVectorBucket / GetVectorBucket both succeed and echo
42+
// the key back); it writes no vectors, so it would not catch an
43+
// AccessDenied on the actual encrypt path. That is out of scope for #1385,
44+
// which is purely about the CFn key spelling never reaching the SDK call.
45+
const key = new kms.Key(this, 'VectorBucketKey', {
46+
description: 'cdkd s3-vectors integ CMK (issue #1385)',
47+
enableKeyRotation: false,
48+
removalPolicy: cdk.RemovalPolicy.DESTROY,
49+
});
50+
3151
const vectorBucket = new s3vectors.CfnVectorBucket(this, 'VectorBucket', {
3252
vectorBucketName: `${this.stackName}-vector-bucket`.toLowerCase(),
53+
encryptionConfiguration: {
54+
sseType: 'aws:kms',
55+
kmsKeyArn: key.keyArn,
56+
},
3357
tags,
3458
});
3559

3660
new cdk.CfnOutput(this, 'VectorBucketName', {
3761
value: vectorBucket.ref,
3862
description: 'S3 Vector Bucket name',
3963
});
64+
65+
new cdk.CfnOutput(this, 'VectorBucketKeyArn', {
66+
value: key.keyArn,
67+
description: 'CMK the vector bucket must be encrypted with',
68+
});
4069
}
4170
}

tests/integration/s3-vectors/verify.sh

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,43 @@ if [ -z "${BUCKET_NAME}" ]; then
107107
exit 1
108108
fi
109109

110-
# Resolve the bucket ARN — ListTagsForResource is keyed by ARN, not name.
111-
BUCKET_ARN=$(aws s3vectors get-vector-bucket \
110+
EXPECTED_KEY_ARN=$(echo "${STATE}" | jq -r '.outputs.VectorBucketKeyArn // empty')
111+
if [ -z "${EXPECTED_KEY_ARN}" ]; then
112+
echo "FAIL: state.outputs.VectorBucketKeyArn missing after deploy" >&2
113+
echo "${STATE}" | jq '.outputs'
114+
exit 1
115+
fi
116+
117+
# One GetVectorBucket read serves both the ARN resolution (ListTagsForResource
118+
# is keyed by ARN, not name) and the #1385 encryption assertion below.
119+
BUCKET_JSON=$(aws s3vectors get-vector-bucket \
112120
--vector-bucket-name "${BUCKET_NAME}" --region "${REGION}" \
113-
--query 'vectorBucket.vectorBucketArn' --output text 2>/dev/null)
114-
if [ -z "${BUCKET_ARN}" ] || [ "${BUCKET_ARN}" = "None" ]; then
121+
--query 'vectorBucket' --output json)
122+
BUCKET_ARN=$(echo "${BUCKET_JSON}" | jq -r '.vectorBucketArn // empty')
123+
if [ -z "${BUCKET_ARN}" ]; then
115124
echo "FAIL: GetVectorBucket(${BUCKET_NAME}) returned no ARN" >&2
116125
exit 1
117126
fi
118127

128+
# --- Assertion: EncryptionConfiguration reached AWS (issue #1385) ----
129+
# The provider used to read the non-existent CFn keys `SSEType` / `KMSKeyArn`,
130+
# so both were always undefined and the bucket came up with the account-default
131+
# AES256 encryption while cdkd reported success. Assert the CMK actually stuck.
132+
SSE_TYPE=$(echo "${BUCKET_JSON}" | jq -r '.encryptionConfiguration.sseType // empty')
133+
KMS_KEY_ARN=$(echo "${BUCKET_JSON}" | jq -r '.encryptionConfiguration.kmsKeyArn // empty')
134+
135+
if [ "${SSE_TYPE}" != "aws:kms" ]; then
136+
echo "FAIL: encryptionConfiguration.sseType is '${SSE_TYPE}', expected 'aws:kms' (issue #1385 NOT closed)" >&2
137+
echo " raw bucket: ${BUCKET_JSON}" >&2
138+
exit 1
139+
fi
140+
if [ "${KMS_KEY_ARN}" != "${EXPECTED_KEY_ARN}" ]; then
141+
echo "FAIL: encryptionConfiguration.kmsKeyArn is '${KMS_KEY_ARN}', expected '${EXPECTED_KEY_ARN}' (issue #1385 NOT closed)" >&2
142+
echo " raw bucket: ${BUCKET_JSON}" >&2
143+
exit 1
144+
fi
145+
echo " OK: VectorBucket encrypted with the requested CMK (SseType/KmsKeyArn reach AWS)"
146+
119147
# --- Assertion: Tags reached AWS via CreateVectorBucket.tags ---------
120148
TAGS_JSON=$(aws s3vectors list-tags-for-resource \
121149
--resource-arn "${BUCKET_ARN}" --region "${REGION}" \
@@ -183,5 +211,20 @@ echo " OK: VectorBucket is gone"
183211
assert_gone "state file s3://${STATE_BUCKET}/${STATE_KEY} still exists after destroy" aws s3api head-object --bucket "${STATE_BUCKET}" --key "${STATE_KEY}"
184212
echo " OK: state file is gone"
185213

214+
# The CMK is NOT an orphan: AWS never hard-deletes a KMS key, so a destroyed
215+
# `RemovalPolicy.DESTROY` key lands in PendingDeletion (7-day default window)
216+
# rather than disappearing. Assert that state explicitly so a key left ENABLED
217+
# (a real leak — the destroy never scheduled it) still fails the run.
218+
KEY_STATE=$(aws kms describe-key --key-id "${EXPECTED_KEY_ARN}" --region "${REGION}" \
219+
--query 'KeyMetadata.KeyState' --output text) || {
220+
echo "FAIL: describe-key on ${EXPECTED_KEY_ARN} failed — cannot determine whether the CMK was scheduled for deletion" >&2
221+
exit 1
222+
}
223+
if [ "${KEY_STATE}" != "PendingDeletion" ]; then
224+
echo "FAIL: CMK ${EXPECTED_KEY_ARN} is '${KEY_STATE}' after destroy, expected PendingDeletion" >&2
225+
exit 1
226+
fi
227+
echo " OK: CMK scheduled for deletion (PendingDeletion)"
228+
186229
echo ""
187-
echo "==> s3-vectors test passed (Tags backfill closed + clean destroy)"
230+
echo "==> s3-vectors test passed (Tags backfill + #1385 CMK encryption + clean destroy)"

tests/unit/provisioning/providers/s3-vectors-provider.test.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,67 @@ describe('S3VectorsProvider', () => {
8282
});
8383
});
8484

85+
it('maps the CFn EncryptionConfiguration spellings (SseType / KmsKeyArn) onto the SDK members (issue #1385)', async () => {
86+
// Regression guard: the provider used to read `SSEType` / `KMSKeyArn`,
87+
// which the CFn registry schema (and CDK's CfnVectorBucket renderer)
88+
// never emit — both lookups were always `undefined`, so a KMS-encrypted
89+
// vector bucket silently came up with the account-default encryption.
90+
mockSend.mockImplementation((cmd: unknown) => {
91+
if (cmd instanceof CreateVectorBucketCommand) {
92+
return Promise.resolve({
93+
vectorBucketArn: 'arn:aws:s3vectors:us-east-1:0:vector-bucket/kms-vector-bucket',
94+
});
95+
}
96+
return Promise.resolve({});
97+
});
98+
99+
await provider.create('MyVectorBucket', 'AWS::S3Vectors::VectorBucket', {
100+
VectorBucketName: 'kms-vector-bucket',
101+
EncryptionConfiguration: {
102+
SseType: 'aws:kms',
103+
KmsKeyArn: 'arn:aws:kms:us-east-1:123456789012:key/abc',
104+
},
105+
});
106+
107+
const createCall = mockSend.mock.calls.find(
108+
(call: unknown[]) => call[0] instanceof CreateVectorBucketCommand
109+
);
110+
expect(createCall![0].input).toStrictEqual({
111+
vectorBucketName: 'kms-vector-bucket',
112+
encryptionConfiguration: {
113+
sseType: 'aws:kms',
114+
kmsKeyArn: 'arn:aws:kms:us-east-1:123456789012:key/abc',
115+
},
116+
});
117+
});
118+
119+
it('maps AES256 EncryptionConfiguration with no KmsKeyArn', async () => {
120+
mockSend.mockImplementation((cmd: unknown) => {
121+
if (cmd instanceof CreateVectorBucketCommand) {
122+
return Promise.resolve({
123+
vectorBucketArn: 'arn:aws:s3vectors:us-east-1:0:vector-bucket/aes-vector-bucket',
124+
});
125+
}
126+
return Promise.resolve({});
127+
});
128+
129+
await provider.create('MyVectorBucket', 'AWS::S3Vectors::VectorBucket', {
130+
VectorBucketName: 'aes-vector-bucket',
131+
EncryptionConfiguration: { SseType: 'AES256' },
132+
});
133+
134+
const createCall = mockSend.mock.calls.find(
135+
(call: unknown[]) => call[0] instanceof CreateVectorBucketCommand
136+
);
137+
// toStrictEqual, not toEqual: toEqual ignores undefined-valued keys, so
138+
// `kmsKeyArn: undefined` would READ as a pinned absence while checking
139+
// nothing. AES256 must never carry a key ARN (AWS rejects it).
140+
expect(createCall![0].input).toStrictEqual({
141+
vectorBucketName: 'aes-vector-bucket',
142+
encryptionConfiguration: { sseType: 'AES256', kmsKeyArn: undefined },
143+
});
144+
});
145+
85146
it('forwards Tags into CreateVectorBucket as the SDK Record<string,string> shape', async () => {
86147
mockSend.mockImplementation((cmd: unknown) => {
87148
if (cmd instanceof CreateVectorBucketCommand) {
@@ -340,8 +401,8 @@ describe('S3VectorsProvider', () => {
340401
'MyVectorBucket',
341402
'my-vector-bucket',
342403
'AWS::S3Vectors::VectorBucket',
343-
{ EncryptionConfiguration: { SSEType: 'aws:kms' } },
344-
{ EncryptionConfiguration: { SSEType: 'AES256' } }
404+
{ EncryptionConfiguration: { SseType: 'aws:kms' } },
405+
{ EncryptionConfiguration: { SseType: 'AES256' } }
345406
)
346407
).rejects.toThrow(ResourceUpdateNotSupportedError);
347408
// Must fail BEFORE any AWS call.

tests/unit/provisioning/s3-vectors-provider-readcurrentstate.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ describe('S3VectorsProvider.readCurrentState', () => {
7171
expect(result).toEqual({
7272
VectorBucketName: 'my-vec-bucket',
7373
EncryptionConfiguration: {
74-
SSEType: 'aws:kms',
75-
KMSKeyArn: 'arn:aws:kms:us-east-1:123:key/abc',
74+
SseType: 'aws:kms',
75+
KmsKeyArn: 'arn:aws:kms:us-east-1:123:key/abc',
7676
},
7777
Tags: [],
7878
});

0 commit comments

Comments
 (0)