OpenPolicyAgentでS3のバケットを作成するときのポリシーチェックをしてみた

今回はS3のバケットへのアクセス制限の設定についてポリシーチェックをしていこうと思います。

環境

  • OS: CentOS 7
  • Terraform : 0.12.26
  • opa: Version: 0.21.0

S3のポリシーチェックで必要なもの

最終的に出来上がるものです。

f:id:bakotako:20200628202542p:plain

EC2からVPC Endpointを通り、Access PointからS3へアクセスするようにします。 S3へのアクセス制限をする際に設定する内容は下記のものを想定しています。

  • EC2に割り当てるIAMロールのIAMポリシーにはS3のアクセスのみ許可
  • S3のAccess PointはVPCのみ許可
  • S3のオブジェクトへはAccess Point 経由でのみアクセスを許可

ポリシーという単語をOpen Policy Agent のRegoで設定するポリシーとIAMポリシー、バケットポリシーいろんな意味で出てきてしまうため、単に「ポリシー」とあるものはOpen Policy Agentでチェックするポリシーとします。

また、今回はTerraformのtfファイルからjsonファイルへ返還する方法は特に記述せずに進めていきます。変換方法や返還後のjsonフォーマットが気になる方は下記を参考にしてみてください。

kobatako.hatenablog.com

www.terraform.io

では、それぞれのポリシーの実装についてみていきましょう。

EC2に割り当てるIAM Role

EC2に割り当てられているIAMロールのIAMポリシーがS3へのアクセスを許可しているかチェックをしていきます。 まず、TerraformでEC2にIAMプロファイルを指定します。

resource "aws_instance" "sample" {
・・・
  iam_instance_profile = aws_iam_instance_profile.sample_profile.name
・・・
}

そしてIAMプロファイルとIAMロール、IAMポリシーを作成します。

resource "aws_iam_instance_profile" "sample_profile" {
  name = "SampleEC2Role"
  role = aws_iam_role.sample_role.name
}


resource "aws_iam_role" "sample_role" {
  name = "SampleEC2Role"

  assume_role_policy = data.aws_iam_policy_document.ec2_role.json
}

resource "aws_iam_role_policy" "ec2_role_policy" {
  name = "SampleEC2RolePolicy"
  role = aws_iam_role.sample_role.id

  policy = data.aws_iam_policy_document.ec2_role_policy.json
}

data "aws_iam_policy_document" "ec2_role_policy" {
  statement {
    effect = "Allow"
    actions = [
      "s3:*"
    ]
    resources = [
      "*"
    ]
  }
}

IAMロールに割り当てるIAMポリシーはすべてのS3に対して、すべてのアクションを許可するようにしています。 IAMポリシーの設定方法はjsonファイルとして記述することもできますが、Open Policy Agentでチェックする場合はjsonではなくHCLで記述したほうが都合がいいためHCLで記述します。

では、このIAMポリシーの設定をチェックするポリシーを見ていきましょう。

valid_ec2_iam_role {
  resources := ec2_role_policy_data
  count({resource| 
    resources[resource]; ec2_iam_role_policy_s3_filter(resource)
  }) == count(resources)
}

ec2_iam_role_policy_s3_filter(resource) {
  statement := resource.expressions.statement[_]
  value := statement.actions.constant_value[_]
  re_match("s3:.*", value)
  statement.effect.constant_value == "Allow"
}

valid_ec2_iam_role のRuleでEC2に割り当てるIAMロールのIAMポリシーをチェックをしています。

まず、 ec2_role_policy_data でEC2に割り当てられているIAMロールのIAMポリシーを取得しています。そして ec2_iam_role_policy_s3_filter 関数でS3へのアクセスが許可されているIAMポリシーをフィルターしています。

statement := resource.expressions.statement[_] でIAMポリシーのステートメントをとりだし value := statement.actions.constant_value[_] でactions の値を取得しています。 re_match("s3:.*", value) では正規表現で "s3:.*" の正規表現に当てはまっているのかをチェックしてし、 statement.effect.constant_value == "Allow"ステートメントが許可する設定となっているかをチェックしています。

