AWS S3 Encryption

Thursday, Jun 5, 2025| Tags: perl, python, aws, localstack

DISCLAIMER: Image is generated using Leonardo AI.


AWS S3 Encryption


1. Introduction

2. Types of S3 Encryption

3. Server-Side Encryption (SSE)

4. Client-Side Encryption (CSE)

5. AWS Managed Keys (SSE-S3)

6. Using aws

7. Using Python

8. Using Perl


Introduction


A week ago, I wrote a blog post about AWS S3 using the LocalStack platform. It was originally intended to cover just the basic operations. However, a few days later, I updated the post to include object versioning in AWS S3. It was fun to experiment with that feature! If you haven’t checked it out yet, I highly recommend taking a closer look.

I also made another significant update to the post by switching the Perl implementation to use the CPAN module Paws. Initially, I used Net::Amazon::S3, but I quickly discovered that it doesn’t yet support some low-level operations. The transition wasn’t smooth, I had a hard time installing the Paws module. Generally, my Ubuntu setup is very accommodating and rarely gives me trouble, but this time I had to dig deep to get it working. Once installed, though, it was a breeze to use.

While working on object versioning, I stumbled upon another AWS S3 feature, bucket encryption. I decided to create a dedicated blog post to cover this topic. I know I promised to discuss AWS DynamoDB at the end of the earlier post, I’ll get to that next, unless something else comes up in the meantime.

Following the same pattern as the previous post, we’ll first go through the aws command and s3api sub-command, and then look at implementations in Python and Perl.

Now, let’s get back to the main topic of this post.

AWS S3 provides multiple layers of encryption to protect the data.


Types of S3 Encryption



In general, there are two types of encryptions:


1. Server-Side Encryption (SSE)
2. Client-Side Encryption (CSE)

Server-Side Encryption (SSE)



Server-Side Encryption (SSE) is a process where AWS encrypts the data automatically after uploading it to S3 bucket.

The encryption and decryption are managed by AWS completely.

There are three types of Server Side Encryption.


1. AWS Managed Keys    (SSE-S3)
2. KMS Managed Keys    (SSE-KMS)
3. Client Managed Keys (SSE-C)

Client-Side Encryption (CSE)



Client-Side Encryption (CSE) is a process where encryption and decryption happen outside of AWS.

The client manages encryption keys, either symmetric or asymmetric.

Data is encrypted before being sent to S3 and AWS stores only the encrypted content.

Keys are generated and stored by the client e.g. KMS.


AWS Managed Keys (SSE-S3)



In this post, I am going to talk about AWS Managed Keys only.

Here S3 manages the encryption keys using AES-256 encryption.


Using aws



We will use the aws command and s3api sub-command here.


Create Bucket


Let’s create the bucket first.


$ aws s3 mb s3://sse-s3-encrypted-bucket
make_bucket: sse-s3-encrypted-bucket

Enable Encryption


Now we will enable the encryption as below:


$ aws s3api put-bucket-encryption \
  --bucket sse-s3-encrypted-bucket \
  --server-side-encryption-configuration '{
    "Rules": [
      {
        "ApplyServerSideEncryptionByDefault": {
          "SSEAlgorithm": "AES256"
        }
      }
    ]
  }'

Upload File


Let’s create a secret file.


$ echo 'This is a secret file content.' > secret-file.txt

Now upload the secret file.


$ aws s3 cp secret-file.txt s3://sse-s3-encrypted-bucket
upload: ./secret-file.txt to s3://sse-s3-encrypted-bucket/secret-file.txt

Download File


Download the file.


$ aws s3 cp s3://sse-s3-encrypted-bucket/secret-file.txt downloaded-secret-file.txt
download: s3://sse-s3-encrypted-bucket/secret-file.txt to ./downloaded-secret-file.txt

Verify the content.


$ cat downloaded-secret-file.txt
This is a secret file content.

Object Metadata


Retrieves metadata about the stored object.


$ aws s3api head-object --bucket sse-s3-encrypted-bucket --key secret-file.txt
{
    "AcceptRanges": "bytes",
    "LastModified": "2025-06-02T02:28:07+00:00",
    "ContentLength": 31,
    "ETag": "\"5c1c7463b3b94cd0e204abf746a42671\"",
    "ContentType": "text/plain",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}

Finally, verify the encryption settings.


$ aws s3api get-bucket-encryption --bucket sse-s3-encrypted-bucket
{
    "ServerSideEncryptionConfiguration": {
        "Rules": [
            {
                "ApplyServerSideEncryptionByDefault": {
                    "SSEAlgorithm": "AES256"
                }
            }
        ]
    }
}

