「Aqua Container Security Platform」を立ててみた(本番環境用編)

はじめに

おはようございます、加藤です。「Aqua Container Security Platform」の環境を本番運用を想定して構築してみました。
Amazon Elastic Container Service (ECS)
上記のドキュメントを参考にしていますが、お読み頂ければわかるとおり結構弄っています。例えば紹介されている手順ではRDBをECS上で立ち上げていますが、RDSに変更したりなどですね。
Aqua自体の説明は本ブログでは行いません、下記のブログをご覧ください。

コンテナセキュリティの決定版「Aqua Container Security Platform」を試してみた(インストール〜イメージスキャン編)

構成図

上図の様にECS(on EC2)で構築します。ドキュメントを確認するとホストOSのdocker.lockをマウントする必要があるので、Fargateは使用できません。
また、aqua-serveraqua-gatewayの2種類のコンテナを1つのタスク定義でまとめて定義し、Replica: 1で立ち上げます。つまり冗長化は行っていません。
Aquaが止まったからといってサービス継続に影響はでないだろうと想定の元に非冗長化で構成しています。冗長化を行いたい場合は、下記ドキュメントが参考になりそうです。冗長化する場合はAct-Sby構成になる様です。
Aqua Server Failover (Cluster Mode)

やってみた

大部分はTerraformを使って構築しました。Secrets ManagerとRoute53はCLIとマネジメントコンソールで設定しています。

Secrets Managerへaquaコンテナリポジトリの認証情報の格納

aquaコンテナリポジトリの認証情報をSecrets Managerに格納します。--kms-key-idを指定していないので暗号化にはAWSアカウントのデフォルトCMKのaws/secretsmanagerが使用されます。
Username, Password は Aqua Security にログインする際に使用するものです。システムで使用する専用のユーザーを作成しておくと良いでしょう。

AQUA_USERNAME="<<YOUR_AQUA_USERNAME>>"
AQUA_PASSWORD="<<YOUR_AQUA_PASSWORD>>"
aws secretsmanager create-secret \
    --name "aqua/container_repository" \
    --description "aqua container repository" \
    --secret-string "{\"username\":\"${AQUA_USERNAME}\",\"password\":\"${AQUA_PASSWORD}\"}"

ADMIN_PASSWORD="<<YOUR_ADMIN_PASSWORD>>"
aws secretsmanager create-secret \
    --name "aqua/admin_password" \
    --description "aqua administrator password" \
    --secret-string "${ADMIN_PASSWORD}"

ADMIN_PASSWORD="<<YOUR_DB_PASSWORD>>"
aws secretsmanager create-secret \
    --name "aqua/db_password" \
    --description "aqua database password" \
    --secret-string "${DB_PASSWORD}"

他リソースの作成

kmd2kmd/aqua_csp-on-aws_ecs

上記のGitHubに使用したTerraformの構成ファイルをアップしています。
HTTPS前提の構成になっています。ACMに証明書を登録しARNをメモしておいてください。またECS用のEC2インスタンスのSSH鍵も事前に作成し名前をメモしておいてください。

git clone git@github.com:kmd2kmd/aqua_csp-on-aws_ecs.git
cd aqua_csp-on-aws_ecs
terraform init
terraform apply \
  -var ssh-key_name="<<YOUR_KEY_NAME>>" \
  -var ssl_certificate_id="<<YOUR_ACM_ARN>>"

Terraform AWS modulesで公開されている物を積極的に使って構築していきます。
変数定義など正直まだまだ酷い所が多いですが、あくまで構成の参考としてご使用頂ければ...

NatGateway無しの2層構成でVPCを作成しています。

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "${var.project}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  public_subnets  = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.3.0/24", "10.0.4.0/24", "10.0.5.0/24"]

  enable_nat_gateway = false

  tags = {
    Terraform = "true"
  }
}

ALBはHTTPでアクセスがあった場合、HTTPSへリダイレクトするように設定しています。

#################################################
# Load Balancer
#################################################
resource "aws_lb" "lb" {
  name                       = "${var.project}-alb"
  internal                   = false
  load_balancer_type         = "application"
  security_groups            = ["${aws_security_group.alb.id}"]
  subnets                    = ["${module.vpc.public_subnets}"]
  enable_deletion_protection = false

  tags = {
    Project   = "${var.project}"
    Name      = "${var.project}-alb"
    Terraform = "true"
  }
}