フィルターし、得られたIAMポリシーの数とEC2に割り当てられているIAMロールのIAMポリシーの数が等しい場合、すべてのIAMポリシーが条件に当てはまっている状態となります。

ec2_role_policy_data の実装方法は下記のようになっています。

ec2_iam_policy_data[resource] {
  policy := ec2_iam_role_policies[_]
  policy_data := policy.expressions.policy.references[_]
  resource := fetch_address_resource[policy_data]
}

ec2_iam_role_policies[resource] {
  role_address := ec2_role_addresses
  resource := data.configuration.root_module.resources[_]
  resource.expressions.role.references[_] == role_address[_]
  resource.type == "aws_iam_role_policy"
}

ec2_role_addresses[resource] {
  profiles := instances[_].expressions.iam_instance_profile.references[_]
  profile_resource := fetch_address_resource[profiles]
  resource := profile_resource.expressions.role.references[_]
}

fetch_address_resource[address] = resource {
  resource := data.configuration.root_module.resources[_]
  address := resource.address
}

instances[resource] {
  resource := data.configuration.root_module.resources[_]
  resource.type == "aws_instance"
}

長くなってしまうため、細かい説明は省略します。 処理としては、EC2インスタンスの一覧を取得し、EC2に割り当てられているIAMプロファイルを取得。IAMプロファイルからIAMロールを取得し、IAMポリシーを取得する流れとなっています。

作成するS3のAccess Point

それでは作成するS3のAccess Pointに対するポリシーチェックをしていきます。 作成するS3とAccess Pointは下記のものとなります。

resource "aws_s3_bucket" "sample" {
  bucket = "sample-bucket"
  acl = "private"
}

resource "aws_s3_access_point" "sample-access-point" {
  bucket = aws_s3_bucket.sample.id
  name = "sample-access-point"

  vpc_configuration {
    vpc_id = aws_vpc.main.id
  }
}

sample-bucket バケットAccess Pointを作成するTerraformになります。Access Point には vpc_configuration を指定し vpc_idVPCに対する作成を行います。

では、実際にS3のAccess PointがすべてVPCのみとなっているかチェックをしていきます。

ポリシーチェックの実装は下記のようになっています。

valid_s3_vpc_access_point {
  s3_access_points := s3_access_point
  count({resource|
    s3_access_points[resource]; count(resource.expressions.vpc_configuration) > 0
  }) == count(s3_access_points) 
}

s3_access_point[resource] {
  s3_address := create_s3_addresses[_]
  resource := reference_s3_resources[s3_address]

  resource.type == "aws_s3_access_point"
}

create_s3_addresses[address] {
  some i
  data.resource_changes[i].type == "aws_s3_bucket"
  data.resource_changes[i].change.actions[_] == "create"
  address := data.resource_changes[i].address
}

reference_s3_resources[s3_address] = resource {
  some i
  s3_address := data.configuration.root_module.resources[i].expressions.bucket.references[_]
  resource := data.configuration.root_module.resources[i]
}

順番に見ていきましょう。

valid_s3_vpc_access_point のRuleでS3のAccess Pointのチェックをしています。チェックするAccess Pointは s3_access_point で取得しています。

s3_access_point内では create_s3_addresses でTerraformで作成するS3のaddressを取得します。 取得したS3のaddressをもとに reference_s3_resources で S3のaddressが関連付けられたリソースを取得します。

そして、S3に関連付けられたリソースで aws_s3_access_point のものを返すようにすることでS3のAccess Pointを取得することができます。

取得したS3のAccess PointですべてのAccess Pointがvpc_configurationを所有しているかどうかをチェックすることですべてのAccess PointがVPCに制限されたものかどうかを判定しています。

S3へのアクセス制限

