S3バケットのサーバ側暗号化の全パターン – SSE-S3/SSE-KMS/SSE-C
S3バケットのサーバ側の暗号化は以下の3パターンあります。
Amazon S3 マネージドキーを用いたサーバー側の暗号化 (SSE-S3)
AWS Key Management Service に保存されている KMS キーによるサーバー側の暗号化 (SSE-KMS)
顧客提供のキーを用いたサーバー側の暗号化 (SSE-C)
サーバー側の暗号化を使用したデータの保護
今回はCloudFormationを使用して、上記の全パターンの暗号化を体験します。
構築する環境
3種類のS3バケットを作成します。
内訳は以下の通りです。
- SSE-S3を設定したバケット
- SSE-KMSを設定したバケット
- SSE-Cを設定したバケット
なおSSE-KMSは以下の2パターンを作成します。
- AWSマネージドキーを使用する。
- カスタマーマネージドキー(CMK)を使用する。
SSE-Cでは、ユーザ側でキーを用意します。
今回はSecrets Managerのシークレットを作成時に、自動的にランダムな文字列を生成し、これをキーとします。
各バケットへのアクセスですが、AWS CLIを使用します。
SSE-C用のバケットについては、Lambda関数からもアクセスすることとします。
CloudFormationテンプレートファイル
上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。
https://github.com/awstut-an-r/awstut-fa/tree/main/125
テンプレートファイルのポイント解説
S3バケット
SSE-S3
Resources:
Bucket1:
Type: AWS::S3::Bucket
Properties:
AccessControl: Private
BucketName: !Sub "${Prefix}-sse-s3"
BucketEncryption:
ServerSideEncryptionConfiguration:
- BucketKeyEnabled: false
ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
Code language: YAML (yaml)
SSE-S3の有効化は、ServerSideEncryptionByDefaultプロパティで設定します。
SSEAlgorithmプロパティに「AES256」を指定することで、有効化されます。
SSE-KMS(AWS Managed Key)
Resources:
Bucket2:
Type: AWS::S3::Bucket
Properties:
AccessControl: Private
BucketName: !Sub "${Prefix}-sse-kms-aws-managed"
BucketEncryption:
ServerSideEncryptionConfiguration:
- BucketKeyEnabled: false
ServerSideEncryptionByDefault:
KMSMasterKeyID: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3"
SSEAlgorithm: aws:kms
Code language: YAML (yaml)
SSE-KMSの有効化も先ほどと同様です。
SSEAlgorithmプロパティに「aws:kms」を指定します。
KMSを使用して暗号化する場合、AWSマネージドキーとカスタマーマネージドキーが選べます。
本バケットはKMSMasterKeyIDプロパティに、S3用のマネージドキーのエイリアスを指定することで、前者を選択します。
SSE-KMS(Customer Managed Key)
Resources:
Bucket3:
Type: AWS::S3::Bucket
Properties:
AccessControl: Private
BucketName: !Sub "${Prefix}-sse-kms-cmk"
BucketEncryption:
ServerSideEncryptionConfiguration:
- BucketKeyEnabled: false
ServerSideEncryptionByDefault:
KMSMasterKeyID: !Ref Key
SSEAlgorithm: aws:kms
Code language: YAML (yaml)
本バケットでは、SSE-KMSのカスタマーマネージドキーを指定します。
KMSMasterKeyIDプロパティに、以下のKMSキーを指定します。
Resources:
Key:
Type: AWS::KMS::Key
Properties:
Enabled: true
KeyPolicy:
Version: 2012-10-17
Id: !Sub "${Prefix}-cmk"
Statement:
- Effect: Allow
Principal:
AWS: "*"
Action:
- kms:Encrypt
- kms:Decrypt
- kms:ReEncrypt*
- kms:GenerateDataKey*
- kms:DescribeKey
Resource: "*"
Condition:
StringEquals:
kms:CallerAccount: !Ref AWS::AccountId
kms:ViaService: s3.ap-northeast-1.amazonaws.com
- Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: "*"
Resource: "*"
KeySpec: SYMMETRIC_DEFAULT
KeyUsage: ENCRYPT_DECRYPT
MultiRegion: false
Code language: YAML (yaml)
キーポリシーにつきましては、S3用のAWSマネージドキーを参考に設定しました。
SSE-C
Resources:
Bucket4:
Type: AWS::S3::Bucket
Properties:
AccessControl: Private
BucketName: !Sub "${Prefix}-sse-c"
Code language: YAML (yaml)
SSE-C用のバケットですが、特別な設定は不要です。
ただし以下のページを参考に、バケットポリシーを設定します。
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html
Resources:
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref Bucket4
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- s3:PutObject
Effect: Deny
Principal: "*"
Resource:
- !Sub "arn:aws:s3:::${Bucket4}/*"
Condition:
"Null":
s3:x-amz-server-side-encryption-customer-algorithm: true
Code language: YAML (yaml)
このポリシーの働きですが、以下の引用の通りです。
SSE-C を要求する x-amz-server-side-encryption-customer-algorithm ヘッダーを含まないすべてのリクエストに対して、オブジェクトのアップロード (s3:PutObject) 許可を拒否します。
SSE-C の要件と制限
カスタマーが用意するキーですが、今回はSecrets Managerで自動的に生成したランダムな文字列を使用します。
Resources:
Secret:
Type: AWS::SecretsManager::Secret
Properties:
Description: Secret for S3 SSE-C.
GenerateSecretString:
ExcludeCharacters: ""
ExcludeLowercase: false
ExcludeNumbers: false
ExcludePunctuation: true
ExcludeUppercase: true
IncludeSpace: false
PasswordLength: !Ref PasswordLength
RequireEachIncludedType: true
KmsKeyId: alias/aws/secretsmanager
Name: !Ref Prefix
Code language: YAML (yaml)
Secrets Managerでランダムパスワードを生成する方法については、以下のページをご確認します。
AES256用のキーということで、以下の条件でランダムパスワードを生成しました。
- パスワードの長さは32文字
- アルファベットの小文字と数字を含む
SSE-Cに関しては、AWS CLIだけでなく、Lambda関数からもオブジェクトのアップロード・ダウンロードを行います。
Lambda関数で実行するコードをインライン形式で記載します。
詳細につきましては、以下のページをご確認ください。
関数は2つ作成します。
1つ目はオブジェクトのアップロード用です。
Resources:
Function1:
Type: AWS::Lambda::Function
Properties:
Architectures:
- !Ref Architecture
Environment:
Variables:
BUCKET_NAME: !Ref Bucket4
OBJECT_KEY: !Ref TestObjectKey
SECRET_ARN: !Ref Secret
Code:
ZipFile: |
import base64
import boto3
import hashlib
import os
bucket_name = os.environ['BUCKET_NAME']
object_key = os.environ['OBJECT_KEY']
secret_arn = os.environ['SECRET_ARN']
secretmanager_client = boto3.client('secretsmanager')
s3_client = boto3.client('s3')
object_body = 'awstut!'
char_code = 'utf-8'
content_type = 'text/plain'
def lambda_handler(event, context):
response = secretmanager_client.get_secret_value(
SecretId=secret_arn
)
key = response['SecretString']
key_base64 = base64.b64encode(key.encode()).decode()
key_hash = hashlib.md5(key.encode()).digest()
key_hash_base64 = base64.b64encode(key_hash).decode()
response = s3_client.put_object(
Bucket=bucket_name,
Key=object_key,
Body=object_body.encode(char_code),
ContentEncoding=char_code,
ContentType=content_type,
SSECustomerAlgorithm='AES256',
SSECustomerKey=key_base64,
SSECustomerKeyMD5=key_hash_base64
)
return response
FunctionName: !Sub "${Prefix}-function1"
Handler: !Ref Handler
Runtime: !Ref Runtime
Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)
コードの内容ですが、以下の通りです。
- Secrets Manager用のクライアントオブジェクトを作成後、シークレットにアクセスし、暗号化用のキーを取得する。
- キーのBase64でエンコードした値と、キーのMD5で計算したハッシュ値をBase64でエンコードした値を計算する。
- S3用のクライアントオブジェクトを作成後、put_objectメソッドを実行する。
ポイントはput_objectメソッドの引数です。
SSE-Cでオブジェクトを暗号化する場合、3つのリクエストヘッダーを使用する必要があります。
x-amz-server-side-encryption-customer-algorithm
x-amz-server-side-encryption-customer-key
x-amz-server-side-encryption-customer-key-MD5
お客様が用意したキーによるサーバー側の暗号化 (SSE-C) の指定
これらに対応する引数として、3つのパラメータ(SSECustomerAlgorithm, SSECustomerKey, SSECustomerKeyMD5)を指定します。
なおSSECustomerKeyMD5ですが、以下に引用した通り、指定しない場合は、自動的に入力されます。
Please note that this parameter is automatically populated if it is not provided. Including this parameter is not required
put_object
2つ目はオブジェクトのダウンロード用です。
Resources:
Function2:
Type: AWS::Lambda::Function
Properties:
Architectures:
- !Ref Architecture
Environment:
Variables:
BUCKET_NAME: !Ref Bucket4
OBJECT_KEY: !Ref TestObjectKey
SECRET_ARN: !Ref Secret
Code:
ZipFile: |
import base64
import boto3
import hashlib
import os
bucket_name = os.environ['BUCKET_NAME']
object_key = os.environ['OBJECT_KEY']
secret_arn = os.environ['SECRET_ARN']
secretmanager_client = boto3.client('secretsmanager')
s3_client = boto3.client('s3')
char_code = 'utf-8'
def lambda_handler(event, context):
response = secretmanager_client.get_secret_value(
SecretId=secret_arn
)
key = response['SecretString']
key_base64 = base64.b64encode(key.encode()).decode()
key_hash = hashlib.md5(key.encode()).digest()
key_hash_base64 = base64.b64encode(key_hash).decode()
response = s3_client.get_object(
Bucket=bucket_name,
Key=object_key,
SSECustomerAlgorithm='AES256',
SSECustomerKey=key_base64,
SSECustomerKeyMD5=key_hash_base64
)
body = response['Body'].read()
return body.decode(char_code)
FunctionName: !Sub "${Prefix}-function2"
Handler: !Ref Handler
Runtime: !Ref Runtime
Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)
1つ目の関数と概ね同様です。
S3用クライアントオブジェクトのget_objectメソッドを実行して、アップロードされたオブジェクトをダウンロードします。
こちらもSSE-C用の3つのパラメータを指定します。
以下が2つの関数用のIAMロールです。
Resources:
FunctionRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: FunctionPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Ref Secret
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource:
- !Sub "arn:aws:s3:::${Bucket4}"
- !Sub "arn:aws:s3:::${Bucket4}/*"
Code language: YAML (yaml)
対象バケットに対してオブジェクトの保存/取得が実行できる権限を与える内容です。
環境構築
CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。
CloudFormationスタックを作成し、スタック内のリソースを確認する
CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- バケット1:fa-125-sse-s3
- バケット2:fa-125-sse-kms-aws-managed
- バケット3:fa-125-sse-kms-cmk
- バケット4:fa-125-sse-c
- CMK用のKMSキー:f395ccae-b5d2-426e-8ad9-db852042cbc4
- Secrets Managerシークレット:fa-125
- 関数1:fa-125-function1
- 関数2:fa-125-function2
AWSマネージメントコンソールから各種リソースを確認します。
各バケットの暗号化設定を確認します。
4つのバケットともに正常に作成されていることがわかります。
SSE-Cのバケット(fa-125-sse-c)については、設定していないにも関わらず、SSE-S3が有効化されています。
これは以下の引用の通り、デフォルトでSSE-S3が有効化されるようになったためです。
Amazon S3 では、Amazon S3 のすべてのバケットに対する基本レベルの暗号化として、Amazon S3 マネージドキーによるサーバー側の暗号化 (SSE-S3) が適用されるようになりました。2023 年 1 月 5 日以降、Amazon S3 にアップロードされるすべての新しいオブジェクトは、追加費用なしで、パフォーマンスに影響を与えずに自動的に暗号化されます。
サーバー側の暗号化を使用したデータの保護
KMSキーを確認します。
CMK用キーです。
キーポリシーも正常に設定されています。
Secrets Managerを確認します。
カスタマーマネージドキーとしてのランダムな文字列が作成されていることがわかります。
2つのLambda関数を確認します。
こちらも正常に作成されています。
動作確認
準備が整いましたので、各バケットの動作を確認します。
バケット1(SSE-S3)
AWS CLIからテスト用ファイルをアップロードします。
$ touch test1.txt
$ aws s3 cp ./test1.txt s3://fa-125-sse-s3
upload: ./test1.txt to s3://fa-125-sse-s3/test1.txt
$ aws s3 ls s3://fa-125-sse-s3
2023-04-05 13:14:07 0 test1.txt
Code language: Bash (bash)
正常にアップロードすることができました。
このようにSSE-S3を有効化したバケットにオブジェクトをアップロードする場合は、特別な操作は不要です。
ファイルのダウンロード時の挙動も確認します。
$ aws s3 cp s3://fa-125-sse-s3/test1.txt ./test1-download.txt
download: s3://fa-125-sse-s3/test1.txt to ./test1-download.txt
Code language: Bash (bash)
正常にダウンロードすることができました。
このようにSSE-S3を有効化したバケットからオブジェクトをダウンロードする場合も、特別な操作は不要です。
バケット2(SSE-KMS: AWS Managed Key)
AWS CLIからテスト用ファイルをアップロードします。
$ touch test2.txt
$ aws s3 cp ./test2.txt s3://fa-125-sse-kms-aws-managed
upload: ./test2.txt to s3://fa-125-sse-kms-aws-managed/test2.txt
$ aws s3 ls s3://fa-125-sse-kms-aws-managed
2023-04-05 13:39:15 0 test2.txt
Code language: Bash (bash)
正常にアップロードすることができました。
このようにAWSマネージドキーを使用したSSE-KMSを有効化したバケットに、オブジェクトをアップロードする場合は、特別な操作は不要です。
ファイルのダウンロード時の挙動も確認します。
$ aws s3 cp s3://fa-125-sse-kms-aws-managed/test2.txt ./test2-download.txt
download: s3://fa-125-sse-kms-aws-managed/test2.txt to ./test2-download.txt
Code language: Bash (bash)
正常にダウンロードすることができました。
このようにAWSマネージドキーを使用したSSE-S3を有効化したバケットから、オブジェクトをダウンロードする場合も、特別な操作は不要です。
バケット3(SSE-KMS: CMK)
AWS CLIからテスト用ファイルをアップロードします。
$ touch test3.txt
$ aws s3 cp ./test3.txt s3://fa-125-sse-kms-cmk
upload: ./test3.txt to s3://fa-125-sse-kms-cmk/test3.txt
$ aws s3 ls s3://fa-125-sse-kms-cmk
2023-04-06 11:39:01 0 test3.txt
Code language: Bash (bash)
正常にアップロードすることができました。
このようにCMKを使用したSSE-KMSを有効化したバケットに、オブジェクトをアップロードする場合は、特別な操作は不要です。
ファイルのダウンロード時の挙動も確認します。
$ aws s3 cp s3://fa-125-sse-kms-cmk/test3.txt ./test3-download.txt
download: s3://fa-125-sse-kms-cmk/test3.txt to ./test3-download.txt
Code language: Bash (bash)
正常にダウンロードすることができました。
このようにCMKを使用したSSE-S3を有効化したバケットから、オブジェクトをダウンロードする場合も、特別な操作は不要です。
バケット4(SSE-C)
AWS CLIからテスト用ファイルをアップロードします。
Secrets Managerに保存されているカスタマーキーに相当する文字列を取得します。
$ key=$(aws secretsmanager get-secret-value --secret-id fa-125 | jq -r .SecretString)
$ echo $key
hws7rqxtlcwd98e8nds94jf8al3yyi5t
Code language: Bash (bash)
カスタマーキーをBase64でエンコードした値を取得します。
$ key_encoded=$(echo -n $key | base64)
$key_encoded
aHdzN3JxeHRsY3dkOThlOG5kczk0amY4YWwzeXlpNXQ=
Code language: Bash (bash)
同様に、カスタマーキーのMD5のハッシュ値をBase64でエンコードした値も取得します。
$ hash=$(echo -n $key | openssl md5 -binary | base64)
$ echo $hash
aLkI4bOu2sXS3Guq7vU37A==
Code language: Bash (bash)
準備が整いました。
まずs3apiコマンドでアップロードする際の挙動を確認します。
まずはカスタマーキー等を指定しない状態で、テストファイルのアップロードを試みます。
$ touch test4-1-1.txt
$ aws s3api put-object \
--bucket fa-125-sse-c \
--key test4-1-1.txt \
--body ./test4-1-1.txt
An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Code language: Bash (bash)
エラーが出ました。
バケットポリシーによって拒否されたということです。
改めてカスタマーキーとハッシュ値を指定しつつ、テストファイルをアップロードします。
$ aws s3api put-object \
--bucket fa-125-sse-c \
--key test4-1-1.txt \
--body ./test4-1-1.txt \
--sse-customer-algorithm AES256 \
--sse-customer-key $key_encoded \
--sse-customer-key-md5 $hash
{
"ETag": "\"f540442da544cc136f5ec839b1b7b1f4\"",
"SSECustomerAlgorithm": "AES256",
"SSECustomerKeyMD5": "aLkI4bOu2sXS3Guq7vU37A=="
}
$ aws s3 ls s3://fa-125-sse-c
2023-04-06 11:58:28 0 test4-1-1.txt
Code language: Bash (bash)
正常にアップロードできました。
次にs3コマンドでアップロードする際の挙動を確認します。
まずはカスタマーキー等を指定しない状態で、テストファイルのアップロードを試みます。
$ aws s3 cp ./test4-1-2.txt s3://fa-125-sse-c
upload failed: ./test4-1-2.txt to s3://fa-125-sse-c/test4-1-2.txt
An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Code language: Bash (bash)
エラーが出ました。
バケットポリシーによって拒否されたということです。
改めてカスタマーキーを指定して、テストファイルのアップロードを行います。
なおこちらの場合は、カスタマーキーをBase64でエンコードする必要はなく、またハッシュ値の指定も不要です。
$ aws s3 cp ./test4-1-2.txt s3://fa-125-sse-c \
--sse-c AES256 \
--sse-c-key $key
upload: ./test4-1-2.txt to s3://fa-125-sse-c/test4-1-2.txt
$ aws s3 ls s3://fa-125-sse-c
2023-04-06 11:58:28 0 test4-1-1.txt
2023-04-06 12:05:19 0 test4-1-2.txt
Code language: Bash (bash)
ファイルのダウンロード時の挙動も確認します。
まずs3apiコマンドにおいて、カスタマーキー等を指定しない場合の挙動を確認します。
$ aws s3api get-object test4-1-1-download.txt \
--bucket fa-125-sse-c \
--key test4-1-1.txt
An error occurred (InvalidRequest) when calling the GetObject operation: The object was stored using a form of Server Side Encryption. The correct parameters must be provided to retrieve the object.
Code language: Bash (bash)
カスタマーキーを指定していないためエラーが発生しました。
改めてカスタマーキー等を指定してダウンロードを試みます。
$ aws s3api get-object test4-1-1-download.txt \
--bucket fa-125-sse-c \
--key test4-1-1.txt \
--sse-customer-algorithm AES256 \
--sse-customer-key $key_encoded \
--sse-customer-key-md5 $hash
{
"AcceptRanges": "bytes",
"LastModified": "2023-04-06T11:58:28+00:00",
"ContentLength": 0,
"ETag": "\"f540442da544cc136f5ec839b1b7b1f4\"",
"ContentType": "binary/octet-stream",
"Metadata": {},
"SSECustomerAlgorithm": "AES256",
"SSECustomerKeyMD5": "aLkI4bOu2sXS3Guq7vU37A=="
}
Code language: Bash (bash)
正常にダウンロードすることができました。
次にs3コマンドにおいて、カスタマーキー等を指定しない場合の挙動を確認します。
$ aws s3 cp s3://fa-125-sse-c/test4-1-2.txt test4-1-2-download.txt
fatal error: An error occurred (400) when calling the HeadObject operation: Bad Request
Code language: Bash (bash)
エラーが発生しました。
改めてカスタマーキー等を指定してダウンロードを試みます。
$ aws s3 cp s3://fa-125-sse-c/test4-1-2.txt ./test4-1-2-download.txt \
--sse-c AES256 \
--sse-c-key $key
download: s3://fa-125-sse-c/test4-1-2.txt to ./test4-1-2-download.txt
Code language: Bash (bash)
正常にダウンロードすることができました。
最後にLambda関数において、SSE-Cでファイルのアップロード/ダウンロードを行います。
アップロード用関数(fa-125-function1)を実行します。
以下が本関数の実行時のログです。
正常に関数が実行されました。
これでテストファイルがアップロードされたはずです。
S3バケットを確認します。
確かにファイル(test4-2)がアップロードされています。
続いてダウンロード用関数(fa-125-function2)を実行します。
以下が本関数の実行時のログです。
正常に関数が実行されました。
確かにテストファイルの中身(「awstut!」)が確認できました。
このようにLambda関数を使用しても、SSE-Cでファイルのアップロード/ダウンロードを行うことができます。
まとめ
S3バケットのサーバ側暗号化の全パターン(SSE-S3/SSE-KMS/SSE-C)を確認しました。