#################################################
# Listener
#################################################
resource "aws_lb_listener" "redirect" {
  load_balancer_arn = "${aws_lb.lb.arn}"
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

resource "aws_lb_listener" "front_end" {
  load_balancer_arn = "${aws_lb.lb.arn}"
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"
  certificate_arn   = "${var.ssl_certificate_id}"

  default_action {
    type             = "forward"
    target_group_arn = "${aws_lb_target_group.lb.arn}"
  }
}
#################################################
# Target Group
#################################################
resource "aws_lb_target_group" "lb" {
  name     = "${var.project}-lb-tg"
  port     = "${var.aqua_server_port}"
  protocol = "HTTP"
  vpc_id   = "${module.vpc.vpc_id}"
}

最新のECS Optimized AMIのIDのを取得してAuto Scaling Groupを作成します。インスタンスはm5.large 1つで十分でした。
必要に応じて、UserdataでSSM Agentを入れるとSession Managerで管理が可能になり、運用が楽になると思います。

data "aws_ami" "ecs-optimized" {
  most_recent = true

  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn-ami-*-amazon-ecs-optimized"]
  }

  owners = ["591542846629"]
}

data "template_file" "userdata" {
  template = "${file("userdata/userdata.tmpl.sh")}"

  vars {
    cluster_name = "${var.project}-cluster"
  }
}

module "asg" {
  source = "terraform-aws-modules/autoscaling/aws"

  name = "service"

  # Launch configuration
  lc_name = "${var.project}-ecs-lc"

  image_id        = "${data.aws_ami.ecs-optimized.image_id}"
  instance_type   = "${var.instance_type}"
  security_groups = ["${aws_security_group.ec2.id}"]

  root_block_device = [
    {
      volume_size = "50"
      volume_type = "gp2"
    },
  ]

  # Auto scaling group
  asg_name                  = "${var.project}-ecs-asg"
  vpc_zone_identifier       = "${module.vpc.public_subnets}"
  health_check_type         = "EC2"
  min_size                  = 0
  max_size                  = 1
  desired_capacity          = 1
  wait_for_capacity_timeout = 0
  user_data                 = "${data.template_file.userdata.rendered}"
  ebs_optimized             = true
  iam_instance_profile      = "${aws_iam_instance_profile.instance_profile-ecs_instance.name}"
  key_name                  = "${var.ssh-key_name}"

  tags = [
    {
      key                 = "Name"
      value               = "${var.project}-ecs-asg-instance"
      propagate_at_launch = true
    },
    {
      key                 = "Project"
      value               = "${var.project}"
      propagate_at_launch = true
    },
    {
      key                 = "Terraform"
      value               = "true"
      propagate_at_launch = true
    },
  ]
}

Secrets Managerの情報を取り込みます。IDや値はECSの定義やRDSのパスワード設定で使用しています。

data "aws_secretsmanager_secret" "container_repository" {
  name = "${var.secretsmanager_container_repository}"
}

data "aws_secretsmanager_secret" "admin_password" {
  name = "${var.secretsmanager_admin_password}"
}

data "aws_secretsmanager_secret" "db_password" {
  name = "${var.secretsmanager_db_password}"
}

data "aws_secretsmanager_secret_version" "container_repository" {
  secret_id = "${data.aws_secretsmanager_secret.container_repository.id}"
}

data "aws_secretsmanager_secret_version" "admin_password" {
  secret_id = "${data.aws_secretsmanager_secret.admin_password.id}"
}

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "${data.aws_secretsmanager_secret.db_password.id}"
}

RDSはPostgreSQL9.5で構築しています。後から気づきましたが、9.6にも対応しているようです。

module "db" {
  source            = "terraform-aws-modules/rds/aws"
  identifier        = "${var.project}-rds"
  engine            = "postgres"
  engine_version    = "9.5.14"
  instance_class    = "${var.db_instance_type}"
  allocated_storage = 30

  name                       = "${var.project}"
  username                   = "${var.postgres_username}"
  password                   = "${data.aws_secretsmanager_secret_version.db_password.secret_string}"
  port                       = "${var.postgres_port}"
  vpc_security_group_ids     = ["${aws_security_group.rds.id}"]
  maintenance_window         = "Fri:17:00-Fri:17:30"
  backup_window              = "16:00-16:30"
  monitoring_interval        = "30"
  monitoring_role_name       = "${var.project}_monitoring_role"
  create_monitoring_role     = true
  subnet_ids                 = "${module.vpc.private_subnets}"
  family                     = "postgres9.5"
  major_engine_version       = "9.5"
  final_snapshot_identifier  = "aqua"
  deletion_protection        = false
  auto_minor_version_upgrade = true
  backup_retention_period    = "0"
  multi_az                   = false
  skip_final_snapshot        = true

