Skip to content

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.

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:

  1. Bucket default encryption — set on the bucket itself, all uploads are automatically encrypted. No application code changes needed.
  2. 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.

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.