S3バケットポリシーにおける別AWSアカウントからの許可を制限する

このブログシリーズ 「クラウドセキュリティ 実践集」 では、一般的なセキュリティ課題を取り上げ、「なぜ危険なのか?」 というリスクの解説から、「どうやって直すのか?」 という具体的な修復手順(コンソール、AWS CLI、Terraformなど)まで、分かりやすく解説します。

この記事では、Security Hubで検出された「[S3.6] S3 汎用バケットポリシーでは、他の AWS アカウントへのアクセスを制限する必要があります」というセキュリティ課題の修正方法について解説します。

ポリシーの説明

Amazon S3 の Security Hub コントロール – AWS Security Hub

このコントロールは、Amazon S3 汎用バケットポリシーが別の AWS アカウント からのプリンシパルが、S3 バケット内のリソースに対して拒否されたアクションの実行を防止するかどうかをチェックします。バケットポリシーで、別の AWS アカウントのプリンシパルに対して前のいずれかのアクションが許可されている場合、このコントロールは失敗します。

S3バケットポリシーは、バケットとそのオブジェクトへのアクセス権限を定義する強力なメカニズムです。しかし、汎用的なポリシーで他のAWSアカウント全体へのアクセスを許可してしまうと、本来アクセスを許可すべきでないアカウントからのアクセスを許してしまう可能性があります。これは、データ漏洩や不正操作のリスクを高めます。したがって、アクセス許可は、具体的なユースケースに基づいて、必要な特定のAWSアカウントまたはIAMプリンシパルに限定することが推奨されます。

修復方法

AWSコンソールでの修正手順

  1. Amazon S3 > バケット で対象バケットを選択
  2. アクセス許可 → バケットポリシーで編集をクリック
  3. BUCKET_NAMEに設定中のバケットのARNを入力し、変更の保存を選択
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowTrustedAccountRead",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
      "Action": ["s3:GetObject","s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::BUCKET_NAME",
        "arn:aws:s3:::BUCKET_NAME/*"
      ]
    }
  ]
}

Terraformでの修復手順

S3バケットポリシーのセキュアな設定のためのTerraformコードと、重要な修正ポイントを説明します。

# KMS key for S3 encryption
resource "aws_kms_key" "s3" {
  description             = "KMS key for S3 encryption"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "Enable IAM User Permissions"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      }
    ]
  })

  tags = var.tags
}

# S3 bucket
resource "aws_s3_bucket" "main" {
  bucket = var.bucket_name
  tags   = var.tags
}

# ★重要: バケットポリシー(セキュアな設定)
resource "aws_s3_bucket_policy" "main" {
  bucket = aws_s3_bucket.main.id

  # ★重要: 最小権限の原則に基づいたポリシー
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = concat([
      {
        Sid    = "DenyDangerousPermissions"
        Effect = "Deny"
        Principal = "*"
        Action = [
          "s3:DeleteBucketPolicy",
          "s3:PutBucketAcl",
          "s3:PutBucketPolicy",
          "s3:PutEncryptionConfiguration",
          "s3:PutObjectAcl"
        ]
        Resource = [
          aws_s3_bucket.main.arn,
          "${aws_s3_bucket.main.arn}/*"
        ]
        Condition = {
          StringNotEquals = {
            "aws:PrincipalAccount" = data.aws_caller_identity.current.account_id
          }
        }
      },
      {
        Sid    = "EnforceEncryption"
        Effect = "Deny"
        Principal = "*"
        Action = "s3:PutObject"
        Resource = "${aws_s3_bucket.main.arn}/*"
        Condition = {
          StringNotEquals = {
            "s3:x-amz-server-side-encryption" = "aws:kms"
          }
        }
      }
    ], var.trusted_account_permissions)
  })
}