  tags = {
    Project   = "${var.project}"
    Terraform = "true"
  }
}

ECSの設定です。
コンテナ定義はなるべくTerraform上の変数で設定できるようにtemplateを使用しています。(ブログを書きながら気づきましたがディレクトリ名が不自然ですね...)
ログはCloudWatch Logsに出力するように設定しています。マネジメントコンソールで操作していると気づきにくいですが、ロググループは自動作成されないので注意です。今回はTerraformで作成しています。

resource "aws_ecs_cluster" "cluster" {
  name = "${var.project}-cluster"

  tags = {
    Name      = "${var.project}-cluster"
    Terraform = "true"
  }
}

resource "aws_ecs_service" "service" {
  name            = "${var.project}-service"
  cluster         = "${aws_ecs_cluster.cluster.id}"
  task_definition = "${aws_ecs_task_definition.task_definition.arn}"
  desired_count   = 1
  iam_role        = "${data.aws_iam_role.service_role-ecs-service.arn}"

  load_balancer {
    target_group_arn = "${aws_lb_target_group.lb.arn}"
    container_name   = "aqua-server"
    container_port   = "${var.aqua_server_port}"
  }
}

resource "aws_ecs_task_definition" "task_definition" {
  family                = "aqua"
  container_definitions = "${data.template_file.service.rendered }"
  execution_role_arn    = "${aws_iam_role.task_execution_role.arn}"
  network_mode          = "bridge"

  volume {
    name      = "docker-socket"
    host_path = "/var/run/docker.sock"
  }

  tags = {
    Name      = "${var.project}-service"
    Terraform = "true"
  }

}