最後に、S3へのアクセスをAccess Pointのみ許可するようにします。

まず、Terraformで指定するバケットポリシーの内容を見ていきましょう。

resource "aws_s3_bucket" "sample" {
  bucket = "sample-bucket"
  acl = "private"
}

resource "aws_s3_access_point" "sample-access-point" {
  bucket = aws_s3_bucket.sample.id
  name = "main-access-point"

  vpc_configuration {
    vpc_id = aws_vpc.main.id
  }
}

data "aws_iam_policy_document" "s3-bucket-policy" {
  statement {
    effect = "Deny"
    actions = [
        "s3:*Object"
    ]
    resources = [
      "${aws_s3_bucket.sample.arn}/*",
      aws_s3_bucket.sample.arn
    ]
    principals {
      type = "AWS"
      identifiers = ["*"]
    }

    condition {
      test = "StringNotEquals"
      variable = "s3:DataAccessPointArn"
      values = [
        aws_s3_access_point.sample-access-point.arn
      ]
    }
  }
}

resource "aws_s3_bucket_policy" "s3-main-policy" {
  bucket = aws_s3_bucket.sample.id
  policy = data.aws_iam_policy_document.s3-bucket-policy.json
}

バケットポリシーもHCLの表記で記述していきます。 conditionの設定で sample-access-point Access Point 以外からのアクセスに対して sample-bucket バケットのオブジェクトへのアクションは拒否する設定となっています。

これで、S3のオブジェクトへのアクセスはAccess Pointを通してのアクセスのみに制限されました。

ではこの設定がされているのかをチェックするポリシーを見ていきます。

valid_s3_bucket_policy {
  policy := s3_bucket_policies[_]
  access_point := s3_access_point
  statement := policy.expressions.statement[_]
  statement.effect.constant_value == "Deny"
  re_match("s3:.*", statement.actions.constant_value[_])
  statement.condition[_].test.constant_value == "StringNotEquals"
  statement.condition[_].variable.constant_value == "s3:DataAccessPointArn"
  statement.condition[_].values.references[_] == access_point[_].address
}

s3_bucket_policies[policy] {
  s3_address := create_s3_addresses
  resource := data.configuration.root_module.resources[_]
  resource.type == "aws_s3_bucket_policy"
  resource.expressions.bucket.references[_] == s3_address[_]
  policy := fetch_address_resource[resource.expressions.policy.references[_]]
}

s3_access_pointcreate_s3_addresses は 作成するS3のAccess Point のところで出てきたものを使っています。

valid_s3_bucket_policyバケットポリシーが想定されているものかどうかをチェックしています。 対象となる、バケットポリシーは s3_bucket_policies で取得しています。

valid_s3_bucket_policy の中を細かく見ていきましょう。

まず、 s3_bucket_policiess3_access_pointバケットポリシーとAccess Pointを取得します。 そしてバケットポリシーのステートメントを取得し、条件に当てはまるかをチェックしていきます。

これで S3のバケットポリシーのチェックができました。

まとめ

EC2に割り当てられてるIAMロールからIAMポリシーのチェックとS3のバケットポリシーのチェックをしていきました。 IAMポリシーとバケットポリシーのチェックをする際に、jsonファイルとして別ファイルなどにせずにHCLの表記にすることでOpen Policy Agent でチェックすることができます。aws_iam_policy_document についても今回初めて知り使ってみましたが個人的な意見としてはjsonよりも使いやすかったので今後は aws_iam_policy_document で書いていこうかと思います。

実装内容としては今回の実装ですとS3のバケットする際にすべてのバケットに対してポリシーチェックをするため、グローバルアクセスするものがある場合は処理を分ける必要があります。用途の異なるS3のバケットを構築する際のポリシーチェックについては別の記事で書いていこうかと思います。

Access Pointへアクセスできるものを制御したいとかある場合はアクセスポイントポリシーも設定することができるのでより厳密に制御することが可能となります。気になる方はぜひやってみてください。