0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

S3バケットのセキュリティ設定チェックリスト:2026年版・ACL廃止前提で組み直す7つのレイヤー

0
Posted at

この記事でわかること

  • 2023年以降のS3デフォルト変更(Block Public Access / SSE-S3 / Object Ownership)を踏まえた最新の構成
  • 「BPAを入れたのにACLで公開されていた」を物理的に不可能にする BucketOwnerEnforced 設定
  • aws:SecureTransport / aws:SourceVpce / aws:PrincipalOrgID / s3:ResourceAccount を組み合わせたバケットポリシーの実例
  • SSE-KMS + Bucket Key で KMS コストを 99% 削減する仕組みと、誤って外す条件
  • Server Access Log と CloudTrail Data Events の使い分け、コストの落とし穴
  • 前回記事(Terraform)の stateバケットを「汎用 secure-bucket モジュール」として一般化する Terraform 一式
  • IAM Access Analyzer / AWS Config / S3 Storage Lens で「設定漏れを自動検出」する3点セット

この記事を書いた経緯

前回の記事(Terraform 実務 Tips)で、state バケットの必須設定として「バージョニング / KMS / Block Public Access」の3点を挙げました。あれは state バケット固有の話ではなく、S3 バケット全般に当てはまる汎用要件です。今回はそれを起点に、汎用の S3 セキュリティとして書き直します。

きっかけになった事例があります。あるサービス引き継ぎで「セキュリティ監査で1件指摘が出ているので確認してほしい」と渡されたのが、sample-data-public という、いかにも公開向けに作られたバケット名でした。

調べた結果はこうでした。

  • Block Public Access は4つすべて ON だった
  • それでも IAM Access Analyzer は「外部からの読み取り可能性あり」を報告していた
  • 原因は 2018年に設定されたバケット ACL の public-read が残っていたこと
  • BPA の IgnorePublicAcls=true で実効的にはブロックされていたが、監査ツール側は「ACL が public-read」という事実を検知していた

つまり「BlockPublicAcls が ACL の新規付与を防ぎ、IgnorePublicAcls既存ACLを実効無効化する」という2段の防御は機能していたものの、ACL そのものを物理的に消す手段を持っていなかったために、監査上は永遠にグレーゾーンの状態でした。

これを解決したのが Object Ownership = BucketOwnerEnforced です。設定するとバケット ACL もオブジェクト ACL も完全に無効化され、public-read というメタデータ自体が存在し得なくなります。S3 のデフォルト変更は段階的に進みました。

時期 変更内容
2023年1月 新規オブジェクトに SSE-S3 が自動適用(暗号化なしの PUT が消滅)
2023年4月 新規バケットで Block Public Access が自動 ONObject Ownership = BucketOwnerEnforced がデフォルト(ACL 無効化)

「S3 はデフォルトでもう安全」が成立したのが2023年ということです。

ただし、既存バケットには遡及適用されません。古い環境ほど、デフォルト変更の恩恵を受けていない状態で運用が続いています。本記事は、その差分を埋めるためのチェックリストです。


実務での背景

S3 のセキュリティ設定ミスによる事故は今も毎年起きていますが、事故の中身は2020年頃と2026年でだいぶ変わりました

時代 主な事故原因
〜2020年頃 バケット ACL を public-read のまま放置(典型例:S3バケットがそのままWeb公開)
2020〜2023年 バケットポリシーで Principal: "*" を意図せず広く許可
2023年〜 暗号化キー / ロール権限の組み合わせ に起因する事故(KMS キーポリシー / VPC Endpoint policy ミス)

防御対象が「公開設定の事故」から「権限境界の事故」にシフトしてきています。本記事はその両方をカバーします。


解決方法:7つのレイヤーで守る

「BPA を入れる」「暗号化する」だけでは2026年の要件には足りません。次の7層で多重防御するのが現代の標準です。

image.png

下にあるレイヤーほど「そもそも事故が起きえない」物理的な防御。上のレイヤーは「起きたら気付ける」検知的防御。下から組むのが原則です。


具体的な手順

