[OpenPolicyAgent] Terraformで構築するEC2のSecurity Groupをチェックする

EC2を構築する際にSecurity Groupを設定することがあると思いますが、その際に想定通りの設定になっているのかを気を付ける必要がありますね。

想定通りの設定になっているのか、Open Policy Agentで確認することができるのでその実装方法についてみていきたいと思います。

(まだまだ、勉強中ですので、ご意見あればコメントいただけますと幸いです!)

動作環境

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

Terraformの内容

今回対象となるTerraformの内容は下記になります。

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Env = terraform.workspace
  }
}

resource "aws_subnet" "frontend_subnet" {
  vpc_id = aws_vpc.main.id
  cidr_block = "10.0.0.0/24"

  tags = {
    Env = terraform.workspace
  }
}

resource "aws_security_group" "http" {
  name = "http"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "ssh" {
  name = "ssh"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = ["1.2.3.4/32"]
  }
}
resource "aws_instance" "web" {
  ami           = "ami-0717724173e4d9989"
  instance_type = "t3.micro"

  subnet_id = aws_subnet.frontend_subnet.id

  tags = {
    Env = terraform.workspace
    Role = "web"
  }

  vpc_security_group_ids = [
    "${aws_security_group.http.id}",
    "${aws_security_group.ssh.id}"
  ]

  root_block_device {
    volume_type = "gp2"
    volume_size = "20"
  }
}

webサーバとしてEC2を構築します。Security Group としてはhttp と ssh へのリクエストを許可し、 ssh については「1.2.3.4/32」のIPからのみ許可しています。

Terraformのファイルの準備ができましたので、下記のコマンドでjson形式に変換してみようと思います。

terraform plan --out tfplan.binary
terraform show -json tfplan.binary > tfplan.json

jsonへ返還後の内容でEC2のSecurity Groupのチェックに必要な部分を下記に抜粋しております。

{
  ・・・
  "planned_values": {
    "root_module": {
      "resources": [
        {
          "address": "aws_instance.web",
          "mode": "managed",
          "type": "aws_instance",
          "name": "web",
          "provider_name": "aws",
          "schema_version": 1,
          "values": {
            "ami": "ami-0717724173e4d9989",
            "credit_specification": [],
            "disable_api_termination": null,
            "ebs_optimized": null,
            "get_password_data": false,
            "hibernation": null,
            "iam_instance_profile": null,
            "instance_initiated_shutdown_behavior": null,
            "instance_type": "t3.micro",
            "monitoring": null,
            "root_block_device": [
              {
                "delete_on_termination": true,
                "volume_size": 20,
                "volume_type": "gp2"
              }
            ],
            "source_dest_check": true,
            "tags": {
              "Env": "stagging",
              "Role": "web"
            },
            "timeouts": null,
            "user_data": null,
            "user_data_base64": null
          }
        },
        {
          "address": "aws_security_group.http",
          "mode": "managed",
          "type": "aws_security_group",
          "name": "http",
          "provider_name": "aws",
          "schema_version": 1,
          "values": {
            "description": "Managed by Terraform",
            "ingress": [
              {
                "cidr_blocks": [
                  "0.0.0.0/0"
                ],
                "description": "",
                "from_port": 80,
                "ipv6_cidr_blocks": [],
                "prefix_list_ids": [],
                "protocol": "tcp",
                "security_groups": [],
                "self": false,
                "to_port": 80
              }
            ],
            "name": "web",
            "name_prefix": null,
            "revoke_rules_on_delete": false,
            "tags": null,
            "timeouts": null
          }
        },
        {
          "address": "aws_security_group.ssh",
          "mode": "managed",
          "type": "aws_security_group",
          "name": "ssh",
          "provider_name": "aws",
          "schema_version": 1,
          "values": {
            "description": "Managed by Terraform",
            "ingress": [
              {
                "cidr_blocks": [
                  "1.2.3.4/32"
                ],
                "description": "",
                "from_port": 22,
                "ipv6_cidr_blocks": [],
                "prefix_list_ids": [],
                "protocol": "tcp",
                "security_groups": [],
                "self": false,
                "to_port": 22
              }
            ],
            "name": "ssh",
            "name_prefix": null,
            "revoke_rules_on_delete": false,
            "tags": null,
            "timeouts": null
          }
        }
      ]
    }
  }
  ・・・
  "configuration": {
    "provider_config": {
    ・・・
    },
    "root_module": {
      "resources": [
        {
          "address": "aws_instance.web",
          "mode": "managed",
          "type": "aws_instance",
          "name": "web",
          "provider_config_key": "aws",
          "expressions": {
            ・・・
            "vpc_security_group_ids": {
              "references": [
                "aws_security_group.http",
                "aws_security_group.ssh"
              ]
            }
          },
          "schema_version": 1
        }
      ]
    }
  }
}