Cleanup


Cleanup now:


$ aws s3 rm s3://sse-s3-encrypted-bucket/secret-file.txt
delete: s3://sse-s3-encrypted-bucket/secret-file.txt

$ aws s3 rb s3://sse-s3-encrypted-bucket
remove_bucket: sse-s3-encrypted-bucket

Using Python



Here is the Python implementation of the above.

File: sse-s3.py


#!/usr/bin/env python3

import os
import sys
import boto3
import argparse
from botocore.config import Config

ENDPOINT_URL      = "http://localhost:4566"
ACCESS_KEY_ID     = "test"
SECRET_ACCESS_KEY = "test"
REGION            = "eu-west-1"
DEFAULT_FILE      = 'secret-file.txt'
DEFAULT_BUCKET    = 'sse-s3-encrypted-bucket'

def create_s3_client():
    return boto3.client(
        's3',
        endpoint_url = ENDPOINT_URL,
        aws_access_key_id = ACCESS_KEY_ID,
        aws_secret_access_key = SECRET_ACCESS_KEY,
        config = Config(signature_version='s3v4'),
        region_name = REGION
    )

def make_enc_bucket(s3, bucket_name):
    try:
        s3.create_bucket(Bucket=bucket_name,
            CreateBucketConfiguration={
                'LocationConstraint': 'eu-west-1'
            })
        print(f"Bucket {bucket_name} created.")

        s3.put_bucket_encryption(
            Bucket=bucket_name,
            ServerSideEncryptionConfiguration={
                'Rules': [
                    {
                        'ApplyServerSideEncryptionByDefault': {
                            'SSEAlgorithm': 'AES256'
                        }
                    }
                ]
            }
        )
        print(f"Enabled AES-256 encryption on {bucket_name}.")
        return True
    except Exception as e:
        print(f"Error creating encrypted bucket: {e}")
        return False

def upload_file(s3, bucket_name, file):
    try:
        if not os.path.exists(file):
            print(f"Error: File '{file}' does not exist")
            return False

        key = os.path.basename(file)
        s3.upload_file(file, bucket_name, key)
        print(f"Uploaded {file} to {bucket_name}.")
        return True
    except Exception as e:
        print(f"Error uploading file: {e}")
        return False

def download_file(s3, bucket_name, file):
    try:
        key = os.path.basename(file)
        download_path = f"new_{key}"
        s3.download_file(bucket_name, file, download_path)
        print(f"File downloaded to '{download_path}'")

        with open(download_path, 'r') as f:
            print("File content:", f.read())
        return True
    except Exception as e:
        print(f"Error downloading file: {e}")
        return False

def fetch_file_meta(s3, bucket_name, file):
    try:
        key = os.path.basename(file)
        response = s3.head_object(
            Bucket=bucket_name,
            Key=file
        )
        encryption = response.get('ServerSideEncryption')
        if encryption:
            print(f"Object encryption: {encryption}")
        else:
            print("Object encryption: None")
        return True
    except Exception as e:
        print(f"Error fetching file meta: {e}")
        return False

def fetch_bucket_meta(s3, bucket_name):
    try:
        encryption = s3.get_bucket_encryption(Bucket=bucket_name)
        rules = encryption['ServerSideEncryptionConfiguration'].get('Rules', [])
        if rules:
            sse_algorithm = rules[0].get('ApplyServerSideEncryptionByDefault', {}).get('SSEAlgorithm')
            print(f"Bucket encryption: {sse_algorithm}")
        else:
            print("Bucket encryption: None")
        return True
    except Exception as e:
        print(f"Error fetching bucket meta: {e}")
        return False

def cleanup(s3, bucket_name):
    try:
        objects = s3.list_objects_v2(Bucket=bucket_name)
        if 'Contents' in objects:
            for obj in objects['Contents']:
                s3.delete_object(Bucket=bucket_name, Key=obj['Key'])

        s3.delete_bucket(Bucket=bucket_name)
        print(f"Bucket '{bucket_name}' deleted successfully")
        return True
    except Exception as e:
        print(f"Error deleting bucket: {e}")
        return False

class MyArgumentParser(argparse.ArgumentParser):
    def error(self, message):
        if message:
            sys.stderr.write(f"ERROR: {message}\n\n")
        sys.stderr.write(f"ERROR: {message}\n\n")
        self.print_help()
        sys.exit(2)