Step 1: ACL を物理的に無効化する(BucketOwnerEnforced

すべての出発点です。2023年4月以降の新規バケットはデフォルトでこの状態ですが、それ以前に作られたバケットには適用されていません。

aws s3api put-bucket-ownership-controls \
  --bucket my-secure-bucket \
  --ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'

これにより以下が起きます。

  • バケット ACL とオブジェクト ACL が無効化(API 自体が拒否される)
  • すべてのオブジェクトの所有者がバケット所有者に固定
  • s3:PutObjectAcl / s3:GetObjectAcl は呼んでも AccessControlListNotSupported で失敗
  • アクセス制御はバケットポリシーと IAM ポリシーのみで行うことになる
Object Ownership 設定 ACL の扱い 推奨
BucketOwnerEnforced (2023デフォルト) 完全無効化 新規・既存問わず全バケットで強く推奨
BucketOwnerPreferred ACL は有効、所有者だけ自動移譲 移行期間中の暫定
ObjectWriter (旧デフォルト) ACL 有効、書き込み元が所有 クロスアカウント書き込みが必要な特殊用途のみ

注意: 既存バケットを BucketOwnerEnforced に切り替える前に、ACL に依存している処理がないか必ず確認してください。CloudFront OAI(旧来の方式)や、外部アカウントが直接 PutObject してくる連携は影響を受けます。CloudFront は OAC(Origin Access Control)に移行済みであれば問題ありません。

Step 2: Block Public Access を4つ全部 ON にする(バケット + アカウント)

BucketOwnerEnforced で ACL は無効化されますが、バケットポリシーで Principal: "*" を書けば公開できてしまうため、BPA が必要です。

# バケット単位
aws s3api put-public-access-block \
  --bucket my-secure-bucket \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# アカウント全体(最強)
aws s3control put-public-access-block \
  --account-id 123456789012 \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
設定 効果
BlockPublicAcls public な ACL の新規付与を拒否
IgnorePublicAcls 既存の public な ACL を実効無効化
BlockPublicPolicy public な内容を含むバケットポリシーの設定を拒否
RestrictPublicBuckets バケットが public 扱いなら、それを参照する他リソースからのアクセスも拒否

アカウント単位 BPA はバケット単位より強い(バケット側で外しても効かない)ので、「意図的に公開するバケットが1つでもあるか」を組織で議論したうえで、なければアカウント単位で全部 ON にしてしまうのが運用上ラクです。例外を1つ作るなら、その1つだけ別アカウントに切り出すほうがクリーン。

Step 3: バケットポリシーで HTTPS / 経路 / 暗号化を強制する

BucketOwnerEnforced + BPA で「意図しない公開」はほぼ防げます。次に防ぐべきは「意図された相手からの、よくない経路でのアクセス」。バケットポリシーで以下を強制します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-secure-bucket",
        "arn:aws:s3:::my-secure-bucket/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    },
    {
      "Sid": "DenyUnencryptedPut",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-secure-bucket/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "DenyWrongKmsKey",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-secure-bucket/*",
      "Condition": {
        "StringNotEqualsIfExists": {
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:ap-northeast-1:123456789012:key/abcd-..."
        }
      }
    },
    {
      "Sid": "RestrictToOrg",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-secure-bucket",
        "arn:aws:s3:::my-secure-bucket/*"
      ],
      "Condition": {
        "StringNotEqualsIfExists": {
          "aws:PrincipalOrgID": "o-xxxxxxxxxx"
        }
      }
    },
    {
      "Sid": "RestrictToVpce",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-secure-bucket",
        "arn:aws:s3:::my-secure-bucket/*"
      ],
      "Condition": {
        "StringNotEqualsIfExists": {
          "aws:SourceVpce": ["vpce-0123456789abcdef0"]
        },
        "Bool": {
          "aws:ViaAWSService": "false"
        }
      }
    }
  ]
}

ポイントを5つに分けて読みます。

  • DenyInsecureTransport: aws:SecureTransport: false を Deny。HTTPS 必須化。
  • DenyUnencryptedPut: s3:x-amz-server-side-encryptionaws:kms 以外の PutObject を拒否。デフォルト暗号化(Step 4)が効いていても、明示的に弱い設定でアップロードされる可能性を物理的に塞ぐ
  • DenyWrongKmsKey: 指定の KMS キー以外を使った PutObject を拒否。「自社管理キーで暗号化しているはずなのに、別アカウントの KMS キーで書かれていた」事故を防ぐ。
  • RestrictToOrg: AWS Organizations 配下からのアクセスのみ許可。Confused Deputy 対策として aws:PrincipalOrgID を必ず入れる。
  • RestrictToVpce: 指定 VPC Endpoint 経由でないアクセスを拒否(インターネット経由で持ち出される事故の物理防止)。aws:ViaAWSService: false を併記しないと S3 内部処理(レプリケーション・インベントリ等)まで止まるため、ここはセットで書く。

StringNotEqualsIfExists を使うのが地味に重要で、条件キーが**存在しないリクエスト(=匿名・外部アクセス)**もちゃんと Deny 側に倒れます。StringNotEquals のみだとキーが無いリクエストでマッチせず素通りすることがあります。

補足: s3:ResourceAccount は VPCe ポリシー側で使う

aws:PrincipalOrgID は「組織内のプリンシパルだけ許可」、対して s3:ResourceAccount は「対象バケットが自社アカウント所有か」を見るキーです。バケット側ポリシーで使うと常に自分自身を指して無意味なので、VPC Endpoint policyIAM ロール側 に書きます。

// VPC Endpoint policy 側の例(IAM/VPCeに置く)
{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": "*",
  "Condition": {
    "StringNotEqualsIfExists": {
      "s3:ResourceAccount": ["123456789012"]
    }
  }
}

これで「自社が所有していない外部 S3 バケットへの書き込み事故(マルウェア・誤設定によるデータ流出)」を経路レベルで遮断できます。aws:PrincipalOrgID(バケット側)と s3:ResourceAccount(VPCe / IAM 側)は で運用するのが現代の標準です。

Step 4: SSE-KMS + Bucket Key でコストを抑えつつ強い暗号化

2023年1月以降、全バケットでデフォルト SSE-S3 が自動有効になっています。これで何もしなくても保存時暗号化はかかりますが、コンプライアンス要件(自社管理キー / キーローテーション / アクセス監査)が必要なら SSE-KMS にします。

aws s3api put-bucket-encryption \
  --bucket my-secure-bucket \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "arn:aws:kms:ap-northeast-1:123456789012:key/abcd-..."
      },
      "BucketKeyEnabled": true
    }]
  }'

BucketKeyEnabled: true を必ず入れるのが現代のベストプラクティスです。

設定 仕組み KMSコスト
BucketKeyEnabled: false(古い設定) オブジェクトごとに KMS GenerateDataKey が呼ばれる 1リクエスト=1課金。大量PUT/GETでコスト激増
BucketKeyEnabled: true(推奨) バケット単位の中間鍵を生成し、KMS呼び出しを集約 KMSリクエスト料を最大99%削減できるケースあり

セキュリティレベルは同等のまま、コストだけ削減できます。外す理由が無い設定ですが、既存バケットでは false のままになっていることが多いので明示的に切り替えましょう。

さらに強い要件があれば DSSE-KMS(Dual-layer SSE-KMS)も選べます。FIPS 140-3 Level 3 相当の二重暗号化が必要な金融・医療系のみ用途と考えてOKです。一般用途では aws:kms + Bucket Key で十分。

Step 5: バージョニング + Object Lock + ライフサイクル

ランサムウェアや誤削除に対する最後の砦です。

# バージョニング有効化
aws s3api put-bucket-versioning \
  --bucket my-secure-bucket \
  --versioning-configuration Status=Enabled

# Object Lock(バケット作成時のみ有効化可能。コンプライアンスモード or ガバナンスモード)
aws s3api put-object-lock-configuration \
  --bucket my-secure-bucket \
  --object-lock-configuration '{
    "ObjectLockEnabled": "Enabled",
    "Rule": {
      "DefaultRetention": {
        "Mode": "GOVERNANCE",
        "Days": 30
      }
    }
  }'

# 古いバージョンを90日後に削除、未完了マルチパートを7日で中止
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-secure-bucket \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "expire-old-versions-and-aborted-mpu",
      "Status": "Enabled",
      "Filter": {},
      "NoncurrentVersionExpiration": { "NoncurrentDays": 90 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
    }]
  }'

Object Lock のモード選択は明確です。

モード 解除可否 用途
COMPLIANCE 誰も解除不可(ルートアカウントですら不可) 金融・医療など法定保管
GOVERNANCE s3:BypassGovernanceRetention を持つ IAM のみ解除可 業務要件の保管・ランサム対策

業務系は迷わず GOVERNANCE。COMPLIANCE は本当に消せないので、誤って設定すると保管期間中はバケット自体を整理できません。

AbortIncompleteMultipartUpload は地味ですが必須。大きいファイルの PUT が失敗したときに残った未完了パートが、永遠に料金だけ発生し続けます。

Step 6: アクセスログと CloudTrail Data Events

「誰が何をしたか」を残す方法は2系統あり、用途が違います。

方法 ログ粒度 コスト 用途
S3 Server Access Logs リクエスト単位、ベストエフォート(一部欠損あり) S3ストレージ料金のみ 監査・トラブルシュート全般。安いので原則これ
CloudTrail Data Events API 単位、欠損なし(マネージドサービス品質) $0.10 / 100,000イベント 機密データ・コンプライアンス監査が必要なバケット

CloudTrail Data Events は便利ですが、全バケットに有効化すると簡単に月数万円規模になります。重要なバケットだけ ON にする運用が現実的。

# S3 Server Access Logs(ログ用バケットは別に作る)
aws s3 mb s3://my-s3-access-logs --region ap-northeast-1

aws s3api put-bucket-logging \
  --bucket my-secure-bucket \
  --bucket-logging-status '{
    "LoggingEnabled": {
      "TargetBucket": "my-s3-access-logs",
      "TargetPrefix": "my-secure-bucket/"
    }
  }'

ログ用バケットの注意点

  • 監査対象バケットと別バケットにする(同一バケットに書くと監査ループが起きる)
  • ログ用バケットにも 同じ7レイヤー を適用する(ログを改竄されたら監査が無意味)
  • 30〜90日でライフサイクル削除しないとストレージコストが積み上がる

Step 7: 検出する仕組み(Config / Access Analyzer / Storage Lens)

ここまでは「正しく設定する」話。最後は「設定が崩れたら気付く」仕組みです。

# Config: 公開バケットを検出
aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "s3-bucket-public-read-prohibited",
  "Source": {
    "Owner": "AWS",
    "SourceIdentifier": "S3_BUCKET_PUBLIC_READ_PROHIBITED"
  }
}'

aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "s3-bucket-server-side-encryption-enabled",
  "Source": {
    "Owner": "AWS",
    "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
  }
}'

# IAM Access Analyzer: 外部アカウントからアクセス可能なバケットを検出
aws accessanalyzer create-analyzer \
  --analyzer-name org-public-access \
  --type ORGANIZATION
ツール 何を検出するか
AWS Config 設定ドリフト(暗号化OFF・BPA OFF・パブリックポリシー など)
IAM Access Analyzer バケットポリシーや ACL が外部に解放されていることを実際の評価で検出
S3 Storage Lens アカウント横断で「暗号化なしオブジェクト数」「BPA OFFバケット数」をダッシュボード化

3つは目的が違うので併用。Configは「設定値の異常」、Access Analyzerは「実効的なアクセス可能性」、Storage Lensは「組織全体の俯瞰」。


構成図

image.png


Terraform:汎用 secure-bucket モジュール

前回記事の state バケット要件を、汎用 S3 セキュリティモジュールとして一般化したものです。Step 1〜7 を Terraform 1モジュールに集約しています。

# modules/secure-bucket/variables.tf
variable "bucket_name" {
  type = string
  validation {
    condition     = length(var.bucket_name) >= 3 && length(var.bucket_name) <= 63
    error_message = "bucket_name は 3〜63 文字"
  }
}

variable "kms_key_arn" {
  type        = string
  description = "SSE-KMS で使用する CMK の ARN"
}

variable "org_id" {
  type        = string
  description = "aws:PrincipalOrgID で許可する Organization ID"
}

variable "object_lock_days" {
  type    = number
  default = 30
}

variable "noncurrent_version_expiration_days" {
  type    = number
  default = 90
}

variable "allowed_vpce_ids" {
  type        = list(string)
  description = "アクセスを許可する VPC Endpoint ID。空にすると VPCe 制限を付けない"
  default     = []
}
# modules/secure-bucket/main.tf

# Layer 1 & 5 (Object Lockはバケット作成時にしか有効化できない)
resource "aws_s3_bucket" "this" {
  bucket              = var.bucket_name
  object_lock_enabled = true
}

# Layer 1: ACL 無効化
resource "aws_s3_bucket_ownership_controls" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

# Layer 2: Block Public Access
resource "aws_s3_bucket_public_access_block" "this" {
  bucket                  = aws_s3_bucket.this.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Layer 4: SSE-KMS + Bucket Key
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = var.kms_key_arn
    }
    bucket_key_enabled = true
  }
}

# Layer 5: Versioning
resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = "Enabled"
  }
}

# Layer 5: Object Lock (GOVERNANCE)
resource "aws_s3_bucket_object_lock_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    default_retention {
      mode = "GOVERNANCE"
      days = var.object_lock_days
    }
  }
}

# Layer 5: Lifecycle (古いバージョン・未完了MPU)
resource "aws_s3_bucket_lifecycle_configuration" "this" {
  bucket = aws_s3_bucket.this.id

  rule {
    id     = "expire-old-versions-and-aborted-mpu"
    status = "Enabled"
    filter {}

    noncurrent_version_expiration {
      noncurrent_days = var.noncurrent_version_expiration_days
    }
    abort_incomplete_multipart_upload {
      days_after_initiation = 7
    }
  }
}

# Layer 3: バケットポリシー
data "aws_iam_policy_document" "this" {
  statement {
    sid     = "DenyInsecureTransport"
    effect  = "Deny"
    actions = ["s3:*"]
    resources = [
      aws_s3_bucket.this.arn,
      "${aws_s3_bucket.this.arn}/*",
    ]
    principals {
      type        = "*"
      identifiers = ["*"]
    }
    condition {
      test     = "Bool"
      variable = "aws:SecureTransport"
      values   = ["false"]
    }
  }

  statement {
    sid       = "DenyUnencryptedPut"
    effect    = "Deny"
    actions   = ["s3:PutObject"]
    resources = ["${aws_s3_bucket.this.arn}/*"]
    principals {
      type        = "*"
      identifiers = ["*"]
    }
    condition {
      test     = "StringNotEquals"
      variable = "s3:x-amz-server-side-encryption"
      values   = ["aws:kms"]
    }
  }

  statement {
    sid       = "DenyWrongKmsKey"
    effect    = "Deny"
    actions   = ["s3:PutObject"]
    resources = ["${aws_s3_bucket.this.arn}/*"]
    principals {
      type        = "*"
      identifiers = ["*"]
    }
    condition {
      test     = "StringNotEqualsIfExists"
      variable = "s3:x-amz-server-side-encryption-aws-kms-key-id"
      values   = [var.kms_key_arn]
    }
  }

  statement {
    sid     = "RestrictToOrg"
    effect  = "Deny"
    actions = ["s3:*"]
    resources = [
      aws_s3_bucket.this.arn,
      "${aws_s3_bucket.this.arn}/*",
    ]
    principals {
      type        = "*"
      identifiers = ["*"]
    }
    condition {
      test     = "StringNotEqualsIfExists"
      variable = "aws:PrincipalOrgID"
      values   = [var.org_id]
    }
  }

  dynamic "statement" {
    for_each = length(var.allowed_vpce_ids) > 0 ? [1] : []
    content {
      sid     = "RestrictToVpce"
      effect  = "Deny"
      actions = ["s3:*"]
      resources = [
        aws_s3_bucket.this.arn,
        "${aws_s3_bucket.this.arn}/*",
      ]
      principals {
        type        = "*"
        identifiers = ["*"]
      }
      condition {
        test     = "StringNotEqualsIfExists"
        variable = "aws:SourceVpce"
        values   = var.allowed_vpce_ids
      }
      # S3 内部処理 (replication / inventory 等) を巻き込まないよう除外
      condition {
        test     = "Bool"
        variable = "aws:ViaAWSService"
        values   = ["false"]
      }
    }
  }
}

resource "aws_s3_bucket_policy" "this" {
  bucket = aws_s3_bucket.this.id
  policy = data.aws_iam_policy_document.this.json

  depends_on = [aws_s3_bucket_public_access_block.this]
}
# 呼び出し側
module "billing_data_bucket" {
  source = "git::https://github.com/myorg/terraform-modules.git//secure-bucket?ref=v1.0.0"

  bucket_name      = "billing-data-prod"
  kms_key_arn      = aws_kms_key.billing.arn
  org_id           = "o-xxxxxxxxxx"
  object_lock_days = 90
}

前回記事で書いた state バケットも、このモジュール呼び出し1個で済むようになります。

aws_s3_bucket_policy には必ず depends_on = [aws_s3_bucket_public_access_block.this] を入れるのが重要です。BPA が後に作成されると、ポリシー側で public 扱いされる構文があれば apply 中に一時的に拒否される可能性があります。


ハマりポイント

BucketOwnerEnforced を CloudFront OAI 環境でいきなり有効化する

CloudFront OAI(Origin Access Identity、旧方式)はバケット ACL に依存しています。BucketOwnerEnforced を入れた瞬間に配信が落ちます。先に CloudFront OAC(Origin Access Control、新方式)に移行してから切り替えること。OAC はバケットポリシーで認可するため ACL に依存しません。

❌ Block Public Access の4つを部分的に ON にして「公開バケットも残せる」と思う

公開バケットを残したいなら、そのバケットだけ別 AWS アカウントに分離するのが最も安全です。同じアカウントに公開・非公開を混ぜると、アカウント単位 BPA で全部止めることができず、結局運用が複雑化します。

DenyUnencryptedPut の条件キー名を間違える

s3:x-amz-server-side-encryption の値は aws:kms / AES256 / aws:kms:dsse のいずれか。バケットのデフォルト暗号化が SSE-S3 なのにポリシーで aws:kms を要求すると、デフォルト暗号化を頼った PUT が全部失敗します。バケット側設定とポリシー側要求は必ず一致させる。

❌ Bucket Key を「セキュリティが弱まる」と勘違いして切る

Bucket Key は鍵管理の方式を変えるだけで、暗号化強度は同じ AES-256 です。KMS への問い合わせ回数を集約しているだけ。コンプライアンス担当に「セキュリティを弱めるのか」と聞かれたら「監査ログも CloudTrail に残るし、暗号アルゴリズムも同じ」と説明できます。

❌ Server Access Log のログ用バケットを同じバケットにする

ログ用バケットを自分自身にすると書き込みループが起きてストレージ料金が膨らみます。必ず別バケット、できれば別アカウントに置くのが理想(監査独立性のため)。

❌ Object Lock を COMPLIANCE モードで安易に設定する

COMPLIANCE はルートアカウントですら解除不可。テスト目的で短期間設定したつもりが「保管期間が終わるまでバケット自体を削除できない」状態になります。業務用途はすべて GOVERNANCE で十分。COMPLIANCE は法的根拠(FINRA / SEC など)があるときだけ。

RestrictToVpceaws:ViaAWSService 例外なしで書く

aws:SourceVpce の Deny だけを書くと、S3 のレプリケーション・インベントリ・ストレージクラス遷移などの内部処理が VPCe を経由しないために巻き込まれて停止します。本記事の例のように aws:ViaAWSService: false を併記して、S3 サービス自身からの呼び出しを除外するのが必須。「VPCe Deny を入れたらレプリカが急に止まった」はほぼこれが原因。

❌ MFA Delete を有効にしたまま運用を続ける

MFA Delete はルートアカウントの MFA でしか操作できない極めて厳しい設定で、一度入れると CI からの operate が困難になります。本当に必要なケース(規制対応など)以外では、Object Lock + バージョニングのほうが現実的。


まとめ:明日から手を付ける順番

全バケットを一気に直すのは現実的ではないので、着手順 を示します。

優先度 やること 効果
1 アカウント単位 BPA を有効化(公開バケット例外がなければ即時) 新規バケット作成時に公開設定不能になる
2 既存バケットに対し BucketOwnerEnforced の影響有無を棚卸し(CloudFront OAI 等) 移行ブロッカーの早期発見
3 棚卸し完了したものから BucketOwnerEnforced に切替 ACL 起因の事故を物理的に除去
4 暗号化を SSE-KMS + Bucket Key に揃える(コスト削減も兼ねる) 自社管理キー化、KMSコスト最大99%減
5 バケットポリシーに aws:SecureTransport / PrincipalOrgID を追加 HTTPS強制 + Confused Deputy 対策
6 IAM Access Analyzer を組織アナライザーで有効化 外部公開を継続的に検出
7 secure-bucket モジュールを社内標準化、新規バケットはモジュール経由のみ 設定漏れを構造的に防止

2026年の S3 セキュリティは「個別設定の積み上げ」ではなく、「ACL は完全に捨てて IAM/ポリシー1本に寄せる」「Block Public Access はアカウント単位で物理ON」という前提に立ち直すと、設定項目数自体を減らせます。少ない設定で強い守りを作るのが現代の S3 セキュリティです。

次回は 「RDS 運用の実践ガイド」 を書きます。本記事で扱った「保管中の暗号化」「ログ保護」をデータベース側に展開し、スナップショット運用とパラメータグループ管理まで踏み込みます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?