特定VPCだけからアクセス可能なS3を作る
Posted On 2025-02-03
ゲートウェイ型VPCエンドポイントを利用し、バケットポリシーで特定VPCのみS3にアクセスできる構成例を解説。誤設定によるアクセス不能リスクと、バケットポリシーの保険設定の重要性も書きました。
目次
はじめに
- 特定のVPCからのみアクセスできるS3を定義してみた
- ゲートウェイ型のVPCエンドポイントとS3のバケットポリシーの併用
- S3のバケットポリシーはミスるとルートアカウントでしか消せないのができるので注意が必要
アーキテクチャー
- 2個VPCを定義する
- プライベートサブネットにEC2をデプロイし、そこからS3にアクセスする。わかりやすいようにNATをつけておく
- プライベートサブネットにしているのは、Session Managerがやりやすいため。テスト時にSession Manager経由で接続する
- 各VPCにゲートウェイ型のVPCエンドポイントを追加し、ルートテーブルに紐づける。そこからS3への通信をオフロード
- S3のバケットポリシーで、VPC1のVPCエンドポイントからの接続のみ許可することで、VPC1からのみ接続できるバケットを実現
- ただし、バケットポリシーはミスるとアクセス不能なバケットができあがるため、保険としてIAMユーザーやルートユーザーは許可しておく(実験用の設定)
重要な注意
バケットポリシーは拒否の条件を間違えると消せないバケットができあがります。今回はVPCエンドポイントからしか許可していないため、それが消えてしまうとバケットポリシーも編集できずにルートユーザーから操作するしかなくなります。保険のためにルートユーザーが使えるアカウント(例:個人アカウント)でやるのをおすすめします
参考:アクセス不能になったS3バケットのバケットポリシーを削除する方法
今回はこの対策として、IAMユーザーは許可しておくというのをやっていますが、SSOの場合だと少し変わるので、この記事のように特定IPレンジ(例:自分のIP)を許可するは結構わかりやすいやり方かなと思います。
VPCエンドポイントの作り方
以前検証したこちらの記事を参照。S3の場合は、このようにGateway型のエンドポイントを定義してルートテーブルに追加すればOK。Gateway型はDynamoDBとS3で使えて無料。
VPCゲートウェイエンドポイントでS3への通信をオフロードする
resource "aws_vpc_endpoint" "s3_vpc1" {
vpc_id = module.vpc1.vpc_id
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
vpc_endpoint_type = "Gateway"
# ここではプライベートサブネットのルートテーブルに関連付け
route_table_ids = [module.vpc1.private_route_table_id]
}
バケットポリシー
- ホワイトリストをAllowで定義すると、EC2にS3FullAccessをもたせたときに貫通してしまうので、IAMポリシー同様に「Deny~NotEquals~」でホワイトリストを追加するのが確実
- ただ、ミスったときにバケットがアクセス不能になる(削除すら不能)なのは実験時にいろいろ面倒なので、IAMユーザーは許可してみた。ここをIP条件で許可するのも良いと思う。
- ただし、IAMポリシーと異なり、
:users/*
の書き方はできない。IAMユーザーを個別に追加する必要がある
- ただし、IAMポリシーと異なり、
- ちなみにどうしようもなくなったらルートユーザーでやればバケットポリシーは消せる。ただこれは最終手段。
data "aws_iam_policy_document" "s3_bucket_policy" {
statement {
sid = "DenyRequestsNotFromVPC1EndpointOrAllowedUser"
effect = "Deny"
# 保険としてIAMユーザーとルートユーザーは許可しておく。SSO経由の場合は違うので注意
not_principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/${var.your_iam_user_name}"
]
}
actions = [
"s3:*"
]
resources = [
aws_s3_bucket.dummy.arn,
"${aws_s3_bucket.dummy.arn}/*"
]
condition {
test = "StringNotEquals"
variable = "aws:sourceVpce"
values = [aws_vpc_endpoint.s3_vpc1.id]
}
}
}
resource "aws_s3_bucket_policy" "dummy_policy" {
bucket = aws_s3_bucket.dummy.bucket
policy = data.aws_iam_policy_document.s3_bucket_policy.json
}
結果
VPC1からアクセス→ファイル取得可能
[root@ip-10-0-5-167 ~]# aws s3 ls
2025-02-02 16:45:39 dummy-bucket-001fca5c5e5d
[root@ip-10-0-5-167 ~]# aws s3 ls s3://dummy-bucket-001fca5c5e5d
2025-02-02 16:40:35 17 dummy.txt
[root@ip-10-0-5-167 ~]# aws s3 cp s3://dummy-bucket-001fca5c5e5d/dummy.txt .
download: s3://dummy-bucket-001fca5c5e5d/dummy.txt to ./dummy.txt
[root@ip-10-0-5-167 ~]# cat dummy.txt
Hello dummy file![root@ip-10-0-5-167 ~]#
VPCからアクセス→バケットポリシーに拒否られてファイル取得不可能
ec2_s3access_role
はS3FullAccessを持たせているので、S3のバケットポリシーで拒否られている
[root@ip-10-1-5-190 ~]# aws s3 ls
2025-02-02 16:45:39 dummy-bucket-001fca5c5e5d
[root@ip-10-1-5-190 ~]# aws s3 ls s3://dummy-bucket-001fca5c5e5d
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: User: arn:aws:sts::123456789012:assumed-role/ec2_s3access_role/i-064cf8349ce31f6fc is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::dummy-bucket-001fca5c5e5d" with an explicit deny in a resource-based policy
マネジメントコンソールから確認
バケットポリシーはこうなっている
アップロードしたファイルはマネジメントコンソールからは見れる
全体コード
VPCの定義はこちらのリポジトリのmodules/vpcを利用
vpc_clusters.tf
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
##############################
# VPCモジュールのインスタンス #
##############################
module "vpc1" {
source = "./modules/vpc"
vpc_name = "vpc1"
vpc_cidr_block = "10.0.0.0/20"
# availability_zonesはmodules/vpc/variables.tfのdefaultが利用されます
}
module "vpc2" {
source = "./modules/vpc"
vpc_name = "vpc2"
vpc_cidr_block = "10.1.0.0/20"
}
######################################
# 各VPCに対するS3ゲートウェイエンドポイント #
######################################
resource "aws_vpc_endpoint" "s3_vpc1" {
vpc_id = module.vpc1.vpc_id
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
vpc_endpoint_type = "Gateway"
# ここではプライベートサブネットのルートテーブルに関連付け
route_table_ids = [module.vpc1.private_route_table_id]
}
resource "aws_vpc_endpoint" "s3_vpc2" {
vpc_id = module.vpc2.vpc_id
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [module.vpc2.private_route_table_id]
}
###############################
# S3バケットとダミーファイルアップロード #
###############################
# 一意のバケット名にするため、random_idを使用
resource "random_id" "bucket_id" {
byte_length = 6
}
resource "aws_s3_bucket" "dummy" {
bucket = "dummy-bucket-${random_id.bucket_id.hex}"
force_destroy = true
}
# ローカルに配置している "dummy.txt" ファイルをアップロード
resource "aws_s3_object" "dummy_file" {
bucket = aws_s3_bucket.dummy.bucket
key = "dummy.txt"
content = "Hello dummy file!"
}
#########################################################
# S3バケットポリシー: vpc1のS3エンドポイントからのアクセスのみ許可 #
#########################################################
data "aws_iam_policy_document" "s3_bucket_policy" {
statement {
sid = "DenyRequestsNotFromVPC1EndpointOrAllowedUser"
effect = "Deny"
# 保険としてIAMユーザーとルートユーザーは許可しておく。SSO経由の場合は違うので注意
not_principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/${var.your_iam_user_name}"
]
}
actions = [
"s3:*"
]
resources = [
aws_s3_bucket.dummy.arn,
"${aws_s3_bucket.dummy.arn}/*"
]
condition {
test = "StringNotEquals"
variable = "aws:sourceVpce"
values = [aws_vpc_endpoint.s3_vpc1.id]
}
}
}
resource "aws_s3_bucket_policy" "dummy_policy" {
bucket = aws_s3_bucket.dummy.bucket
policy = data.aws_iam_policy_document.s3_bucket_policy.json
}
ec2.tf
###############################################
# 共通のIAMロール/インスタンスプロファイル作成 #
###############################################
resource "aws_iam_role" "ec2_role" {
name = "ec2_s3access_role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = { Service = "ec2.amazonaws.com" },
Action = "sts:AssumeRole"
}]
})
}
# Session Manager用のマネージドポリシー
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# S3フルアクセスのマネージドポリシー
resource "aws_iam_role_policy_attachment" "s3" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
resource "aws_iam_instance_profile" "ec2_profile" {
name = "ec2_s3access_profile"
role = aws_iam_role.ec2_role.name
}
#############################################
# Security Group (SSH を使わず Session Manager 接続想定なのでインバウンドは最小限)
#############################################
resource "aws_security_group" "ec2_vpc1_sg" {
name = "ec2-vpc1-sg"
vpc_id = module.vpc1.vpc_id
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
resource "aws_security_group" "ec2_vpc2_sg" {
name = "ec2-vpc2-sg"
vpc_id = module.vpc2.vpc_id
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
#############################################
# EC2インスタンス作成 (各VPCのプライベートサブネット) #
#############################################
# Amazon Linux 2023 の最新AMIを取得
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["137112412989"] # AmazonのAMI所有者ID
filter {
name = "name"
# Amazon Linux 2023 AMIの名前パターン。minimumを除外する
values = ["al2023-ami-2023*-kernel-*-x86_64"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_instance" "ec2_vpc1" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = element(module.vpc1.private_subnet_ids, 0)
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
vpc_security_group_ids = [aws_security_group.ec2_vpc1_sg.id]
associate_public_ip_address = false
tags = {
Name = "ec2-vpc1"
}
}
resource "aws_instance" "ec2_vpc2" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = element(module.vpc2.private_subnet_ids, 0)
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
vpc_security_group_ids = [aws_security_group.ec2_vpc2_sg.id]
associate_public_ip_address = false
tags = {
Name = "ec2-vpc2"
}
}
所感
- 理屈的には簡単なんだけど、思ったよりバケットポリシーに対して神経をすり減らすなという印象
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー