Skip to content

Commit 13587b4

Browse files
christiangdaclaude
andcommitted
fix(template): scope Lambda role S3+KMS perms to the state object
Tighten the Lambda execution role in template.yaml so it can only touch the single state object via the single intended path. No behavior change for normal operation; the role is now strictly scoped. S3: - s3:GetObject*/PutObject* scoped to the exact <bucket>/<BucketKey> ARN instead of <bucket>/*. - s3:ListBucket stays on the bucket ARN (required by S3) but is gated by an s3:prefix condition matching BucketKey. KMS (both KMSGetDataPolicy and KMSDecryptPolicy): - kms:ViaService = s3.<region>.amazonaws.com — the CMK can only be used through S3-forwarded requests, not via direct kms:Decrypt/Encrypt. - kms:EncryptionContext:aws:s3:arn pinned to the state object ARN — S3 always populates this context for SSE-KMS objects, so legitimate reads/writes continue to work; any other object path is rejected. Refs: #521 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bf36ac5 commit 13587b4

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

docs/Whats-New.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ This document tracks notable changes, new features, and bug fixes across release
44

55
## Unreleased
66

7+
### IAM least-privilege hardening for the state-file Lambda role
8+
9+
Tightens the Lambda execution role in `template.yaml` so it can only touch the single state object via the single intended path. No behavior change for normal operation; the role is now strictly scoped.
10+
11+
**S3 statement (split in two):**
12+
13+
* `S3ObjectPolicy``s3:GetObject*` / `s3:PutObject*` are now scoped to the exact state object (`arn:${Partition}:s3:::<bucket>/<BucketKey>`) instead of `<bucket>/*`.
14+
* `S3ListBucketPolicy``s3:ListBucket` stays on the bucket ARN (required by S3) but is now gated by a `s3:prefix` condition matching `BucketKey`.
15+
16+
**KMS statements (`KMSGetDataPolicy` and `KMSDecryptPolicy`):**
17+
18+
Both now carry the AWS-recommended SSE-KMS scoping conditions:
19+
20+
* `kms:ViaService = s3.<region>.amazonaws.com` — the CMK can only be used through requests S3 forwards on the role's behalf, not via direct `kms:Decrypt` / `kms:Encrypt` calls.
21+
* `kms:EncryptionContext:aws:s3:arn = arn:${Partition}:s3:::<bucket>/<BucketKey>` — the KMS grant only applies when S3 passes that exact object ARN as encryption context. S3 always populates this context for SSE-KMS objects, so legitimate reads/writes of the state file continue to work; any other object path is rejected by KMS.
22+
23+
These are belt-and-braces additions on top of the existing bucket policy, public-access block, `aws:SecureTransport` deny, and SSE-KMS-with-bucket-key configuration.
24+
25+
References: [AWS docs — kms:ViaService](https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-via-service), [AWS docs — SSE-KMS encryption context](https://docs.aws.amazon.com/AmazonS3/latest/userguide/specifying-kms-encryption.html).
26+
727
### OpenSSF Scorecard Hardening (Phase 4) — Fuzzing
828

929
Closes the **Fuzzing** Scorecard check by adding native Go fuzz targets and a CI workflow that exercises them.

template.yaml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,24 +342,36 @@ Resources:
342342
- !Ref AWSGWSUserEmailSecret
343343
- !Ref AWSSCIMEndpointSecret
344344
- !Ref AWSSCIMAccessTokenSecret
345-
- Sid: S3Policy
345+
- Sid: S3ObjectPolicy
346346
Effect: Allow
347347
Action:
348348
- s3:GetObject
349349
- s3:GetObjectAcl
350350
- s3:GetObjectVersion
351351
- s3:PutObject
352352
- s3:PutObjectAcl
353+
Resource:
354+
- !Sub "arn:${AWS::Partition}:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}/${BucketKey}"
355+
- Sid: S3ListBucketPolicy
356+
Effect: Allow
357+
Action:
353358
- s3:ListBucket
354359
Resource:
355-
- !Sub "arn:aws:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}"
356-
- !Sub "arn:aws:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}/*"
360+
- !Sub "arn:${AWS::Partition}:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}"
361+
Condition:
362+
StringLike:
363+
s3:prefix:
364+
- !Ref BucketKey
357365
- Sid: KMSGetDataPolicy
358366
Effect: Allow
359367
Action:
360368
- kms:GenerateDataKeyPair
361369
Resource:
362370
- !GetAtt KMSKey.Arn
371+
Condition:
372+
StringEquals:
373+
kms:ViaService: !Sub "s3.${AWS::Region}.amazonaws.com"
374+
kms:EncryptionContext:aws:s3:arn: !Sub "arn:${AWS::Partition}:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}/${BucketKey}"
363375
- Sid: KMSDecryptPolicy
364376
Effect: Allow
365377
Action:
@@ -368,6 +380,10 @@ Resources:
368380
- kms:GenerateDataKey
369381
Resource:
370382
- !GetAtt KMSKey.Arn
383+
Condition:
384+
StringEquals:
385+
kms:ViaService: !Sub "s3.${AWS::Region}.amazonaws.com"
386+
kms:EncryptionContext:aws:s3:arn: !Sub "arn:${AWS::Partition}:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}/${BucketKey}"
371387

372388
AWSGWSServiceAccountFileSecret:
373389
Type: AWS::SecretsManager::Secret

0 commit comments

Comments
 (0)