def main():
    parser = MyArgumentParser(description="SSE-S3 Encryption")
    parser.add_argument('--bucket', default=DEFAULT_BUCKET, help=f"Bucket namei, default {DEFAULT_BUCKET}")
    parser.add_argument('--file', default=DEFAULT_FILE, help=f"File to upload/download, default {DEFAULT_FILE}")

    parser.add_argument('--make-enc-bucket', action='store_true', help="Create encrypted bucket")
    parser.add_argument('--upload', action='store_true', help="Upload file")
    parser.add_argument('--download', action='store_true', help="Download file")
    parser.add_argument('--fetch-file-meta', action='store_true', help="Fetch file metadata")
    parser.add_argument('--fetch-bucket-meta', action='store_true', help="Fetch bucket metadata")
    parser.add_argument('--cleanup', action='store_true', help="Remove bucket and its content")

    args = parser.parse_args()

    # Enforce only one action is selected
    actions = [
        args.make_enc_bucket,
        args.upload,
        args.download,
        args.fetch_file_meta,
        args.fetch_bucket_meta,
        args.cleanup,
    ]

    if sum(bool(action) for action in actions) != 1:
        parser.error("You must specify exactly one of: "
                     "--make-enc-bucket, --upload, --download, "
                     "--fetch-file-meta, --fetch-bucket-meta, --cleanup")

    s3 = create_s3_client()

    if args.make_enc_bucket:
        make_enc_bucket(s3, args.bucket)

    if args.upload:
        upload_file(s3, args.bucket, args.file)

    if args.download:
        download_file(s3, args.bucket, args.file)

    if args.fetch_file_meta:
        fetch_file_meta(s3, args.bucket, args.file)

    if args.fetch_bucket_meta:
        fetch_bucket_meta(s3, args.bucket)

    if args.cleanup:
        cleanup(s3, args.bucket)

if __name__ == '__main__':
    main()

This is how we can use the script:


(myenv) $ py sse-s3.py --help
usage: sse-s3.py [-h] [--bucket BUCKET] [--file FILE] [--make-enc-bucket] [--upload] [--download] [--fetch-file-meta] [--fetch-bucket-meta] [--cleanup]

SSE-S3 Encryption

options:
  -h, --help           show this help message and exit
  --bucket BUCKET      Bucket namei, default sse-se-encrypted-bucket
  --file FILE          File to upload/download, default secret-file.txt
  --make-enc-bucket    Create encrypted bucket
  --upload             Upload file
  --download           Download file
  --fetch-file-meta    Fetch file metadata
  --fetch-bucket-meta  Fetch bucket metadata
  --cleanup            Remove bucket and its content

Create Bucket


Let’s create encrypted bucket.


(myenv) $ py sse-s3.py --bucket sse-s3-encrypted-bucket --make-enc-bucket
Bucket sse-s3-encrypted-bucket created.
Enabled AES-256 encryption on sse-s3-encrypted-bucket.

Upload File


Upload file to bucket


(myenv) $ py sse-s3.py --bucket sse-s3-encrypted-bucket --file secret-file.txt --upload
Uploaded secret-file.txt to sse-s3-encrypted-bucket.

Download File


Download file locally


(myenv) $ py sse-s3.py --bucket sse-s3-encrypted-bucket --file secret-file.txt --download
File downloaded to 'new_secret-file.txt'
File content: This is a secret file content.

Object Metadata


Fetch file meta


(myenv) $ py sse-s3.py --bucket sse-s3-encrypted-bucket --file secret-file.txt --fetch-file-meta
File encryption: AES256

Fetch bucket meta


(myenv) $ py sse-s3.py --bucket sse-s3-encrypted-bucket --fetch-bucket-meta
Bucket encryption: AES256

Cleanup


Cleanup in the end


(myenv) $ py sse-s3.py --bucket sse-s3-encrypted-bucket --cleanup
Bucket 'sse-s3-encrypted-bucket' deleted successfully.

Using Perl



Now, Perl implementation:

File: sse-s3.pl


#!/usr/bin/env perl

use v5.38;
use Try::Tiny;
use File::Slurp;
use Getopt::Long;
use File::Basename;

use Paws;
use Paws::Credential::Explicit;

my $REGION         = 'eu-west-1';
my $ENDPOINT       = 'http://localhost:4566';
my $ACCESS_KEY     = 'test';
my $SECRET_KEY     = 'test';
my $DEFAULT_FILE   = 'secret-file.txt';
my $DEFAULT_BUCKET = 'sse-s3-encrypted-bucket';