# バケットの暗号化設定
resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
  bucket = aws_s3_bucket.main.id

  rule {
    apply_server_side_encryption_by_default {
      kms_master_key_id = aws_kms_key.s3.arn
      sse_algorithm     = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

# パブリックアクセスのブロック
resource "aws_s3_bucket_public_access_block" "main" {
  bucket = aws_s3_bucket.main.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# バージョニングの有効化
resource "aws_s3_bucket_versioning" "main" {
  bucket = aws_s3_bucket.main.id
  versioning_configuration {
    status = "Enabled"
  }
}

# アクセスログの設定
resource "aws_s3_bucket_logging" "main" {
  count = var.enable_logging ? 1 : 0

  bucket        = aws_s3_bucket.main.id
  target_bucket = var.log_bucket_id
  target_prefix = "logs/${var.bucket_name}/"
}

# CloudWatch アラーム
resource "aws_cloudwatch_metric_alarm" "policy_changes" {
  alarm_name          = "s3-policy-changes-${var.environment}"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name        = "PutBucketPolicy"
  namespace          = "AWS/S3"
  period             = "300"
  statistic          = "Sum"
  threshold          = "0"
  alarm_description  = "This metric monitors S3 bucket policy changes"
  alarm_actions      = var.alarm_sns_topic_arns

  dimensions = {
    BucketName = aws_s3_bucket.main.id
  }

  tags = var.tags
}

# Variables
variable "bucket_name" {
  description = "Name of the S3 bucket"
  type        = string
}

variable "environment" {
  description = "Environment name"
  type        = string
}

# ★重要: 信頼済みアカウントの権限設定
variable "trusted_account_permissions" {
  description = "List of policy statements for trusted accounts"
  type        = list(any)
  default     = []

  # 使用例:
  # trusted_account_permissions = [
  #   {
  #     Sid    = "AllowTrustedAccountRead"
  #     Effect = "Allow"
  #     Principal = {
  #       AWS = "arn:aws:iam::TRUSTED_ACCOUNT_ID:root"
  #     }
  #     Action   = ["s3:GetObject", "s3:ListBucket"]
  #     Resource = [
  #       aws_s3_bucket.main.arn,
  #       "${aws_s3_bucket.main.arn}/*"
  #     ]
  #   }
  # ]
}

variable "enable_logging" {
  description = "Enable S3 access logging"
  type        = bool
  default     = true
}

variable "log_bucket_id" {
  description = "ID of the logging bucket"
  type        = string
  default     = ""
}

variable "alarm_sns_topic_arns" {
  description = "List of SNS topic ARNs for CloudWatch alarms"
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "Tags for resources"
  type        = map(string)
  default     = {}
}

# Data sources
data "aws_caller_identity" "current" {}

主要な修正ポイントは以下の通りです:

  1. 危険な権限の明示的な拒否(最重要):
resource "aws_s3_bucket_policy" "main" {
  policy = jsonencode({
    Statement = [
      {
        Effect = "Deny"
        Action = [
          "s3:DeleteBucketPolicy",
          "s3:PutBucketAcl",
          # その他の危険な権限
        ]
        Condition = {
          StringNotEquals = {
            "aws:PrincipalAccount" = data.aws_caller_identity.current.account_id
          }
        }
      }
    ]
  })
}

信頼済みアカウントの権限管理:

variable "trusted_account_permissions" {
  # 最小権限の原則に基づいた権限設定
  type = list(any)
}

追加のセキュリティ設定:

# 暗号化の強制
resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

# パブリックアクセスのブロック
resource "aws_s3_bucket_public_access_block" "main" {
  block_public_acls = true
  block_public_policy = true
}

この設定により:

  • 危険な権限の明示的な拒否
  • 必要最小限の権限付与
  • 暗号化の強制
  • アクセスの監視とログ記録

が実現されます。

注意:

  • 既存のポリシーの移行には慎重な計画が必要
  • アプリケーションの動作確認が必要
  • 信頼済みアカウントの定期的な見直しを推奨

最後に

今回は、S3バケットポリシーで他のAWSアカウントへのアクセスを制限する方法についてご紹介しました。S3バケットは様々なデータを保存するため、適切なアクセス制御は非常に重要です。汎用的なアクセス許可は避け、必要な特定のプリンシパルにのみアクセス権限を付与するように心がけてください。

この問題の検出は弊社が提供するSecurifyのCSPM機能で簡単に検出及び管理する事が可能です。

運用が非常に楽に出来る製品になっていますので、ぜひ興味がある方はお問い合わせお待ちしております。

最後までお読みいただきありがとうございました。この記事が皆さんの役に立てば幸いです

この記事をシェアする

クラウドセキュリティ対策実践集一覧へ戻る

貴社の利用状況に合わせた見積もりを作成します。

料金プランを詳しく見る