An auditor requires that objects in an Amazon S3 bucket can only be written with SSE-KMS and that nothing in the bucket is ever accessed over plain HTTP. How do you enforce both with a bucket policy, and where do such policies commonly go wrong?
answer
- intent versus enforcement
- two Deny statements, not one
- a missing header is still a mismatch
- name the bucket and the objects
- policies bind writes, not history
basics
~10 sAttach two Deny statements to the bucket policy: deny s3:PutObject unless the s3:x-amz-server-side-encryption condition equals aws:kms, and deny s3:* when aws:SecureTransport is false, listing both the bucket ARN and the object ARN.
solid answer
~40 sTwo Deny statements, because default encryption expresses intent while a policy is what enforces it. For transport, deny `s3:*` with a `Bool` condition on `aws:SecureTransport` being `false`, and list **both** `arn:aws:s3:::my-bucket` and `arn:aws:s3:::my-bucket/*` — a policy that names only the object ARN still allows bucket-level calls such as ListBucket over HTTP. For encryption, deny `s3:PutObject` when `s3:x-amz-server-side-encryption` is not `aws:kms`, and pin the key with `s3:x-amz-server-side-encryption-aws-kms-key-id` if the auditor cares which key. The trap: that condition inspects the **request header**, not the object that results. Because a Deny with `StringNotEquals` also matches when the header is absent, clients that were relying on the bucket's default encryption start getting AccessDenied the moment you attach it — which is usually intended, but it must be rolled out knowingly.
code
json · 30 lines{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
}
},
{
"Sid": "DenyUnlessKmsEncrypted",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}go deeper
Know that forcing TLS on an S3 bucket is a bucket-policy Deny using the aws:SecureTransport condition, and that requiring a particular encryption on upload is a separate Deny on s3:PutObject.
Explain that the encryption condition matches the request header rather than the stored object, and that the transport Deny must cover both the bucket ARN and the object ARN because bucket-level and object-level actions authorize against different resources.
Show the rollout judgment: an absent header is denied too, so set the bucket default, migrate callers, then attach the guardrail — and separately audit and rewrite the objects that predate the policy, since a policy never fixes history.
Own the boundary of the mechanism: a bucket policy guards one bucket, so decide how the rule is guaranteed across every bucket in the estate, who may amend it, and how you detect drift rather than assuming a single policy is the organisational control.
## Two requirements, two mechanisms "Encrypted at rest and in transit" is one sentence and two unrelated controls. Treat them separately. ## Forcing TLS S3 endpoints answer on both HTTPS and plain HTTP. Nothing about how an object is stored constrains how it travels. The control is the global condition key `aws:SecureTransport`, which is `true` when the request arrived over TLS: ``` { "Sid": "DenyInsecureTransport", "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"], "Condition": { "Bool": { "aws:SecureTransport": "false" } } } ``` The single most common defect here is listing only `arn:aws:s3:::my-bucket/*`. Object-level actions are authorized against the object ARN, but bucket-level actions — listing the bucket, reading its configuration — are authorized against the bucket ARN. Omit it and an unencrypted `ListBucket` still succeeds, which is both an audit finding and a real information leak. **Both ARNs, every time.** Note also that this denies plaintext HTTP; it does not pin a TLS version. Where a policy must also require a minimum TLS version, that is a separate condition, and it is worth knowing the two are not the same requirement. ## Forcing SSE-KMS on write ``` { "Sid": "DenyUnlessKmsEncrypted", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-bucket/*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" } } } ``` When the auditor also cares *which* key, add a second Deny on `s3:x-amz-server-side-encryption-aws-kms-key-id` not matching the intended key ARN — otherwise a caller can satisfy the first statement with any KMS key they control, which in a cross-account scenario is a meaningful hole. ## The subtlety that decides this question These condition keys describe the **request header**, not the resulting object. Three consequences follow, and interviewers probe all three: 1. **Absent header is denied.** A Deny with `StringNotEquals` matches when the key is not present in the request at all, because a non-existent value is "not equal" to `aws:kms`. So a client that sends no encryption header and expects the bucket default to fill it in gets `AccessDenied`. That is usually what you want — but it will break console uploads, older SDK calls and any tooling built around the bucket default, so the rollout order matters: set the default encryption first, tell the callers to send the header, then attach the Deny. (The `...IfExists` operator variants exist precisely to opt out of this absent-key matching, which is the wrong choice here.) 2. **The policy governs writes, not the bucket's contents.** Attaching it today does nothing about the objects already in the bucket. Proving compliance means auditing the objects themselves — HeadObject per object, or an S3 Inventory report with the encryption fields — and rewriting the non-conforming ones. 3. **The policy is not the only path.** Objects can arrive by replication or by a copy from elsewhere; make sure every write path sends the header the policy demands, or those pipelines fail after the policy lands. ## Where else these policies go wrong - **`Principal: "*"` in a Deny frightens people.** It is correct: a Deny applies to everyone including your own roles, and that is the point of a guardrail. What it does not do is grant anything. - **Denying yourself out of the bucket.** A broad `s3:*` Deny with a badly written condition can lock out the administrators too. Read the condition twice; there is no back door on a bucket policy other than the bucket owner's ability to rewrite it. - **Confusing default encryption with enforcement.** The default encryption configuration fills in what a request omits; it never rejects a request that explicitly asks for something weaker. Only a policy condition rejects. - **Forgetting that this is one bucket.** A bucket policy protects the bucket it is attached to. Making the rule hold across an account or organisation is a different mechanism, owned elsewhere in the identity stack — do not claim a bucket policy gives you an org-wide guarantee. ## Verifying rather than asserting After attaching the policy, test both branches: a PUT without the header should fail with `AccessDenied`, a PUT with `--server-side-encryption aws:kms` should succeed, and an `http://` request should be refused. Then run an inventory over the existing objects to see what the policy could not retroactively fix. The difference between a candidate who has done this and one who has read about it is whether they mention the pre-existing objects at all.
- Why must the aws:SecureTransport Deny list both the bucket ARN and the object ARN?Because S3 authorizes object actions against `arn:aws:s3:::bucket/*` and bucket actions such as ListBucket or GetBucketPolicy against `arn:aws:s3:::bucket`. A Deny that names only the object ARN leaves every bucket-level call reachable over plain HTTP — the object contents are protected but the key listing is not, which is both a leak and an audit finding.
- After attaching the policy, how do you prove the bucket's existing objects comply?You cannot infer it from the policy — it governs future requests only. Audit the objects themselves: HeadObject returns `x-amz-server-side-encryption` per object, and S3 Inventory can emit the encryption status for the whole bucket on a schedule. Non-conforming objects have to be rewritten, typically with an S3 Batch Operations copy that specifies the required encryption.
- A team complains that uploads broke the day the policy landed, even though the bucket already defaulted to SSE-KMS. What happened?Their clients sent no encryption header, relying on the bucket default to fill it in. The Deny condition inspects the request header, and a `StringNotEquals` Deny matches when the header is absent, so those requests are refused before the default is ever applied. The fix is to send the header explicitly; the lesson is to announce this change before attaching the policy.
saying these in an interview costs you the question
- Believes default encryption alone rejects unencrypted uploads
- Lists only the object ARN in the SecureTransport Deny
- Thinks the policy retroactively fixes existing objects
- Says Principal "*" in a Deny grants public access
- Assumes the condition inspects the stored object, not the request