運用中のS3を暗号化する話

概要

"""社会"""にはいろいろあるので、運用中のS3を暗号化しました

HERPの話です。採用してます。

careers.herp.co.jp

ざっくり以下の内容(+ S3 Bucket Key)をやった、という感じです

Encrypting objects with Amazon S3 Batch Operations | AWS Storage Blog

制約と方向性

ここでいう暗号化はSSE(Server Side Encryption)です

AWS Key Management Service (SSE-KMS) に保存されている CMK でサーバー側の暗号化を使用してデータを保護する - Amazon Simple Storage Service

S3の暗号化はオブジェクトごとなので、暗号化済みオブジェクトとそうでないものが混ざったBucketに対して権限さえ適切であれば問題なくR/Wすることができます。

ちなみに鍵の指定方法として

  • Amazon S3 が管理する暗号化キー
  • AWS 管理の CMK
  • カスタマー管理の CMK
  • 持ち込みの暗号化キー

がありますが、今回はカスタマー管理のCMKにすることにしました。SSE-KMSです。

カスタマー管理のCMKはクロスアカウントできるとか、キーロテーションの期間を変更することができるとかのメリットがあります。

なので、今回の適切な権限はアプリケーション側が使用している権限に以下が追加されることを想定しています

        {
            "Action": [
                "kms:GenerateDataKey",
                "kms:Decrypt"
            ],
            "Effect": "Allow",
            "Resource": [
                "なんか鍵"
            ]
        }

2020年ごろに S3 バケットキーという感じの機能が追加されて、R/WのたびにKMS使用コストがかかっていたのが不要になるのでそっちを使います

Amazon S3 バケットキーを使用した SSE-KMS のコストの削減 - Amazon Simple Storage Service

KMS キーポリシーが適切なPrincipalに対して kms:Decrypt が許可されているのを確認します。クロスアカウントでなければ問題ないはずです。

余談ですが、Principalが s3.amazonaws.com でなくていいんすね。ここら辺のPrincipal感覚はあまりわかっていないですね。

フロー

というわけで、以下のフローでやります

  1. アプリケーションに上記の権限を足して、デプロイする

  2. BucketのSSEを有効にする

  3. 暗号化されていないものを暗号化する

BucketのSSEを有効化

terraformの擬似コードですが、以下のようになります

resource "aws_kms_key" "this" { }

resource "aws_s3_bucket" "this" {
  bucket = "example"
  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        kms_master_key_id = aws_kms_key.this.arn
        sse_algorithm     = "aws:kms"
      }
      bucket_key_enabled = true
    }
  }
  versioning {
    enabled = true
  }
}

暗号化されていないものを暗号化する

Objectを再Putすると暗号化されます。 最近はs3 batchと呼ばれる便利なものがあるのでそれを使います

inventory 設定

s3 batchを実行するためにkeyのリストが必要なので s3 inventoryを使います。

s3 inventoryの実行結果を保存するBucketを用意します。

resource "aws_s3_bucket" "s3-batch" {
  bucket        = "example-s3-batch"
  force_destroy = true
  policy        = <<EOF
{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Effect": "Allow",
      "Principal": {
          "Service": "s3.amazonaws.com"
      },
      "Action": [
          "s3:PutObject"
      ],
      "Resource": [
          "arn:aws:s3:::example-s3-batch/*"
      ],
      "Condition": {
          "ArnLike": {
              "aws:SourceArn": "arn:aws:s3:::*"
          },
          "StringEquals": {
              "aws:SourceAccount": "${アカウントID}",
              "s3:x-amz-acl": "bucket-owner-full-control"
          }
      }
    }
  ]
}
EOF
}

s3 inventoryの設定は以下のようになります

resource "aws_s3_bucket_inventory" "auth" {
  bucket                   = aws_s3_bucket.this.bucket
  name                     = "exapmle"
  included_object_versions = "All"
  optional_fields          = ["EncryptionStatus"]//, "BucketKeyStatus"
  schedule {
    frequency = "Daily"
  }

  destination {
    bucket {
      format     = "CSV"
      bucket_arn = "arn:aws:s3:::example-s3-batch"
    }
  }
}

このコメントアウトされているBucketKeyStatus部分はBucket Keyで暗号化されているかのステータスが取れるので本当は欲しいんですが、terraformのaws providerで対応されていないのでterraformでやりたければ諦める必要があります。(今週末時間があれば修正したいな...)

inventory結果