configuration > root_module > resources の項目に構築するEC2とそれに関連するSecurity Groupの address 一覧が vpc_security_group_ids 内に記載されています。 planned_values > root_module > resources の項目にはリソースの値が入っています。 ですので、EC2に関連するSecurity Groupの address を取得し、その address をもとにSecurity Groupにどのような設定がされているのかをチェックするように実装していきます。

それぞれの内容がどういうものなのか気になる方はTerraformの公式にフォーマットの情報が記載していますので、そちらをご確認ください。

www.terraform.io

ポリシーチェック

どのようにEC2に関連しているSecurity Groupのチェックをするのかを軽く述べましたので、実装部分を見ていきたいと思います。 まず、Security Groupの設定として下記の内容をチェックしていきます。

  • SSHのリクエストは「1.2.3.4/32」のアドレスからのみ許可する ※1.2.3.4のアドレスは架空のものとなります。
  • HTTP、HTTPSのリクエストを許可する
  • 上記内容以外のSecurity Groupの設定はエラーとする

EC2のSecurity Group

初めにEC2に関連付けられているSecurity Groupを取得していきます。

web_instances[instance] {
  instance := data.planned_values.root_module.resources[_]
  instance.type == "aws_instance"
  instance.values.tags.Role == "web"
}

web_instance_security_groups[resource] {
  instances := web_instances
  resource := data.planned_values.root_module.resources[_]
  data.configuration.root_module.resources[_].address == instances[_].address
  resource.address == data.configuration.root_module.resources[_].expressions.vpc_security_group_ids.references[_]
  resource.type == "aws_security_group"
}

web_instances の RuleでTerraformで構築するEC2で「Role」タグにweb と設定されているEC2を取得します。 そして下記の部分でEC2のaddressと等しい configuration 内の リソース かどうかを比較しています。同じaddress の場合、そのリソースの vpc_security_group_ids に定義されているSecurity Group の addressと等しい リソースかどうかを比較しています。

data.configuration.root_module.resources[_].address == instances[_].address
resource.address == data.configuration.root_module.resources[_].expressions.vpc_security_group_ids.references[_]

これで、EC2に関連付けられたSecurity Groupが取得することができました。 では実際にSecurity Groupのチェックを行っていきます。

Security Groupのポリシーチェック

EC2に関連するSecurity Group が取得できたので次はその設定がポリシー通りになっているのかをチェックします。 全体の実装内容は下記のようになります。

permit_ssh_ips = ["1.2.3.4/32"]

default valid_web_instance_security_group = false
valid_web_instance_security_group {
  valid_ssh_instance_security_group
  valid_http_instance_security_group
  valid_other_instance_security_group
}

valid_other_instance_security_group {
  count(web_instance_security_groups) == count(http_security_groups) + count(https_security_groups) + count(ssh_security_groups)
}

default valid_http_instance_security_group = false
valid_http_instance_security_group {
  count(http_security_groups) > 0
}

valid_http_instance_security_group {
  count(https_security_groups) > 0
}

default valid_ssh_instance_security_group = false
valid_ssh_instance_security_group {
  security_groups := ssh_security_groups
  count({security_group|
    security_groups[security_group]; security_group.values.ingress[_].cidr_blocks == permit_ssh_ips
  }) == count(security_groups)
}

ssh_security_groups[security_group] {
  security_groups := web_instance_security_groups
  security_group := security_groups[_]
  security_group.values.ingress[_].from_port == 22
  security_group.values.ingress[_].to_port == 22
}

http_security_groups[security_group] {
  security_groups := web_instance_security_groups
  security_group := security_groups[_]
  security_group.values.ingress[_].from_port == 80
  security_group.values.ingress[_].to_port == 80
}

https_security_groups[security_group] {
  security_groups := web_instance_security_groups
  security_group := security_groups[_]
  security_group.values.ingress[_].from_port == 443
  security_group.values.ingress[_].to_port == 443
}

valid_web_instance_security_group の RuleでSecurity Groupのポリシーをチェックする各Ruleを読んでいっています。 上から順にみていきたいと思います。 まず、 valid_ssh_instance_security_group のRuleを見ていきましょう。