data "template_file" "service" {
  template = "${file("task-definitions/service.tmpl.json")}"

  vars = {
    awslogs_group        = "/ecs/${var.project}"
    awslogs_region       = "${var.region}"
    aqua_server_port     = "${var.aqua_server_port}"
    admin_password       = "${var.secretsmanager_admin_password}"
    db_hostname          = "${module.db.this_db_instance_address}"
    db_port              = "${var.postgres_port}"
    db_username          = "${var.postgres_username}"
    db_password          = "${var.secretsmanager_db_password}"
    credentialsParameter = "${data.aws_secretsmanager_secret.container_repository.arn}"
  }
}
[
    {
        "dnsSearchDomains": null,
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-group": "${awslogs_group}",
                "awslogs-region": "${awslogs_region}",
                "awslogs-stream-prefix": "ecs"
            }
        },
        "entryPoint": null,
        "portMappings": [
            {
                "hostPort": ${aqua_server_port},
                "protocol": "tcp",
                "containerPort": ${aqua_server_port}
            }
        ],
        "command": null,
        "linuxParameters": null,
        "cpu": 512,
        "environment": [
            {
                "name": "SCALOCK_AUDIT_DBHOST",
                "value": "${db_hostname}"
            },
            {
                "name": "SCALOCK_AUDIT_DBNAME",
                "value": "slk_audit"
            },
            {
                "name": "SCALOCK_AUDIT_DBPORT",
                "value": "${db_port}"
            },
            {
                "name": "SCALOCK_AUDIT_DBUSER",
                "value": "${db_username}"
            },
            {
                "name": "SCALOCK_DBHOST",
                "value": "${db_hostname}"
            },
            {
                "name": "SCALOCK_DBNAME",
                "value": "scalock"
            },
            {
                "name": "SCALOCK_DBPORT",
                "value": "${db_port}"
            },
            {
                "name": "SCALOCK_DBUSER",
                "value": "${db_username}"
            },
            {
                "name": "SCALOCK_WEBSERVER_NAME",
                "value": "web1"
            }
        ],
        "resourceRequirements": null,
        "ulimits": null,
        "repositoryCredentials": {
            "credentialsParameter": "${credentialsParameter}"
        },
        "dnsServers": null,
        "mountPoints": [
            {
                "readOnly": null,
                "containerPath": "/var/run/docker.sock",
                "sourceVolume": "docker-socket"
            }
        ],
        "workingDirectory": null,
        "secrets": [
            {
                "name": "ADMIN_PASSWORD",
                "valueFrom": "/aws/reference/secretsmanager/${admin_password}"
            },
            {
                "name": "SCALOCK_DBPASSWORD",
                "valueFrom": "/aws/reference/secretsmanager/${db_password}"
            },
            {
                "name": "SCALOCK_AUDIT_DBPASSWORD",
                "valueFrom": "/aws/reference/secretsmanager/${db_password}"
            }
        ],
        "dockerSecurityOptions": null,
        "memory": 512,
        "memoryReservation": null,
        "volumesFrom": [],
        "image": "registry.aquasec.com/console:3.5",
        "disableNetworking": null,
        "interactive": null,
        "healthCheck": null,
        "essential": true,
        "links": [
            "aqua-gateway:aqua-gateway"
        ],
        "hostname": null,
        "extraHosts": null,
        "pseudoTerminal": null,
        "user": null,
        "readonlyRootFilesystem": null,
        "dockerLabels": null,
        "systemControls": null,
        "privileged": null,
        "name": "aqua-server"
    },
    {
        "dnsSearchDomains": null,
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-group": "${awslogs_group}",
                "awslogs-region": "${awslogs_region}",
                "awslogs-stream-prefix": "ecs"
            }
        },
        "entryPoint": null,
        "portMappings": [
            {
                "hostPort": 3622,
                "protocol": "tcp",
                "containerPort": 3622
            }
        ],
        "command": null,
        "linuxParameters": null,
        "cpu": 256,
        "environment": [
            {
                "name": "SCALOCK_AUDIT_DBPORT",
                "value": "${db_port}"
            },
            {
                "name": "SCALOCK_DBHOST",
                "value": "${db_hostname}"
            },
            {
                "name": "SCALOCK_DBNAME",
                "value": "scalock"
            },
            {
                "name": "SCALOCK_AUDIT_DBNAME",
                "value": "slk_audit"
            },
            {
                "name": "SCALOCK_GATEWAY_NAME",
                "value": "gateway"
            },
            {
                "name": "SCALOCK_AUDIT_DBUSER",
                "value": "${db_username}"
            },
            {
                "name": "SCALOCK_DBPORT",
                "value": "${db_port}"
            },
            {
                "name": "SCALOCK_DBUSER",
                "value": "${db_username}"
            },
            {
                "name": "SCALOCK_AUDIT_DBHOST",
                "value": "${db_hostname}"
            }
        ],
        "resourceRequirements": null,
        "ulimits": null,
        "repositoryCredentials": {
            "credentialsParameter": "${credentialsParameter}"
        },
        "dnsServers": null,
        "mountPoints": [],
        "workingDirectory": null,
        "secrets": [
            {
                "name": "SCALOCK_AUDIT_DBPASSWORD",
                "valueFrom": "/aws/reference/secretsmanager/${db_password}"
            },
            {
                "name": "SCALOCK_DBPASSWORD",
                "valueFrom": "/aws/reference/secretsmanager/${db_password}"
            }
        ],
        "dockerSecurityOptions": null,
        "memory": 256,
        "memoryReservation": null,
        "volumesFrom": [],
        "image": "registry.aquasec.com/gateway:3.5",
        "disableNetworking": null,
        "interactive": null,
        "healthCheck": null,
        "essential": true,
        "links": [],
        "hostname": null,
        "extraHosts": null,
        "pseudoTerminal": null,
        "user": null,
        "readonlyRootFilesystem": null,
        "dockerLabels": null,
        "systemControls": null,
        "privileged": null,
        "name": "aqua-gateway"
    }
]

他のリソースはGitHubでご覧頂ければと思います。

Route53の設定

ALBのDNS Nameを確認し、Aliasレコードで名前解決できるように設定します。

動作確認

定義した、ドメイン名でアクセスしてみます。

無事にアクセスできました!!今回はここまで終了とします。
実際にはAqua GatewayがEnforcerから接続を受けるためのSecurity Groupの設定などが必要になると思われます。

あとがき

実はこのブログを書いている途中で気づいたのですが、Fargateでも動く???

Aqua CSP also supports:
Amazon AWS Fargate
Microsoft Azure Service Fabric

System Requirements
Server側の話かわからないですが、調べてうまく出来たらブログでまた公開しようと思います。