24~48時間程度たつと、結果がexample-s3-batchに保存され始めます。 Dailyでどのタイミングのリストかわからないので、余裕を持つ必要があります。 X日にSSEを有効にしていた場合、X+2日の日付のを使うぐらいがいいんじゃないでしょうか。

s3 batch

s3 batch実行用のRoleを作ります

Granting permissions for Amazon S3 Batch Operations - Amazon Simple Storage Service

上を参考にしますが、ブログ用に書き直すがめんどくさかったので、サンプルのコードではs3の任意actionを許可しました。 kmsの権限を足すことを忘れないように

resource "aws_iam_role" "s3-batch" {
  name               = "s3-batch"
  assume_role_policy = <<EOF
{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Effect":"Allow",
         "Principal":{
            "Service":"batchoperations.s3.amazonaws.com"
         },
         "Action":"sts:AssumeRole"
      }
   ]
}
EOF
}

resource "aws_iam_role_policy" "s3-batch" {
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:*"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::*"
        },
        {
            "Action": [
                "kms:GenerateDataKey",
                "kms:Decrypt"
            ],
            "Effect": "Allow",
            "Resource": [
                "*"
            ]
        },
    ]
}
EOF
  role   = aws_iam_role.s3-batch.id
}

実行していきます。s3 inventoryから出力されたcsvからs3 batchを積むスクリプトが以下になります。適宜環境変数は埋めましょう。 わかりにくいですが、envはs3 inventoryのname属性になります。

manifest_bucket=
env=
target_bucket=
date=

# manifest.jsonにcsvのpathが入っている
target_bucket_arn=arn:aws:s3:::$target_bucket
key=$(aws s3 cp s3://$manifest_bucket/$target_bucket/$env/$date/manifest.json - | \
jq -r '.files[].key')

# Amazon S3 Selectを使って暗号化されていないobjectのpathだけ取得する
# s._4が isLatest s._6がEncryptionStatus
aws s3api select-object-content \
  --bucket "$manifest_bucket" --key "$key" \
  --input-serialization 'CSV={},CompressionType=GZIP' \
  --output-serialization 'CSV={}'\
  --expression "select s._1, s._2 from s3object s where s._4 = 'true' and s._6 = 'NOT-SSE'" \
  --expression-type SQL $env-output.csv

csv_key=$env/output.csv

# なんかs3に上がってないとダメなんでアップロードする

aws s3 cp $env-output.csv s3://$manifest_bucket/$csv_key

etag=$(aws s3api head-object --bucket $manifest_bucket --key $csv_key | jq -r .ETag)

# s3 batchのJobを積む
aws s3control create-job \
    --region ap-northeast-1 \
    --account-id "$AWS_ACCOUNT_ID" \
    --operation "S3PutObjectCopy={BucketKeyEnabled=true,TargetResource=$target_bucket_arn}" \
    --manifest '{"Spec":{"Format":"S3BatchOperations_CSV_20180820","Fields":["Bucket", "Key"]},"Location":{"ObjectArn":"arn:aws:s3:::'$manifest_bucket/$csv_key'","ETag":'$etag'}}' \
    --report "Bucket=arn:aws:s3:::$manifest_bucket,Prefix=s3-batch-reports,Format=Report_CSV_20180820,Enabled=true,ReportScope=FailedTasksOnly" \
    --priority 42 \
    --role-arn "$ROLE_ARN" \
    --client-request-token $(uuidgen) \
    --description "$env" \
    --no-confirmation-required

operationのoptionに BucketKeyEnabled=true が含まれているんですが、S3 Bucket Keyが有効である場合このオプションなしで動くと思っていたんですが、つけないとダメでした。

このオプションなしだとS3 Bucket Keyなしで暗号化されます。

最初、kms のkey policyのPrincipalにs3 batchが含まれていないことが原因かと思ったのですが、そこそこ試した結果kmsのkey policyは関係ないと判断しました。

AWS有識者の情報をお待ちしております。

確認

暗号化されているか確認するために後日再びS3 Selectを使って確認します

select s._1, s._2 from s3object s where s._4 = 'true' and s._6 = 'NOT-SSE' の結果が空であれば全ての暗号化が完了しています。 現状Objectを上書きする運用をしていないので、今回s._4を見る必要がない気はします。 BucketKeyStatus を有効にしてあれば、s._7 も確認すると S3 Bucket Keyが使われているか確認することができます。

再掲

careers.herp.co.jp

上記の内容はConsoleをぽちぽちしていてもできると思いますが、HERPでは仕事の完了の定義の中にコード化することを含めています。