AWS

S3バケットのサーバ側暗号化の全パターン – SSE-S3/SSE-KMS/SSE-C

スポンサーリンク
S3バケットのサーバ側暗号化の全パターン - SSE-S3/SSE-KMS/SSE-C AWS
スポンサーリンク
スポンサーリンク

S3バケットのサーバ側暗号化の全パターン – SSE-S3/SSE-KMS/SSE-C

S3バケットのサーバ側の暗号化は以下の3パターンあります。

Amazon S3 マネージドキーを用いたサーバー側の暗号化 (SSE-S3)

AWS Key Management Service に保存されている KMS キーによるサーバー側の暗号化 (SSE-KMS)

顧客提供のキーを用いたサーバー側の暗号化 (SSE-C)

サーバー側の暗号化を使用したデータの保護

今回はCloudFormationを使用して、上記の全パターンの暗号化を体験します。

構築する環境

Diagram of all patterns of server-side encryption of S3 buckets - SSE-S3/SSE-KMS/SSE-C

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テンプレートを配置しています。

awstut-fa/125 at main · awstut-an-r/awstut-fa
Contribute to awstut-an-r/awstut-fa development by creating an account on GitHub.

テンプレートファイルのポイント解説

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用のバケットですが、特別な設定は不要です。
ただし以下のページを参考に、バケットポリシーを設定します。

お客様が指定したキーによるサーバー側の暗号化 (SSE-C) の使用 - Amazon Simple Storage Service
独自のカスタムキーを使用して Amazon S3 に格納するオブジェクトを暗号化するには、お客様が用意した暗号化キーによるサーバー側の暗号化 (SSE-C) を使用します。
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マネージメントコンソールから各種リソースを確認します。

各バケットの暗号化設定を確認します。

Detail of S3 1.
Detail of S3 2.
Detail of S3 3.
Detail of S3 4.

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キーを確認します。

Detail of KMS 1.

CMK用キーです。
キーポリシーも正常に設定されています。

Secrets Managerを確認します。

Detail of Secrets Manager 1.

カスタマーマネージドキーとしてのランダムな文字列が作成されていることがわかります。

2つのLambda関数を確認します。

Detail of Lambda 1.
Detail of Lambda 2.

こちらも正常に作成されています。

動作確認

準備が整いましたので、各バケットの動作を確認します。

バケット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)を実行します。
以下が本関数の実行時のログです。

Detail of Lambda 3.

正常に関数が実行されました。
これでテストファイルがアップロードされたはずです。

S3バケットを確認します。

Detail of Lambda 4.

確かにファイル(test4-2)がアップロードされています。

続いてダウンロード用関数(fa-125-function2)を実行します。
以下が本関数の実行時のログです。

Detail of Lambda 5.

正常に関数が実行されました。
確かにテストファイルの中身(「awstut!」)が確認できました。

このようにLambda関数を使用しても、SSE-Cでファイルのアップロード/ダウンロードを行うことができます。

まとめ

S3バケットのサーバ側暗号化の全パターン(SSE-S3/SSE-KMS/SSE-C)を確認しました。

タイトルとURLをコピーしました