運用中のS3を暗号化する話
概要
"""社会"""にはいろいろあるので、運用中のS3を暗号化しました
HERPの話です。採用してます。
ざっくり以下の内容(+ S3 Bucket Key)をやった、という感じです
Encrypting objects with Amazon S3 Batch Operations | AWS Storage Blog
制約と方向性
ここでいう暗号化はSSE(Server Side Encryption)です
S3の暗号化はオブジェクトごとなので、暗号化済みオブジェクトとそうでないものが混ざったBucketに対して権限さえ適切であれば問題なくR/Wすることができます。
ちなみに鍵の指定方法として
がありますが、今回はカスタマー管理の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感覚はあまりわかっていないですね。
フロー
というわけで、以下のフローでやります
アプリケーションに上記の権限を足して、デプロイする
BucketのSSEを有効にする
暗号化されていないものを暗号化する
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は関係ないと判断しました。
確認
暗号化されているか確認するために後日再び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が使われているか確認することができます。
再掲
上記の内容はConsoleをぽちぽちしていてもできると思いますが、HERPでは仕事の完了の定義の中にコード化することを含めています。