S3 Encryption¶
When storing sensitive files (PII documents, contracts, etc.) in S3, server-side encryption ensures data is protected at rest. This page compares the available encryption options and provides setup guidance for the recommended approach.
Encryption Options Compared¶
SSE-S3 (S3-Managed Keys)¶
| Aspect | Detail |
|---|---|
| How it works | AWS manages keys entirely. Encryption/decryption is automatic and transparent. |
| Access control | Anyone with s3:GetObject permission can read file contents. |
| Audit trail | No per-object decrypt logging. |
| Cost | Free (included in S3). |
Verdict: Not suitable for sensitive data. Encryption is transparent to all IAM users with bucket access — effectively no access separation.
SSE-KMS (KMS-Managed Keys) — Recommended¶
| Aspect | Detail |
|---|---|
| How it works | AWS encrypts/decrypts using a KMS key. Each object gets a unique data key, wrapped by the KMS master key. |
| Access control | Requires both s3:GetObject AND kms:Decrypt permissions, controlled independently. |
| Audit trail | Every decrypt operation is logged in AWS CloudTrail (who, when, which object). |
| Key rotation | Automatic annual rotation, no code changes needed. |
| Cost | ~$1/month per key + $0.03 per 10,000 API requests. |
Verdict: Recommended. Provides true access separation — infrastructure operators can manage the bucket without being able to read file contents.
SSE-C (Customer-Provided Keys)¶
| Aspect | Detail |
|---|---|
| How it works | You provide the encryption key with every upload/download request. |
| Access control | Full control, but key management burden is entirely on you. |
| Risk | Lost key = permanently lost data. No recovery possible. |
Verdict: Not recommended. High operational risk and unnecessary complexity for most use cases.
Summary¶
| Criteria | SSE-S3 | SSE-KMS | SSE-C |
|---|---|---|---|
| Access separation | No | Yes | Yes |
| Audit trail | No | Yes | No |
| Key rotation | N/A | Automatic | Manual |
| Code changes needed | None | None | Significant |
| Operational risk | Low | Low | High |
| Monthly cost | Free | ~$1–7 | Free |
SSE-KMS Setup¶
There are two ways to enable SSE-KMS encryption:
- Bucket default encryption — set on the bucket itself, all uploads are automatically encrypted. No application code changes needed.
- Per-upload encryption via
upload_extra_args— explicitly specify encryption parameters on every upload from the application side.
Both approaches can be combined: bucket default encryption acts as a safety net, while upload_extra_args ensures the correct key is used regardless of bucket configuration.
Per-Upload Encryption with upload_extra_args¶
Use the upload_extra_args parameter to explicitly pass encryption settings with every upload:
import os
from amsdal_storages.s3 import S3Storage
from amsdal.storages import set_default_storage
set_default_storage(
S3Storage(
upload_extra_args={
'ServerSideEncryption': 'aws:kms',
'SSEKMSKeyId': os.environ.get('AWS_S3_KMS_KEY_ARN'),
},
)
)
This is useful when:
- You need to use a specific KMS key rather than the bucket's default key.
- The bucket does not have default encryption enabled and you want to enforce encryption from the application side.
- Different storage instances need to encrypt with different keys (e.g. per-tenant encryption).
Bucket Default Encryption¶
Alternatively, configure encryption at the bucket level. S3Storage works transparently with KMS-encrypted buckets — S3 handles encryption/decryption automatically on upload and download.
1. Create a KMS Key¶
aws kms create-key \
--description "Encryption key for my-app S3 bucket" \
--key-usage ENCRYPT_DECRYPT \
--key-spec SYMMETRIC_DEFAULT \
--region us-east-1
# Note the KeyId from the output
export KMS_KEY_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Create an alias for convenience
aws kms create-alias \
--alias-name alias/my-app-s3-encryption \
--target-key-id ${KMS_KEY_ID} \
--region us-east-1
# Enable automatic key rotation
aws kms enable-key-rotation --key-id ${KMS_KEY_ID} --region us-east-1
2. Set Bucket Default Encryption¶
aws s3api put-bucket-encryption \
--bucket my-app-files \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "'${KMS_KEY_ID}'"
},
"BucketKeyEnabled": true
}]
}' \
--region us-east-1
Tip
BucketKeyEnabled: true reduces KMS API calls (and costs) by generating a bucket-level key instead of per-object keys.
3. Grant KMS Permissions to Your Application¶
The IAM user or role used by your application needs KMS permissions in addition to S3 permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3BucketAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-app-files/*"
},
{
"Sid": "S3BucketList",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-app-files"
},
{
"Sid": "KMSKeyUsage",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:ACCOUNT_ID:key/KEY_ID"
}
]
}
Replace ACCOUNT_ID and KEY_ID with your actual values.
4. Deny Unencrypted Uploads (Optional but Recommended)¶
Add a bucket policy that rejects any upload not using KMS encryption:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonKMSEncryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-app-files/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
},
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-app-files/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}
]
}
Verification¶
After setup, verify that objects are encrypted with the correct key:
aws s3api head-object \
--bucket my-app-files \
--key test-file.txt \
--query '{Encryption: ServerSideEncryption, KeyId: SSEKMSKeyId}'
# Expected: {"Encryption": "aws:kms", "KeyId": "arn:aws:kms:..."}
Access Separation Example¶
With SSE-KMS, access to the S3 bucket and access to file contents are two independent permissions:
Infrastructure operator:
s3:ListBucket, s3:PutObject ✓ can deploy, manage bucket
kms:Decrypt ✗ CANNOT read file contents
Application (IAM role):
s3:GetObject, s3:PutObject ✓ can upload/download files
kms:Decrypt, kms:GenerateDataKey ✓ can encrypt/decrypt contents
Security team:
kms:DescribeKey ✓ can audit key usage
CloudTrail access ✓ can see who accessed what and when
An infrastructure operator with full S3 access who attempts to download a file will receive AccessDenied — because they lack the kms:Decrypt permission on the encryption key.
Cost Estimate¶
| Monthly Volume | Upload + Download Requests | KMS Cost/Month | Total/Year |
|---|---|---|---|
| 1,000 documents | ~2,000 | ~$1.00 (within free tier) | ~$12 |
| 10,000 documents | ~20,000 | ~$1.00 (within free tier) | ~$12 |
| 100,000 documents | ~200,000 | ~$1.54 | ~$18 |
| 1,000,000 documents | ~2,000,000 | ~$6.94 | ~$83 |
KMS pricing: $1/month per key + $0.03 per 10,000 requests. Free tier: 20,000 requests/month.