Severity
Reported: Low. Adjusted: Low (confirmed). A clear deviation from a hardening standard the repo itself follows for its CloudTrail sink, but defense-in-depth: the attack requires a principal that already holds s3:GetObject on the bucket. Correctly self-rated.
OWASP / standard
A02:2021 (Cryptographic Failures); Checkov CKV_AWS_145 (S3 encrypted with KMS CMK).
Attack scenario
The Pulumi state buckets hold serialized stack state (resource outputs, ARNs, non-secret config), the Config bucket holds full resource-configuration snapshots, and the central logging bucket holds server-access logs. All three are encrypted with SSE-S3 (AES256), which is transparent to anyone holding s3:GetObject: there is no second kms:Decrypt authorization layer and no per-decryption CloudTrail data event tied to a dedicated key. A principal that gains only s3:GetObject reads plaintext directly, whereas a customer-managed KMS key would require a separate grant and would surface each decrypt in CloudTrail. The policy pack's encryption check treats this as compliant because it only verifies that some sseAlgorithm string is present, so it cannot flag SSE-S3 (or a downgrade) on a sensitive store.
Evidence
pulumi/infra/pulumi_state.py:228-240 — _state_bucket_encryption_rules uses sse_algorithm="AES256" (SSE-S3) for primary and replica state buckets
pulumi/infra/security_account_controls.py:240-252 — Config delivery bucket sse_algorithm="AES256"
pulumi/infra/logging_bucket.py:307-320 — central logging bucket sse_algorithm="AES256" (contrast operations_monitoring.py:499-502, where the CloudTrail bucket correctly uses aws:kms with a CMK)
policy/guardrails.py:286-294 — _has_default_s3_encryption_rule returns True as soon as any applyServerSideEncryptionByDefault.sseAlgorithm string exists, with no CMK/kmsMasterKeyId requirement (grep of policy/ for kmsMasterKeyId/aws:kms/CKV_AWS_145 returns zero matches)
Impact
A single-permission (s3:GetObject) compromise on a state/config/log bucket yields plaintext with no second authorization layer and no dedicated-key decrypt audit trail — a missing defense-in-depth layer on the repo's most sensitive stores, inconsistent with the CMK pattern already applied to CloudTrail.
Remediation
- Encrypt the Pulumi-state, Config, and central-logging buckets with dedicated customer-managed KMS keys (
sse_algorithm="aws:kms" + kms_master_key_id, with enable_key_rotation), matching the CloudTrail bucket pattern.
- Tighten the CrossGuard encryption rule so audit/state buckets require
aws:kms with a CMK rather than accepting SSE-S3.
Acceptance criteria (5/5)
Verification notes
All four citations verified across passes: the three buckets use AES256 while CloudTrail uses aws:kms + CMK (proving the pattern exists and was applied to only one of four sensitive stores). _has_default_s3_encryption_rule returns True on any sseAlgorithm string with no CMK check. Public-access blocks and SSE-C blocking are present but add neither the second kms:Decrypt layer nor the per-decrypt data event. Defense-in-depth requiring prior s3:GetObject access; the report does not overclaim secret exposure (Pulumi encrypts secret config separately). Low is appropriate.
Severity
Reported: Low. Adjusted: Low (confirmed). A clear deviation from a hardening standard the repo itself follows for its CloudTrail sink, but defense-in-depth: the attack requires a principal that already holds
s3:GetObjecton the bucket. Correctly self-rated.OWASP / standard
A02:2021 (Cryptographic Failures); Checkov CKV_AWS_145 (S3 encrypted with KMS CMK).
Attack scenario
The Pulumi state buckets hold serialized stack state (resource outputs, ARNs, non-secret config), the Config bucket holds full resource-configuration snapshots, and the central logging bucket holds server-access logs. All three are encrypted with SSE-S3 (AES256), which is transparent to anyone holding
s3:GetObject: there is no secondkms:Decryptauthorization layer and no per-decryption CloudTrail data event tied to a dedicated key. A principal that gains onlys3:GetObjectreads plaintext directly, whereas a customer-managed KMS key would require a separate grant and would surface each decrypt in CloudTrail. The policy pack's encryption check treats this as compliant because it only verifies that somesseAlgorithmstring is present, so it cannot flag SSE-S3 (or a downgrade) on a sensitive store.Evidence
pulumi/infra/pulumi_state.py:228-240—_state_bucket_encryption_rulesusessse_algorithm="AES256"(SSE-S3) for primary and replica state bucketspulumi/infra/security_account_controls.py:240-252— Config delivery bucketsse_algorithm="AES256"pulumi/infra/logging_bucket.py:307-320— central logging bucketsse_algorithm="AES256"(contrastoperations_monitoring.py:499-502, where the CloudTrail bucket correctly usesaws:kmswith a CMK)policy/guardrails.py:286-294—_has_default_s3_encryption_rulereturnsTrueas soon as anyapplyServerSideEncryptionByDefault.sseAlgorithmstring exists, with no CMK/kmsMasterKeyIdrequirement (grep ofpolicy/forkmsMasterKeyId/aws:kms/CKV_AWS_145 returns zero matches)Impact
A single-permission (
s3:GetObject) compromise on a state/config/log bucket yields plaintext with no second authorization layer and no dedicated-key decrypt audit trail — a missing defense-in-depth layer on the repo's most sensitive stores, inconsistent with the CMK pattern already applied to CloudTrail.Remediation
sse_algorithm="aws:kms"+kms_master_key_id, withenable_key_rotation), matching the CloudTrail bucket pattern.aws:kmswith a CMK rather than accepting SSE-S3.Acceptance criteria (5/5)
aws:kmswith a customer-managed key and key rotation; unit tests assert the CMK encryption config on each.kmsMasterKeyId; a policy test covers an AES256 bucket → fail.aws:kmstoAES256on these buckets is rejected by CI.kms:Decryptto the intended principals.Verification notes
All four citations verified across passes: the three buckets use
AES256while CloudTrail usesaws:kms+ CMK (proving the pattern exists and was applied to only one of four sensitive stores)._has_default_s3_encryption_rulereturns True on anysseAlgorithmstring with no CMK check. Public-access blocks and SSE-C blocking are present but add neither the secondkms:Decryptlayer nor the per-decrypt data event. Defense-in-depth requiring priors3:GetObjectaccess; the report does not overclaim secret exposure (Pulumi encrypts secret config separately). Low is appropriate.