SSHのリクエストは「1.2.3.4/32」のアドレスからのみ許可する

valid_ssh_instance_security_group の Ruleでsshに関するSecurity Groupのポリシーをチェックしています。関連する実装部分は下記になります。

permit_ssh_ips = ["1.2.3.4/32"]

default valid_ssh_instance_security_group = false
valid_ssh_instance_security_group {
  security_groups := ssh_security_groups
  count({security_group|
    security_groups[security_group]; security_group.values.ingress[_].cidr_blocks == permit_ssh_ips
  }) == count(security_groups)
}

ssh_security_groups[security_group] {
  security_groups := web_instance_security_groups
  security_group := security_groups[_]
  security_group.values.ingress[_].from_port == 22
  security_group.values.ingress[_].to_port == 22
}

valid_ssh_instance_security_group ではsshのSecurity Groupのポリシーチェックをしています。 最初にssh_security_groups の RuleでEC2に関連付けられたSecurity Groupでポートが22番(ssh)のSecurity Groupのみ取得しています。

そして、下記で接続ものとIPアドレス帯もチェック項目となります。

  count({security_group|
    security_groups[security_group]; security_group.values.ingress[_].cidr_blocks == permit_ssh_ips
  }) == count(security_groups)

permit_ssh_ips には許可するIPアドレスの配列が記載されており、別の値が入っていた場合、count(security_groups) の数より少なくなってしまうため、falseとなる仕組みになります。 これで、EC2に関連するSecurity Groupでsshに関するポリシーチェックは完了しました。

次にhttp(https)のSecurity Groupについてみていきます。

HTTP、HTTPSのリクエストを許可する

httpとhttpsでは両方設定されている場合もありますが、どちらか片方のみしか設定されていない場合もあります。ですのでhttp or httpsの実装がある、というチェックを行う必要があります。では実装を見ていきましょう。

default valid_http_instance_security_group = false
valid_http_instance_security_group {
  count(http_security_groups) > 0
}

valid_http_instance_security_group {
  count(https_security_groups) > 0
}

http_security_groups[security_group] {
  security_groups := web_instance_security_groups
  security_group := security_groups[_]
  security_group.values.ingress[_].from_port == 80
  security_group.values.ingress[_].to_port == 80
}

https_security_groups[security_group] {
  security_groups := web_instance_security_groups
  security_group := security_groups[_]
  security_group.values.ingress[_].from_port == 443
  security_group.values.ingress[_].to_port == 443
}

httpとhttpsで同名のRuleとして定義しています。これは valid_http_instance_security_group の Rule でどちらかのRule が true になったら true になります。ですので、http か https のSecurity Group がある場合、 valid_http_instance_security_group は true となります。

上記内容以外のSecurity Groupの設定はエラーとする

最後に、http(https)とssh 以外のSecurity Group が設定されていた場合、falseになるように Rule を追加します。

valid_other_instance_security_group {
  count(web_instance_security_groups) == count(http_security_groups) + count(https_security_groups) + count(ssh_security_groups)
}

count(web_instance_security_groups) で EC2に関連しているSecurity Groupの数を取得し、 count(http_security_groups) + count(https_security_groups) + count(ssh_security_groups) で http(https)とsshのSecurity Group の数を取得し同数になっているかを比較しています。同数になっている場合は別のSecurity Group が設定されていないため true となります。

これで、不要なSecurity Group が設定されていないかチェックすることができました。

まとめ

EC2に関連するSecurity Groupの設定からポリシーをチェックする方法を見てみました。リソース間の関連付けについては configuration 内のリソースに定義されており、そこから planned_values の値のポリシーをチェックしています。Regoの表記で configuration のリソース内の情報から planned_values 内のリソースへの関連付けを記述することができるので、割とサクッと実装することができました。 各 Security Groupを取得しているとことですが、愚直に http, https, sshを別々に定義していますが、ここは下記のようにまとめることができます。

security_groups[port] = security_group {
  some port
  ports[port]
  security_groups := web_instance_security_groups
  security_group := security_groups[_]
  security_group.values.ingress[_].from_port == port
  security_group.values.ingress[_].to_port == port
}

> security_groups[80]

こうすることで、 security_groups の Ruleに渡したポート番号のSecurity Groupの一覧を取得することができます。 Regoの表記もまだまだ触ったことがないものなどありますので、別の機会に見ていきたいと思います。 また、今回はwebサーバに直接リクエストが行くようになっていますが、ALBを用いた場合のポリシーチェックなどをもしていきたいと思います。