├── .gitignore ├── Dockerfile ├── README.md ├── buildspec.yml ├── clair-build.py ├── clair-build.template ├── clair-deploy-fargate.py ├── clair-deploy-fargate.template └── config.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/coreos/clair:v2.0.3 2 | 3 | COPY config.yaml /config/config.yaml 4 | 5 | ENTRYPOINT [] 6 | CMD sed -i "s/localhost/$DB_HOST/" /config/config.yaml && sed -i "s/db_password/$DB_PASSWORD/" /config/config.yaml && exec /clair -config=/config/config.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clair running on AWS ECS Fargate 2 | The Clair image scanner service from CoreOS (https://github.com/coreos/clair) can be used to scan a particular Docker image for known vulnerabilities as part of your build or deployment pipeline. This project is to package and deploy the scanner in a way that can run on AWS ECS via Fargate. 3 | 4 | I use it in conjunction with a commandline tool called Klar (https://github.com/optiopay/klar) which I invoke as the last step of a CodeBuild to ask Clair to scan the new image once it has been pushed to the Elastic Container Registry (ECR). An example of how to do that is here - https://github.com/jasonumiker/ghost-ecs-fargate-pipeline/blob/master/ghost-container/buildspec.yml 5 | 6 | ## Example of using klar to invoke clair on an image in ECR in your CodeBuild 7 | wget https://github.com/optiopay/klar/releases/download/v2.3.0/klar-2.3.0-linux-amd64 8 | chmod +x ./klar-2.3.0-linux-amd64 9 | mv ./klar-2.3.0-linux-amd64 ./klar 10 | DOCKER_LOGIN=`aws ecr get-login --region $AWS_DEFAULT_REGION` 11 | PASSWORD=`echo $DOCKER_LOGIN | cut -d' ' -f6` 12 | DOCKER_USER=AWS DOCKER_PASSWORD=${PASSWORD} CLAIR_ADDR=$CLAIR_URL ./klar $IMAGE_URI 13 | 14 | ## Why do we need to build our own image? 15 | One minor change was required to the upstream CoreOS Clair (https://github.com/coreos/clair) Docker image to get it to run in Fargate. Clair doesn't take options like the configuration of its database via environment variables but from a config file - and there is not a capability currently to mount that into the container at runtime in ECS Fargate. 16 | As such, we rebuild the Clair image to incorporate the config file in the image as well as convert the DB host and password environment variable to values in that file at runtime. 17 | 18 | If you don't want to build this yourself it is available at `jasonumiker/clair:latest` on hub.docker.com. 19 | 20 | ## Prerequisites 21 | A Fargate task requires a VPC ID and at least one subnet ID to deploy to. Ideally these will be private subnet(s) as there is not any login/password/token on the use of Clair as configured. 22 | 23 | I suggest using the Quick Start for creating this VPC if you don't already have one - https://aws.amazon.com/quickstart/architecture/vpc/ 24 | 25 | ## Deployment Instructions 26 | The CloudFormation template `clair-deploy-fargate.template` deploys the Clair service including creating the: 27 | 1. Task and task execution IAM Roles 28 | 1. PostgreSQL RDS database 29 | 1. Application Load Balancer (ALB) 30 | 1. Security Groups set up to limit access so that: 31 | 1. Only the tasks can talk to the database 32 | 1. Only the ALB can talk to the tasks 33 | 1. The ECS task definition and associated service set up to run on Clair on Fargate 34 | 35 | The parameters it requires are: 36 | 1. The name of the ECS cluster to deploy to 37 | 1. The clair docker image to deploy 38 | 1. If you don't want to build this yourself you can use `jasonumiker/clair:latest` from the Docker Hub 39 | 1. The VPC ID and two subnet IDs to run the tasks, ALB and RDS database in 40 | 1. Ideally these should be private subnets which will prevent any access to the service from the Internet 41 | 1. The database password to use 42 | 1. This will be set as the master password on the RDS and set as the DB_PASSWORD environment variable for the task in plaintext 43 | 1. This is a not fully secure approach however this RDS is dedicated just to Clair, the security group for the database only allows network connections from the Clair tasks and there is no sensitive information stored in this database. I'll investigate storing this in Secrets Manager a future release though. 44 | 45 | ## Building the Container 46 | If you'd prefer to build and host the container yourself rather than use `jasonumiker/clair:latest` on Docker Hub there is an included `buildspec.yml` CodeBuild build spec and `clair-build.template` CloudFormation template. These are what I use to build the image on the Docker Hub. 47 | 48 | The CloudFormation stack will create an ECR repository as well as S3 bucket for storing the results as well as all necessary IAM roles. 49 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | # AWS CodeBuild buildspec to build the clair container image 2 | # By Jason Umiker (jason.umiker@gmail.com) 3 | # This requires the following environment variables be set on the Project: 4 | # AWS_DEFAULT_REGION (Supplied by CodeBuild) 5 | # AWS_ACCOUNT_ID 6 | # IMAGE_REPO_NAME 7 | # IMAGE_TAG 8 | 9 | version: 0.2 10 | 11 | phases: 12 | pre_build: 13 | commands: 14 | - echo Logging in to Amazon ECR... 15 | - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION) 16 | - CODEBUILD_RESOLVED_SOURCE_VERSION="${CODEBUILD_RESOLVED_SOURCE_VERSION:-$IMAGE_TAG}" 17 | - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) 18 | - IMAGE_URI="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG" 19 | build: 20 | commands: 21 | - echo Build started on `date` 22 | - echo Building the Docker image... 23 | - docker build -t $IMAGE_URI . 24 | post_build: 25 | commands: 26 | - bash -c "if [ /"$CODEBUILD_BUILD_SUCCEEDING/" == /"0/" ]; then exit 1; fi" 27 | - echo Build stage successfully completed on `date` 28 | - docker push $IMAGE_URI 29 | - printf '[{"name":"clair","imageUri":"%s"}]' "$IMAGE_URI" > images.json 30 | artifacts: 31 | files: images.json -------------------------------------------------------------------------------- /clair-build.py: -------------------------------------------------------------------------------- 1 | # Troposphere to create CloudFormation template to build the Clair image 2 | # By Jason Umiker (jason.umiker@gmail.com) 3 | 4 | from troposphere import Output, Join, Ref, Template 5 | from troposphere import AWS_ACCOUNT_ID, AWS_REGION 6 | from troposphere import ecr, s3, iam, codebuild 7 | 8 | t = Template() 9 | 10 | t.add_description("Template to set up a CodeBuild for the Clair container") 11 | 12 | # Create the clair Repository 13 | Repository = t.add_resource( 14 | ecr.Repository( 15 | "Repository", 16 | RepositoryName="clair" 17 | ) 18 | ) 19 | 20 | # Create the S3 Bucket for Output 21 | S3Bucket = t.add_resource( 22 | s3.Bucket( 23 | "ClairBuildOutput" 24 | ) 25 | ) 26 | 27 | # CodeBuild Service Role 28 | ServiceRole = t.add_resource(iam.Role( 29 | "InstanceRole", 30 | AssumeRolePolicyDocument={ 31 | "Statement": [ 32 | { 33 | 'Effect': 'Allow', 34 | 'Principal': {'Service': 'codebuild.amazonaws.com'}, 35 | "Action": "sts:AssumeRole" 36 | } 37 | ] 38 | } 39 | )) 40 | 41 | # CodeBuild Service Policy 42 | CodeBuildServiceRolePolicy = t.add_resource(iam.PolicyType( 43 | "CodeBuildServiceRolePolicy", 44 | PolicyName="CodeBuildServiceRolePolicy", 45 | PolicyDocument={"Version": "2012-10-17", 46 | "Statement": [ 47 | { 48 | "Sid": "CloudWatchLogsPolicy", 49 | "Effect": "Allow", 50 | "Action": [ 51 | "logs:CreateLogGroup", 52 | "logs:CreateLogStream", 53 | "logs:PutLogEvents" 54 | ], 55 | "Resource": [ 56 | "*" 57 | ] 58 | }, 59 | { 60 | "Sid": "CodeCommitPolicy", 61 | "Effect": "Allow", 62 | "Action": [ 63 | "codecommit:GitPull" 64 | ], 65 | "Resource": [ 66 | "*" 67 | ] 68 | }, 69 | { 70 | "Sid": "S3GetObjectPolicy", 71 | "Effect": "Allow", 72 | "Action": [ 73 | "s3:GetObject", 74 | "s3:GetObjectVersion" 75 | ], 76 | "Resource": [ 77 | "*" 78 | ] 79 | }, 80 | { 81 | "Sid": "S3PutObjectPolicy", 82 | "Effect": "Allow", 83 | "Action": [ 84 | "s3:PutObject" 85 | ], 86 | "Resource": [ 87 | "*" 88 | ] 89 | }, 90 | {'Action': ['ecr:GetAuthorizationToken'], 91 | 'Resource': ['*'], 92 | 'Effect': 'Allow'}, 93 | {'Action': ['ecr:*'], 94 | 'Resource': [ 95 | Join("", ["arn:aws:ecr:", 96 | Ref(AWS_REGION), 97 | ":", Ref(AWS_ACCOUNT_ID), 98 | ":repository/", 99 | Ref(Repository)] 100 | ), 101 | ], 102 | 'Effect': 'Allow'}, 103 | ]}, 104 | Roles=[Ref(ServiceRole)], 105 | )) 106 | 107 | # Create CodeBuild Projects 108 | # Image Build 109 | ImageArtifacts = codebuild.Artifacts( 110 | Type='S3', 111 | Name='artifacts', 112 | Location=Ref(S3Bucket) 113 | ) 114 | 115 | ImageEnvironment = codebuild.Environment( 116 | ComputeType="BUILD_GENERAL1_SMALL", 117 | Image="aws/codebuild/docker:17.09.0", 118 | Type="LINUX_CONTAINER", 119 | EnvironmentVariables=[{'Name': 'AWS_ACCOUNT_ID', 'Value': Ref(AWS_ACCOUNT_ID)}, 120 | {'Name': 'IMAGE_REPO_NAME', 'Value': Ref(Repository)}, 121 | {'Name': 'IMAGE_TAG', 'Value': 'latest'}], 122 | PrivilegedMode=True 123 | ) 124 | 125 | ImageSource = codebuild.Source( 126 | Location="https://github.com/jasonumiker/clair-ecs-fargate", 127 | Type="GITHUB" 128 | ) 129 | 130 | ImageProject = codebuild.Project( 131 | "ImageBuildProject", 132 | Artifacts=ImageArtifacts, 133 | Environment=ImageEnvironment, 134 | Name="clair-build", 135 | ServiceRole=Ref(ServiceRole), 136 | Source=ImageSource, 137 | DependsOn=CodeBuildServiceRolePolicy 138 | ) 139 | t.add_resource(ImageProject) 140 | 141 | # Output clair repository URL 142 | t.add_output(Output( 143 | "RepositoryURL", 144 | Description="The docker repository URL", 145 | Value=Join("", [ 146 | Ref(AWS_ACCOUNT_ID), 147 | ".dkr.ecr.", 148 | Ref(AWS_REGION), 149 | ".amazonaws.com/", 150 | Ref(Repository) 151 | ]), 152 | )) 153 | 154 | print(t.to_json()) 155 | -------------------------------------------------------------------------------- /clair-build.template: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Template to set up a CodeBuild for the Clair container", 3 | "Outputs": { 4 | "RepositoryURL": { 5 | "Description": "The docker repository URL", 6 | "Value": { 7 | "Fn::Join": [ 8 | "", 9 | [ 10 | { 11 | "Ref": "AWS::AccountId" 12 | }, 13 | ".dkr.ecr.", 14 | { 15 | "Ref": "AWS::Region" 16 | }, 17 | ".amazonaws.com/", 18 | { 19 | "Ref": "Repository" 20 | } 21 | ] 22 | ] 23 | } 24 | } 25 | }, 26 | "Resources": { 27 | "ClairBuildOutput": { 28 | "Type": "AWS::S3::Bucket" 29 | }, 30 | "CodeBuildServiceRolePolicy": { 31 | "Properties": { 32 | "PolicyDocument": { 33 | "Statement": [ 34 | { 35 | "Action": [ 36 | "logs:CreateLogGroup", 37 | "logs:CreateLogStream", 38 | "logs:PutLogEvents" 39 | ], 40 | "Effect": "Allow", 41 | "Resource": [ 42 | "*" 43 | ], 44 | "Sid": "CloudWatchLogsPolicy" 45 | }, 46 | { 47 | "Action": [ 48 | "codecommit:GitPull" 49 | ], 50 | "Effect": "Allow", 51 | "Resource": [ 52 | "*" 53 | ], 54 | "Sid": "CodeCommitPolicy" 55 | }, 56 | { 57 | "Action": [ 58 | "s3:GetObject", 59 | "s3:GetObjectVersion" 60 | ], 61 | "Effect": "Allow", 62 | "Resource": [ 63 | "*" 64 | ], 65 | "Sid": "S3GetObjectPolicy" 66 | }, 67 | { 68 | "Action": [ 69 | "s3:PutObject" 70 | ], 71 | "Effect": "Allow", 72 | "Resource": [ 73 | "*" 74 | ], 75 | "Sid": "S3PutObjectPolicy" 76 | }, 77 | { 78 | "Action": [ 79 | "ecr:GetAuthorizationToken" 80 | ], 81 | "Effect": "Allow", 82 | "Resource": [ 83 | "*" 84 | ] 85 | }, 86 | { 87 | "Action": [ 88 | "ecr:*" 89 | ], 90 | "Effect": "Allow", 91 | "Resource": [ 92 | { 93 | "Fn::Join": [ 94 | "", 95 | [ 96 | "arn:aws:ecr:", 97 | { 98 | "Ref": "AWS::Region" 99 | }, 100 | ":", 101 | { 102 | "Ref": "AWS::AccountId" 103 | }, 104 | ":repository/", 105 | { 106 | "Ref": "Repository" 107 | } 108 | ] 109 | ] 110 | } 111 | ] 112 | } 113 | ], 114 | "Version": "2012-10-17" 115 | }, 116 | "PolicyName": "CodeBuildServiceRolePolicy", 117 | "Roles": [ 118 | { 119 | "Ref": "InstanceRole" 120 | } 121 | ] 122 | }, 123 | "Type": "AWS::IAM::Policy" 124 | }, 125 | "ImageBuildProject": { 126 | "DependsOn": "CodeBuildServiceRolePolicy", 127 | "Properties": { 128 | "Artifacts": { 129 | "Location": { 130 | "Ref": "ClairBuildOutput" 131 | }, 132 | "Name": "artifacts", 133 | "Type": "S3" 134 | }, 135 | "Environment": { 136 | "ComputeType": "BUILD_GENERAL1_SMALL", 137 | "EnvironmentVariables": [ 138 | { 139 | "Name": "AWS_ACCOUNT_ID", 140 | "Value": { 141 | "Ref": "AWS::AccountId" 142 | } 143 | }, 144 | { 145 | "Name": "IMAGE_REPO_NAME", 146 | "Value": { 147 | "Ref": "Repository" 148 | } 149 | }, 150 | { 151 | "Name": "IMAGE_TAG", 152 | "Value": "latest" 153 | } 154 | ], 155 | "Image": "aws/codebuild/docker:17.09.0", 156 | "PrivilegedMode": "true", 157 | "Type": "LINUX_CONTAINER" 158 | }, 159 | "Name": "clair-build", 160 | "ServiceRole": { 161 | "Ref": "InstanceRole" 162 | }, 163 | "Source": { 164 | "Location": "https://github.com/jasonumiker/clair-ecs-fargate", 165 | "Type": "GITHUB" 166 | } 167 | }, 168 | "Type": "AWS::CodeBuild::Project" 169 | }, 170 | "InstanceRole": { 171 | "Properties": { 172 | "AssumeRolePolicyDocument": { 173 | "Statement": [ 174 | { 175 | "Action": "sts:AssumeRole", 176 | "Effect": "Allow", 177 | "Principal": { 178 | "Service": "codebuild.amazonaws.com" 179 | } 180 | } 181 | ] 182 | } 183 | }, 184 | "Type": "AWS::IAM::Role" 185 | }, 186 | "Repository": { 187 | "Properties": { 188 | "RepositoryName": "clair" 189 | }, 190 | "Type": "AWS::ECR::Repository" 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /clair-deploy-fargate.py: -------------------------------------------------------------------------------- 1 | # Troposphere to create CloudFormation template of Clair Fargate deployment 2 | # By Jason Umiker (jason.umiker@gmail.com) 3 | 4 | from troposphere import Parameter, Ref, Template, Output, Join, GetAtt 5 | from troposphere import ecs 6 | from troposphere.logs import LogGroup 7 | from troposphere.rds import DBSubnetGroup, DBInstance 8 | from troposphere.iam import Role, PolicyType 9 | from troposphere.ec2 import SecurityGroup, SecurityGroupRule 10 | from troposphere.elasticloadbalancingv2 import LoadBalancer, TargetGroup, Matcher, Listener, Action 11 | 12 | t = Template() 13 | t.add_version('2010-09-09') 14 | 15 | # Get the required Parameters 16 | 17 | cluster = t.add_parameter(Parameter( 18 | 'Cluster', 19 | Type='String', 20 | Description='The ECS Cluster to deploy to.', 21 | )) 22 | 23 | clair_image = t.add_parameter(Parameter( 24 | 'ClairImage', 25 | Type='String', 26 | Default='jasonumiker/clair:latest', 27 | Description='The Clair container image to deploy.', 28 | )) 29 | 30 | clair_vpc = t.add_parameter(Parameter( 31 | 'ClairVPC', 32 | Type='AWS::EC2::VPC::Id', 33 | Description='A VPC ID for the container.', 34 | )) 35 | 36 | clair_subnet = t.add_parameter(Parameter( 37 | 'ClairSubnet', 38 | Type='AWS::EC2::Subnet::Id', 39 | Description='A VPC subnet ID for the container.', 40 | )) 41 | 42 | clair_subnet2 = t.add_parameter(Parameter( 43 | 'ClairSubnet2', 44 | Type='AWS::EC2::Subnet::Id', 45 | Description='A 2nd VPC subnet ID for the container.', 46 | )) 47 | 48 | clair_db_password = t.add_parameter(Parameter( 49 | 'ClairDBPassword', 50 | Type='String', 51 | NoEcho=True, 52 | Description='The initial Clair RDS Password.', 53 | )) 54 | 55 | # Create the Resources 56 | 57 | # Create CloudWatch Log Group 58 | clair_loggroup = t.add_resource(LogGroup( 59 | "ClairLogGroup", 60 | )) 61 | 62 | # Create Security group that allows traffic into the ALB 63 | alb_security_group = SecurityGroup( 64 | "ALBSecurityGroup", 65 | GroupDescription="Clair ALB Security Group", 66 | VpcId=Ref(clair_vpc), 67 | SecurityGroupIngress=[ 68 | SecurityGroupRule( 69 | IpProtocol="tcp", 70 | FromPort="6060", 71 | ToPort="6061", 72 | CidrIp="0.0.0.0/0", 73 | ), 74 | ] 75 | ) 76 | t.add_resource(alb_security_group) 77 | 78 | # Create Security group for the host/ENI/Fargate that allows 6060-6061 79 | clair_host_security_group = SecurityGroup( 80 | "ClairHostSecurityGroup", 81 | GroupDescription="Clair ECS Security Group.", 82 | VpcId=Ref(clair_vpc), 83 | SecurityGroupIngress=[ 84 | SecurityGroupRule( 85 | IpProtocol="tcp", 86 | FromPort="6060", 87 | ToPort="6061", 88 | SourceSecurityGroupId=(GetAtt(alb_security_group, 'GroupId')) 89 | ), 90 | ] 91 | ) 92 | t.add_resource(clair_host_security_group) 93 | 94 | # Create the Task Role 95 | TaskRole = t.add_resource(Role( 96 | "TaskRole", 97 | AssumeRolePolicyDocument={ 98 | 'Statement': [{ 99 | 'Effect': 'Allow', 100 | 'Principal': {'Service': ['ecs-tasks.amazonaws.com']}, 101 | 'Action': ["sts:AssumeRole"] 102 | }]}, 103 | )) 104 | 105 | # Create the Task Execution Role 106 | TaskExecutionRole = t.add_resource(Role( 107 | "TaskExecutionRole", 108 | AssumeRolePolicyDocument={ 109 | 'Statement': [{ 110 | 'Effect': 'Allow', 111 | 'Principal': {'Service': ['ecs-tasks.amazonaws.com']}, 112 | 'Action': ["sts:AssumeRole"] 113 | }]}, 114 | )) 115 | 116 | # Create the Fargate Execution Policy (access to ECR and CW Logs) 117 | FargateExecutionPolicy = t.add_resource(PolicyType( 118 | "FargateExecutionPolicy", 119 | PolicyName="fargate-execution", 120 | PolicyDocument={'Version': '2012-10-17', 121 | 'Statement': [{'Action': ['ecr:GetAuthorizationToken', 122 | 'ecr:BatchCheckLayerAvailability', 123 | 'ecr:GetDownloadUrlForLayer', 124 | 'ecr:BatchGetImage', 'logs:CreateLogStream', 125 | 'logs:PutLogEvents'], 126 | 'Resource': ['*'], 127 | 'Effect': 'Allow'}, 128 | ]}, 129 | Roles=[Ref(TaskExecutionRole)], 130 | )) 131 | 132 | # Add the application ELB 133 | ClairALB = t.add_resource(LoadBalancer( 134 | "ClairALB", 135 | Scheme="internal", 136 | Subnets=[Ref(clair_subnet), Ref(clair_subnet2)], 137 | SecurityGroups=[Ref(alb_security_group)] 138 | )) 139 | 140 | ClairTargetGroup = t.add_resource(TargetGroup( 141 | "ClairTargetGroup", 142 | HealthCheckIntervalSeconds="30", 143 | HealthCheckProtocol="HTTP", 144 | HealthCheckTimeoutSeconds="10", 145 | HealthyThresholdCount="4", 146 | HealthCheckPort="6061", 147 | HealthCheckPath="/health", 148 | Matcher=Matcher(HttpCode="200"), 149 | Port="6060", 150 | Protocol="HTTP", 151 | UnhealthyThresholdCount="3", 152 | TargetType="ip", 153 | VpcId=Ref(clair_vpc) 154 | )) 155 | 156 | Listener = t.add_resource(Listener( 157 | "Listener", 158 | Port="6060", 159 | Protocol="HTTP", 160 | LoadBalancerArn=Ref(ClairALB), 161 | DefaultActions=[Action( 162 | Type="forward", 163 | TargetGroupArn=Ref(ClairTargetGroup) 164 | )] 165 | )) 166 | 167 | # Create the DB Subnet Group 168 | dbsubnetgroup = t.add_resource(DBSubnetGroup( 169 | "DBSubnetGroup", 170 | DBSubnetGroupDescription="Subnets available for the RDS DB Instance", 171 | SubnetIds=[Ref(clair_subnet), Ref(clair_subnet2)], 172 | )) 173 | 174 | # Create the DB's Security group which only allows access to members of the Ghost Host SG 175 | dbsecuritygroup = t.add_resource(SecurityGroup( 176 | "DBSecurityGroup", 177 | GroupDescription="Security group for RDS DB Instance.", 178 | VpcId=Ref(clair_vpc), 179 | SecurityGroupIngress=[ 180 | SecurityGroupRule( 181 | IpProtocol="tcp", 182 | FromPort="5432", 183 | ToPort="5432", 184 | SourceSecurityGroupId=(GetAtt(clair_host_security_group, 'GroupId')) 185 | ), 186 | ] 187 | )) 188 | 189 | # Create the Postgres RDS 190 | clair_db = t.add_resource(DBInstance( 191 | "ClairDB", 192 | DBName='postgres', 193 | AllocatedStorage='20', 194 | DBInstanceClass='db.t2.micro', 195 | Engine='postgres', 196 | EngineVersion='9.6.8', 197 | MasterUsername='postgres', 198 | MasterUserPassword=Ref(clair_db_password), 199 | DBSubnetGroupName=Ref(dbsubnetgroup), 200 | VPCSecurityGroups=[Ref(dbsecuritygroup)], 201 | MultiAZ='False', 202 | StorageType='gp2' 203 | )) 204 | 205 | clair_task_definition = t.add_resource(ecs.TaskDefinition( 206 | 'ClairTaskDefinition', 207 | RequiresCompatibilities=['FARGATE'], 208 | Cpu='512', 209 | Memory='1GB', 210 | NetworkMode='awsvpc', 211 | TaskRoleArn=Ref(TaskRole), 212 | ExecutionRoleArn=Ref(TaskExecutionRole), 213 | ContainerDefinitions=[ 214 | ecs.ContainerDefinition( 215 | Name='clair', 216 | Image=Ref(clair_image), 217 | Essential=True, 218 | PortMappings=[ecs.PortMapping(ContainerPort=6060),ecs.PortMapping(ContainerPort=6061)], 219 | Environment=[ 220 | ecs.Environment( 221 | Name='DB_HOST', 222 | Value=GetAtt(clair_db, "Endpoint.Address") 223 | ), 224 | ecs.Environment( 225 | Name='DB_PASSWORD', 226 | Value=Ref(clair_db_password), 227 | ), 228 | ], 229 | LogConfiguration=ecs.LogConfiguration( 230 | LogDriver='awslogs', 231 | Options={'awslogs-group': Ref(clair_loggroup), 232 | 'awslogs-region': Ref('AWS::Region'), 233 | 'awslogs-stream-prefix': 'clair'} 234 | ) 235 | ) 236 | ] 237 | )) 238 | 239 | clair_service = t.add_resource(ecs.Service( 240 | 'ClairService', 241 | Cluster=Ref(cluster), 242 | DesiredCount=1, 243 | TaskDefinition=Ref(clair_task_definition), 244 | LaunchType='FARGATE', 245 | LoadBalancers=[ 246 | ecs.LoadBalancer( 247 | ContainerName='clair', 248 | ContainerPort=6060, 249 | TargetGroupArn=Ref('ClairTargetGroup') 250 | ) 251 | ], 252 | NetworkConfiguration=ecs.NetworkConfiguration( 253 | AwsvpcConfiguration=ecs.AwsvpcConfiguration( 254 | Subnets=[Ref(clair_subnet), Ref(clair_subnet2)], 255 | SecurityGroups=[Ref(clair_host_security_group)], 256 | ) 257 | ), 258 | DependsOn='ClairALB' 259 | )) 260 | 261 | # Create the required Outputs 262 | 263 | t.add_output(Output( 264 | "ClairURL", 265 | Description="URL of the ALB", 266 | Value=Join("", ["http://", GetAtt(ClairALB, "DNSName")]) 267 | )) 268 | 269 | print(t.to_json()) -------------------------------------------------------------------------------- /clair-deploy-fargate.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Outputs": { 4 | "ClairURL": { 5 | "Description": "URL of the ALB", 6 | "Value": { 7 | "Fn::Join": [ 8 | "", 9 | [ 10 | "http://", 11 | { 12 | "Fn::GetAtt": [ 13 | "ClairALB", 14 | "DNSName" 15 | ] 16 | } 17 | ] 18 | ] 19 | } 20 | } 21 | }, 22 | "Parameters": { 23 | "ClairDBPassword": { 24 | "Description": "The initial Clair RDS Password.", 25 | "NoEcho": true, 26 | "Type": "String" 27 | }, 28 | "ClairImage": { 29 | "Default": "jasonumiker/clair:latest", 30 | "Description": "The Clair container image to deploy.", 31 | "Type": "String" 32 | }, 33 | "ClairSubnet": { 34 | "Description": "A VPC subnet ID for the container.", 35 | "Type": "AWS::EC2::Subnet::Id" 36 | }, 37 | "ClairSubnet2": { 38 | "Description": "A 2nd VPC subnet ID for the container.", 39 | "Type": "AWS::EC2::Subnet::Id" 40 | }, 41 | "ClairVPC": { 42 | "Description": "A VPC ID for the container.", 43 | "Type": "AWS::EC2::VPC::Id" 44 | }, 45 | "Cluster": { 46 | "Description": "The ECS Cluster to deploy to.", 47 | "Type": "String" 48 | } 49 | }, 50 | "Resources": { 51 | "ALBSecurityGroup": { 52 | "Properties": { 53 | "GroupDescription": "Clair ALB Security Group", 54 | "SecurityGroupIngress": [ 55 | { 56 | "CidrIp": "0.0.0.0/0", 57 | "FromPort": "6060", 58 | "IpProtocol": "tcp", 59 | "ToPort": "6061" 60 | } 61 | ], 62 | "VpcId": { 63 | "Ref": "ClairVPC" 64 | } 65 | }, 66 | "Type": "AWS::EC2::SecurityGroup" 67 | }, 68 | "ClairALB": { 69 | "Properties": { 70 | "Scheme": "internal", 71 | "SecurityGroups": [ 72 | { 73 | "Ref": "ALBSecurityGroup" 74 | } 75 | ], 76 | "Subnets": [ 77 | { 78 | "Ref": "ClairSubnet" 79 | }, 80 | { 81 | "Ref": "ClairSubnet2" 82 | } 83 | ] 84 | }, 85 | "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" 86 | }, 87 | "ClairDB": { 88 | "Properties": { 89 | "AllocatedStorage": "20", 90 | "DBInstanceClass": "db.t2.micro", 91 | "DBName": "postgres", 92 | "DBSubnetGroupName": { 93 | "Ref": "DBSubnetGroup" 94 | }, 95 | "Engine": "postgres", 96 | "EngineVersion": "9.6.8", 97 | "MasterUserPassword": { 98 | "Ref": "ClairDBPassword" 99 | }, 100 | "MasterUsername": "postgres", 101 | "MultiAZ": "false", 102 | "StorageType": "gp2", 103 | "VPCSecurityGroups": [ 104 | { 105 | "Ref": "DBSecurityGroup" 106 | } 107 | ] 108 | }, 109 | "Type": "AWS::RDS::DBInstance" 110 | }, 111 | "ClairHostSecurityGroup": { 112 | "Properties": { 113 | "GroupDescription": "Clair ECS Security Group.", 114 | "SecurityGroupIngress": [ 115 | { 116 | "FromPort": "6060", 117 | "IpProtocol": "tcp", 118 | "SourceSecurityGroupId": { 119 | "Fn::GetAtt": [ 120 | "ALBSecurityGroup", 121 | "GroupId" 122 | ] 123 | }, 124 | "ToPort": "6061" 125 | } 126 | ], 127 | "VpcId": { 128 | "Ref": "ClairVPC" 129 | } 130 | }, 131 | "Type": "AWS::EC2::SecurityGroup" 132 | }, 133 | "ClairLogGroup": { 134 | "Type": "AWS::Logs::LogGroup" 135 | }, 136 | "ClairService": { 137 | "DependsOn": "ClairALB", 138 | "Properties": { 139 | "Cluster": { 140 | "Ref": "Cluster" 141 | }, 142 | "DesiredCount": 1, 143 | "LaunchType": "FARGATE", 144 | "LoadBalancers": [ 145 | { 146 | "ContainerName": "clair", 147 | "ContainerPort": 6060, 148 | "TargetGroupArn": { 149 | "Ref": "ClairTargetGroup" 150 | } 151 | } 152 | ], 153 | "NetworkConfiguration": { 154 | "AwsvpcConfiguration": { 155 | "SecurityGroups": [ 156 | { 157 | "Ref": "ClairHostSecurityGroup" 158 | } 159 | ], 160 | "Subnets": [ 161 | { 162 | "Ref": "ClairSubnet" 163 | }, 164 | { 165 | "Ref": "ClairSubnet2" 166 | } 167 | ] 168 | } 169 | }, 170 | "TaskDefinition": { 171 | "Ref": "ClairTaskDefinition" 172 | } 173 | }, 174 | "Type": "AWS::ECS::Service" 175 | }, 176 | "ClairTargetGroup": { 177 | "Properties": { 178 | "HealthCheckIntervalSeconds": "30", 179 | "HealthCheckPath": "/health", 180 | "HealthCheckPort": "6061", 181 | "HealthCheckProtocol": "HTTP", 182 | "HealthCheckTimeoutSeconds": "10", 183 | "HealthyThresholdCount": "4", 184 | "Matcher": { 185 | "HttpCode": "200" 186 | }, 187 | "Port": "6060", 188 | "Protocol": "HTTP", 189 | "TargetType": "ip", 190 | "UnhealthyThresholdCount": "3", 191 | "VpcId": { 192 | "Ref": "ClairVPC" 193 | } 194 | }, 195 | "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" 196 | }, 197 | "ClairTaskDefinition": { 198 | "Properties": { 199 | "ContainerDefinitions": [ 200 | { 201 | "Environment": [ 202 | { 203 | "Name": "DB_HOST", 204 | "Value": { 205 | "Fn::GetAtt": [ 206 | "ClairDB", 207 | "Endpoint.Address" 208 | ] 209 | } 210 | }, 211 | { 212 | "Name": "DB_PASSWORD", 213 | "Value": { 214 | "Ref": "ClairDBPassword" 215 | } 216 | } 217 | ], 218 | "Essential": "true", 219 | "Image": { 220 | "Ref": "ClairImage" 221 | }, 222 | "LogConfiguration": { 223 | "LogDriver": "awslogs", 224 | "Options": { 225 | "awslogs-group": { 226 | "Ref": "ClairLogGroup" 227 | }, 228 | "awslogs-region": { 229 | "Ref": "AWS::Region" 230 | }, 231 | "awslogs-stream-prefix": "clair" 232 | } 233 | }, 234 | "Name": "clair", 235 | "PortMappings": [ 236 | { 237 | "ContainerPort": 6060 238 | }, 239 | { 240 | "ContainerPort": 6061 241 | } 242 | ] 243 | } 244 | ], 245 | "Cpu": "512", 246 | "ExecutionRoleArn": { 247 | "Ref": "TaskExecutionRole" 248 | }, 249 | "Memory": "1GB", 250 | "NetworkMode": "awsvpc", 251 | "RequiresCompatibilities": [ 252 | "FARGATE" 253 | ], 254 | "TaskRoleArn": { 255 | "Ref": "TaskRole" 256 | } 257 | }, 258 | "Type": "AWS::ECS::TaskDefinition" 259 | }, 260 | "DBSecurityGroup": { 261 | "Properties": { 262 | "GroupDescription": "Security group for RDS DB Instance.", 263 | "SecurityGroupIngress": [ 264 | { 265 | "FromPort": "5432", 266 | "IpProtocol": "tcp", 267 | "SourceSecurityGroupId": { 268 | "Fn::GetAtt": [ 269 | "ClairHostSecurityGroup", 270 | "GroupId" 271 | ] 272 | }, 273 | "ToPort": "5432" 274 | } 275 | ], 276 | "VpcId": { 277 | "Ref": "ClairVPC" 278 | } 279 | }, 280 | "Type": "AWS::EC2::SecurityGroup" 281 | }, 282 | "DBSubnetGroup": { 283 | "Properties": { 284 | "DBSubnetGroupDescription": "Subnets available for the RDS DB Instance", 285 | "SubnetIds": [ 286 | { 287 | "Ref": "ClairSubnet" 288 | }, 289 | { 290 | "Ref": "ClairSubnet2" 291 | } 292 | ] 293 | }, 294 | "Type": "AWS::RDS::DBSubnetGroup" 295 | }, 296 | "FargateExecutionPolicy": { 297 | "Properties": { 298 | "PolicyDocument": { 299 | "Statement": [ 300 | { 301 | "Action": [ 302 | "ecr:GetAuthorizationToken", 303 | "ecr:BatchCheckLayerAvailability", 304 | "ecr:GetDownloadUrlForLayer", 305 | "ecr:BatchGetImage", 306 | "logs:CreateLogStream", 307 | "logs:PutLogEvents" 308 | ], 309 | "Effect": "Allow", 310 | "Resource": [ 311 | "*" 312 | ] 313 | } 314 | ], 315 | "Version": "2012-10-17" 316 | }, 317 | "PolicyName": "fargate-execution", 318 | "Roles": [ 319 | { 320 | "Ref": "TaskExecutionRole" 321 | } 322 | ] 323 | }, 324 | "Type": "AWS::IAM::Policy" 325 | }, 326 | "Listener": { 327 | "Properties": { 328 | "DefaultActions": [ 329 | { 330 | "TargetGroupArn": { 331 | "Ref": "ClairTargetGroup" 332 | }, 333 | "Type": "forward" 334 | } 335 | ], 336 | "LoadBalancerArn": { 337 | "Ref": "ClairALB" 338 | }, 339 | "Port": "6060", 340 | "Protocol": "HTTP" 341 | }, 342 | "Type": "AWS::ElasticLoadBalancingV2::Listener" 343 | }, 344 | "TaskExecutionRole": { 345 | "Properties": { 346 | "AssumeRolePolicyDocument": { 347 | "Statement": [ 348 | { 349 | "Action": [ 350 | "sts:AssumeRole" 351 | ], 352 | "Effect": "Allow", 353 | "Principal": { 354 | "Service": [ 355 | "ecs-tasks.amazonaws.com" 356 | ] 357 | } 358 | } 359 | ] 360 | } 361 | }, 362 | "Type": "AWS::IAM::Role" 363 | }, 364 | "TaskRole": { 365 | "Properties": { 366 | "AssumeRolePolicyDocument": { 367 | "Statement": [ 368 | { 369 | "Action": [ 370 | "sts:AssumeRole" 371 | ], 372 | "Effect": "Allow", 373 | "Principal": { 374 | "Service": [ 375 | "ecs-tasks.amazonaws.com" 376 | ] 377 | } 378 | } 379 | ] 380 | } 381 | }, 382 | "Type": "AWS::IAM::Role" 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2015 clair authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # The values specified here are the default values that Clair uses if no configuration file is specified or if the keys are not defined. 16 | clair: 17 | database: 18 | # Database driver 19 | type: pgsql 20 | options: 21 | # PostgreSQL Connection string 22 | # https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING 23 | source: host=localhost port=5432 user=postgres password=db_password sslmode=disable statement_timeout=60000 24 | 25 | # Number of elements kept in the cache 26 | # Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database. 27 | cachesize: 16384 28 | 29 | api: 30 | # API server port 31 | port: 6060 32 | 33 | # Health server port 34 | # This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server. 35 | healthport: 6061 36 | 37 | # Deadline before an API request will respond with a 503 38 | timeout: 900s 39 | 40 | # 32-bit URL-safe base64 key used to encrypt pagination tokens 41 | # If one is not provided, it will be generated. 42 | # Multiple clair instances in the same cluster need the same value. 43 | paginationkey: 44 | 45 | # Optional PKI configuration 46 | # If you want to easily generate client certificates and CAs, try the following projects: 47 | # https://github.com/coreos/etcd-ca 48 | # https://github.com/cloudflare/cfssl 49 | servername: 50 | cafile: 51 | keyfile: 52 | certfile: 53 | 54 | updater: 55 | # Frequency the database will be updated with vulnerabilities from the default data sources 56 | # The value 0 disables the updater entirely. 57 | interval: 2h 58 | 59 | notifier: 60 | # Number of attempts before the notification is marked as failed to be sent 61 | attempts: 3 62 | 63 | # Duration before a failed notification is retried 64 | renotifyinterval: 2h 65 | 66 | http: 67 | # Optional endpoint that will receive notifications via POST requests 68 | endpoint: 69 | 70 | # Optional PKI configuration 71 | # If you want to easily generate client certificates and CAs, try the following projects: 72 | # https://github.com/cloudflare/cfssl 73 | # https://github.com/coreos/etcd-ca 74 | servername: 75 | cafile: 76 | keyfile: 77 | certfile: 78 | 79 | # Optional HTTP Proxy: must be a valid URL (including the scheme). 80 | proxy: --------------------------------------------------------------------------------