my %opts;
GetOptions(
    'bucket=s'          => \$opts{bucket},
    'file=s'            => \$opts{file},
    'make-enc-bucket'   => \$opts{make_enc_bucket},
    'upload'            => \$opts{upload},
    'download'          => \$opts{download},
    'fetch-file-meta'   => \$opts{fetch_file_meta},
    'fetch-bucket-meta' => \$opts{fetch_bucket_meta},
    'cleanup'           => \$opts{cleanup},
    'help'              => \$opts{help},
) or show_help_and_exit(1);

show_help_and_exit(0) if $opts{help};

# Enforce only one action is selected
my @actions = qw(make_enc_bucket upload download fetch_file_meta fetch_bucket_meta cleanup);
my $count   = grep { defined $opts{$_} } @actions;
show_help_and_exit(0, "You must specify exactly one of: --" . join(', --', @actions))
    unless $count == 1;

my $s3 = create_s3_client();

$opts{bucket} = $DEFAULT_BUCKET unless defined $opts{bucket};
$opts{file}   = $DEFAULT_FILE   unless defined $opts{file};

make_enc_bucket($s3, $opts{bucket})              if ($opts{make_enc_bucket});
upload_file($s3, $opts{bucket}, $opts{file})     if ($opts{upload});
download_file($s3, $opts{bucket}, $opts{file})   if ($opts{download});
fetch_file_meta($s3, $opts{bucket}, $opts{file}) if ($opts{fetch_file_meta});
fetch_bucket_meta($s3, $opts{bucket})            if ($opts{fetch_bucket_meta});
cleanup($s3, $opts{bucket})                      if ($opts{cleanup});

#
#
# HELPER SUBROUTINES

sub show_help_and_exit($exit_code, $message=undef) {

    my $help =<<"END_HELP";
usage: $0 [--help] [--bucket BUCKET] [--file FILE] [--make-enc-bucket]
          [--upload] [--download]
          [--view-file-meta] [--view-bucket-meta]
          [--cleanup]

SSE-S3 Encryption

options:
  --help              Show this help message and exit
  --bucket BUCKET     Bucket name, default $DEFAULT_BUCKET
  --file FILE         File to upload/download, default $DEFAULT_FILE
  --make-enc-bucket   Create encrypted bucket
  --upload            Upload file
  --download          Download file
  --fetch-file-meta   Fetch file metadata
  --fetch-bucket-meta Fetch bucket metadata
  --cleanup           Remove file and bucket
END_HELP

    $help = "ERROR: $message\n\n$help" if defined $message;
    print $help and exit $exit_code;
}

sub create_s3_client {
    return Paws->service('S3',
        region      => $REGION,
        endpoint    => $ENDPOINT,
        credentials => Paws::Credential::Explicit->new(
            access_key => $ACCESS_KEY,
            secret_key => $SECRET_KEY,
        ),
    );
}

sub make_enc_bucket($s3, $bucket_name) {
    die "ERROR: Missing S3 client."   unless defined $s3;
    die "ERROR: Missing bucket name." unless defined $bucket_name;

    try {
        say "Creating enc bucket: $bucket_name";
        $s3->CreateBucket(
            Bucket => $bucket_name,
            CreateBucketConfiguration => {
                LocationConstraint => $REGION
            }
        );

        $s3->PutBucketEncryption(
            Bucket => $bucket_name,
            ServerSideEncryptionConfiguration => {
                Rules => [
                    {
                        ApplyServerSideEncryptionByDefault => {
                            SSEAlgorithm => 'AES256'
                        }
                    }
                ]
            }
        );
        say "Enabled AES-256 encryption on $bucket_name";
    }
    catch {
        die "ERROR: unable to create encrypted bucket: $_\n";
    };
}

sub upload_file($s3, $bucket_name, $file) {
    die "ERROR: Missing S3 client."   unless defined $s3;
    die "ERROR: Missing bucket name." unless defined $bucket_name;
    die "ERROR: Missing file."        unless defined $file;

    try {
        $file = basename($file);
        say "Uploading file: $file";

        $s3->PutObject(
            Bucket => $bucket_name,
            Key    => $file,
            Body   => read_file($file, binmode => ':raw')
        );

        say "Uploaded $file to $bucket_name";
    }
    catch {
        die "ERROR: unable to upload file: $_\n";
    };
}

sub download_file($s3, $bucket_name, $file) {
    die "ERROR: Missing S3 client."   unless defined $s3;
    die "ERROR: Missing bucket name." unless defined $bucket_name;
    die "ERROR: Missing file."        unless defined $file;

    try {
        $file = basename($file);
        my $downloaded = $s3->GetObject(
            Bucket => $bucket_name,
            Key    => $file
        );

        my $content = $downloaded->Body;
        say "Downloaded content: $content";
    }
    catch {
        die "ERROR: unable to download file: $_\n";
    };
}

sub fetch_file_meta($s3, $bucket_name, $file) {
    die "ERROR: Missing S3 client."   unless defined $s3;
    die "ERROR: Missing bucket name." unless defined $bucket_name;
    die "ERROR: Missing file."        unless defined $file;

    try {
        $file = basename($file);
        my $head = $s3->HeadObject(
            Bucket => $bucket_name,
            Key    => $file
        );

        say "Object encryption: ", ($head->ServerSideEncryption // 'None');
    }
    catch {
        die "ERROR: unable to view file meta: $_\n";
    };
}

sub fetch_bucket_meta($s3, $bucket_name) {
    die "ERROR: Missing S3 client."   unless defined $s3;
    die "ERROR: Missing bucket name." unless defined $bucket_name;

    try {
        my $encryption = $s3->GetBucketEncryption(Bucket => $bucket_name);

        say "Bucket encryption:";
        my $config = $encryption->ServerSideEncryptionConfiguration;
        foreach my $rule (@{ $config->Rules }) {
            my $default = $rule->ApplyServerSideEncryptionByDefault;
            say "  SSE Algorithm: ", $default->SSEAlgorithm;
        }
    }
    catch {
        die "ERROR: unable to view bucket meta: $_\n";
    };
}

sub cleanup($s3, $bucket_name) {
    die "ERROR: Missing S3 client."   unless defined $s3;
    die "ERROR: Missing bucket name." unless defined $bucket_name;

    try {
        my $resp = $s3->ListObjectsV2(Bucket => $bucket_name);
        for my $obj (@{ $resp->Contents }) {
            say " Delete key: ", $obj->{Key};
            $s3->DeleteObject(Bucket => $bucket_name, Key => $obj->{Key});
        }
        $s3->DeleteBucket(Bucket => $bucket_name);
        say "Bucket '$bucket_name' deleted successfully.";
    }
    catch {
        die "ERROR: unable to cleanup: $_\n";
    };
}

This is how we can use the script:


$ perl sse-s3.pl --help
usage: sse-s3.pl [--help] [--bucket BUCKET] [--file FILE] [--make-enc-bucket]
          [--upload] [--download]
          [--view-file-meta] [--view-bucket-meta]
          [--cleanup]

SSE-S3 Encryption

options:
  --help              Show this help message and exit
  --bucket BUCKET     Bucket namei, default sse-se-encrypted-bucket
  --file FILE         File to upload/download, default secret-file.txt
  --make-enc-bucket   Create encrypted bucket
  --upload            Upload file
  --download          Download file
  --fetch-file-meta   Fetch file metadata
  --fetch-bucket-meta Fetch bucket metadata
  --cleanup           Remove file and bucket

Create Bucket


Let’s create encypted bucket.


$ perl sse-s3.pl --bucket sse-s3-encrypted-bucket --make-enc-bucket
Creating enc bucket: sse-s3-encrypted-bucket
Enabled AES-256 encryption on sse-s3-encrypted-bucket.

Upload File


Upload file to bucket


$ perl sse-s3.pl --bucket sse-s3-encrypted-bucket --file secret-file.txt --upload
Uploading file: secret-file.txt
Uploaded secret-file.txt to sse-s3-encrypted-bucket.

Download File


Download file locally


$ perl sse-s3.pl --bucket sse-s3-encrypted-bucket --file secret-file.txt --download
Downloaded content: This is a secret file content.

Object Metadata


Fetch file meta


$ perl sse-s3.pl --bucket sse-s3-encrypted-bucket --file secret-file.txt --fetch-file-meta
Object encryption: AES256

Fetch bucket meta


$ perl sse-s3.pl --bucket sse-s3-encrypted-bucket --fetch-bucket-meta
Bucket encryption:
  SSE Algorithm: AES256

Cleanup


Cleanup in the end


$ perl sse-s3.pl --bucket sse-s3-encrypted-bucket --cleanup
Delete key: secret-file.txt
Bucket 'sse-s3-encrypted-bucket' deleted successfully.

What next?


The next post would be about Server-Side Encryption using KMS Managed Keys.



Happy Hacking !!!

SO WHAT DO YOU THINK ?

If you have any suggestions or ideas then please do share with us.

Contact with me