What about container security?

What about container security?

 27.08.2018, last updated 05.03.2021 -  Jeremy T. Bouse -  ~5 Minutes

With normal legacy server security you would routinely scan your servers for vulnerabilities, but with the move to using containers this methodology for vulnerability detection doesn’t exactly fit as you would typically build the container and move it along through your environments during the software development life cycle. So how do we check our containers to ensure they are as secure as our old servers? How do you know your image is still secure after it’s been built?

This was a problem that I had to address when making the move in my own environment to use a more container-based solution. While being a big proponent for open-source software, but still needing to look to work within corporate environments I had to find the solution that would work for both. Thankfully the great team over at Anchore  have worked to build just that. Anchore has built a strong open source engine  capable of doing the vulnerability scans I needed for my Docker containers, but still have the Emterprise support offerings  to make corporate leadership happy.

While Anchore had a provided solution  to deploy cleanly into a Kubernetes environment, that didn’t exactly work for me within Amazon Web Services using Elastic Container Service. I therfore had had to work my way through the caveats of AWS to get a full solution working. The process took many iterations to get to the point I have it working today, so let’s take a look at some of the aspects I had to work through to get it to work.

While designing my solution I wanted to be able to provide auto-scaling of the services that make up Anchore Engine. This was made easier in the v0.2.x version which removed the need for setting a Host ID and instead allowed to use a UUID instead. The trick however was when looking at ECS and placing Anchore Engine behind an ALB. Each ALB Listener can only listen on one port and each Anchore Engine service listened on it’s own unique port. You then add to that the fact that an ECS Service can only attach a container to exactly 1 ALB Target Group. This meant that each of the Anchore Engine services would need to be deployed separately, so instead of 1 ECS Task Definition and 1 ECS Service I would need to deploy 6 of each. These Anchore Engine services were the API (anchore-api), Kubernetes webhook (anchore-kubernetes-webhook), Policy Engine (anchore-policy-engine), Simple Queue (anchore-simplequeue), Catalog (anchore-catalog) and the Worker (anchore-analyzer). All except the worker will be placed behind the ALB so they would be assigned to their own Target Groups and Listener. The Worker talks with the other services through the ALB and doesn’t need to be directly accessible.

CatalogTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: "anchore-catalog"
TaskRoleArn: !Ref TaskRole
ContainerDefinitions:
- Name: "catalog"
Image: !Ref engineImage
PortMappings:
- HostPort: 0
ContainerPort: 8082
Cpu: 128
MemoryReservation: 256
Essential: true
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: "anchore-engine"
MountPoints:
- SourceVolume: "config"
ContainerPath: "/config/config.yaml"
ReadOnly: true
Environment:
- Name: ANCHORE_ENGINE_SERVICES
Value: anchore-catalog
- Name: ANCHORE_CLI_USER
Value: admin
- Name: ANCHORE_CLI_PASS
Value: !Ref adminPassword
- Name: ANCHORE_ADMIN_EMAIL
Value: !Ref adminEmail
- Name: ANCHORE_DB_USER
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-UserName"
- Name: ANCHORE_DB_PASSWORD
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-Password"
- Name: ANCHORE_DB_NAME
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-Name"
- Name: ANCHORE_DB_POSTGRESQL_SERVICE_HOST
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-EndPoint"
- Name: ANCHORE_DB_POSTGRESQL_SERVICE_PORT
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-Port"
- Name: ANCHORE_ENGINE_SERVICE_SERVICE_HOST
Value: !If [HasZone, !Ref RecordSet, {'Fn::ImportValue': !Sub '${ParentECSStack}-DNSName'}]
- Name: ANCHORE_ENABLE_SSL
Value: !If [HasLoadBalancerCertificateArn, 'True', 'False']
Volumes:
- Name: "config"
Host:
SourcePath: "/mnt/efs/anchore/config.yaml"
CatalogService:
Type: AWS::ECS::Service
DependsOn:
- CatalogListener
Properties:
Role: !GetAtt ServiceRole.Arn
Cluster:
Fn::ImportValue: !Sub "${ParentECSStack}-Cluster"
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DesiredCount: 1
TaskDefinition: !Ref CatalogTaskDefinition
LoadBalancers:
- TargetGroupArn: !Ref CatalogTargetGroup
ContainerPort: 8082
ContainerName: "catalog"
CatalogTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 90
HealthCheckTimeoutSeconds: 30
HealthyThresholdCount: 2
UnhealthyThresholdCount: 10
HealthCheckPath: /health
Matcher:
HttpCode: 200
Port: 80
Protocol: HTTP
VpcId:
Fn::ImportValue: !Sub "${ParentVPCStack}-VPC"
Tags:
- Key: Name
Value: "anchore-catalog"
CatalogListener:
Type : AWS::ElasticLoadBalancingV2::Listener
Properties:
Certificates:
- CertificateArn: !If [HasLoadBalancerCertificateArn, !Ref LoadBalancerCertificateArn, !Ref 'AWS::NoValue']
DefaultActions:
- Type: forward
TargetGroupArn: !Ref CatalogTargetGroup
LoadBalancerArn: {'Fn::ImportValue': !Sub '${ParentECSStack}-LoadBalancer'}
Port: 8082
Protocol: !If [HasLoadBalancerCertificateArn, 'HTTPS', 'HTTP']
SslPolicy: !If [HasLoadBalancerCertificateArn, ELBSecurityPolicy-FS-2018-06, !Ref 'AWS::NoValue']

Above you can see an excerpt from the ecs/anchore.yaml template demonstrating all the resources for the Catalog service. This is a representative example of how each of the other services (API, Simple Queue, Policy Engine and Kubernetes webhook) would be setup with one exception. Only the Catalog requires the TaskRoleArn property be set if you’re going to make use of the Elastic Container Registry (ECR). If you’re not going to use ECR then you could leave the TaskRoleArn property off completely. The rest of the services all can run without any specific IAM permissions being granted to them.

WorkerTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: "anchore-analyzer"
TaskRoleArn: !Ref TaskRole
ContainerDefinitions:
- Name: "analyzer"
Image: !Ref engineImage
Cpu: 256
MemoryReservation: 512
Essential: true
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: "anchore-engine"
MountPoints:
- SourceVolume: "config"
ContainerPath: "/config/config.yaml"
ReadOnly: true
Environment:
- Name: ANCHORE_ENGINE_SERVICES
Value: anchore-worker
- Name: ANCHORE_CLI_USER
Value: admin
- Name: ANCHORE_CLI_PASS
Value: !Ref adminPassword
- Name: ANCHORE_ADMIN_EMAIL
Value: !Ref adminEmail
- Name: ANCHORE_DB_USER
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-UserName"
- Name: ANCHORE_DB_PASSWORD
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-Password"
- Name: ANCHORE_DB_NAME
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-Name"
- Name: ANCHORE_DB_POSTGRESQL_SERVICE_HOST
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-EndPoint"
- Name: ANCHORE_DB_POSTGRESQL_SERVICE_PORT
Value:
Fn::ImportValue: !Sub "${ParentRDSStack}-Port"
- Name: ANCHORE_ENGINE_SERVICE_SERVICE_HOST
Value: !If [HasZone, !Ref RecordSet, {'Fn::ImportValue': !Sub '${ParentECSStack}-DNSName'}]
- Name: ANCHORE_ENABLE_SSL
Value: !If [HasLoadBalancerCertificateArn, 'True', 'False']
Volumes:
- Name: "config"
Host:
SourcePath: "/mnt/efs/anchore/config.yaml"
WorkerService:
Type: AWS::ECS::Service
Properties:
Cluster:
Fn::ImportValue: !Sub "${ParentECSStack}-Cluster"
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DesiredCount: 1
PlacementStrategies:
- Type: spread
Field: instanceId
TaskDefinition: !Ref WorkerTaskDefinition

The Worker analyzer as mentioned only requires the ECS TaskDefinition and Service resources to be defined and since it will also need access to ECR it has the TaskRoleArn set like the Catalog Service did. You will also note that in both excerpts I’m defining several environment variables to be made available in the TaskDefinition for the services. These are all the same regardless of the service they are listed for with the exception of the ANCHORE_ENGINE_SERVICES variable. All of the environment variables except the ANCHORE_ENGINE_SERVICES are used within the config.yaml that every service references through the bind volume mount.

# Anchore Service Configuration File
# General system-wide configuration options, these should not need to
# be altered for basic operation
#
# service_dir: '/config'
# tmp_dir: '/tmp'
# log_level: 'DEBUG'
#
allow_awsecr_iam_auto: True
cleanup_images: True
# docker_conn: 'unix://var/run/docker.sock'
# docker_conn_timeout: 600
#
#
log_level: 'INFO'
# log_level: 'DEBUG'
# This is necessary to ensure that a re-start of the pod on another host works. The string can be anything, but should be explicitly set to identify the host.
# host_id: ${ANCHORE_HOST_ID}
internal_ssl_verify: False
auto_restart_services: True
# Uncomment if you have a local endpoint that can accept
# notifications from the anchore-engine, as configured below
#
#webhooks:
# webhook_user: 'user'
# webhook_pass: 'pass'
# ssl_verify: False
# general:
# url: 'http://localhost:9090/general/<notification_type>/<userId>'
# policy_eval:
# url: 'http://localhost:9090/policy_eval/<userId>'
# webhook_user: 'mehuser'
# webhook_pass: 'mehpass'
## special webhook for FATAL service events - system will store in DB if not enabled here
# # error_event:
# # url: 'http://localhost:9090/error_event/'
# #
#
# A feeds section is available for override, but shouldn't be
# needed. By default, the 'admin' credentials are used if present,
# otherwise anonymous access for feed sync is used
#feeds:
# selective_sync:
# # If enabled only sync specific feeds instead of all.
# enabled: True
# feeds:
# vulnerabilities: True
# # Warning: enabling the package sync causes the service to require much
# # more memory to do process the significant data volume. We recommend at least 4GB available for the container
# packages: False
# anonymous_user_username: anon@ancho.re
# anonymous_user_password: pbiU2RYZ2XrmYQ
# url: 'https://ancho.re/v1/service/feeds'
# client_url: 'https://ancho.re/v1/account/users'
# token_url: 'https://ancho.re/oauth/token'
# connection_timeout_seconds: 3
# read_timeout_seconds: 60
credentials:
users:
admin:
password: ${ANCHORE_CLI_PASS}
email: '${ANCHORE_ADMIN_EMAIL}'
external_service_auths:
# anchoreio:
# anchorecli:
# auth: 'myanchoreiouser:myanchoreiopass'
#auto_policy_sync: True
database:
db_connect: "postgresql+pg8000://${ANCHORE_DB_USER}:${ANCHORE_DB_PASSWORD}@${ANCHORE_DB_POSTGRESQL_SERVICE_HOST}:${ANCHORE_DB_POSTGRESQL_SERVICE_PORT}/${ANCHORE_DB_NAME}"
db_connect_args:
timeout: 120
ssl: False
db_pool_size: 30
db_pool_max_overflow: 100
services:
apiext:
enabled: True
require_auth: True
endpoint_hostname: ${ANCHORE_ENGINE_SERVICE_SERVICE_HOST}
listen: '0.0.0.0'
port: 8228
external_tls: ${ANCHORE_ENABLE_SSL}
kubernetes_webhook:
enabled: True
require_auth: False
endpoint_hostname: ${ANCHORE_ENGINE_SERVICE_SERVICE_HOST}
listen: '0.0.0.0'
port: 8338
external_tls: ${ANCHORE_ENABLE_SSL}
catalog:
enabled: True
require_auth: True
endpoint_hostname: ${ANCHORE_ENGINE_SERVICE_SERVICE_HOST}
listen: '0.0.0.0'
port: 8082
external_tls: ${ANCHORE_ENABLE_SSL}
archive:
compression:
enabled: False
min_size_kbytes: 100
storage_driver:
name: db
config: {}
cycle_timer_seconds: '1'
cycle_timers:
image_watcher: 3600
policy_eval: 3600
vulnerability_scan: 14400
analyzer_queue: 1
notifications: 30
service_watcher: 15
policy_bundle_sync: 300
repo_watcher: 60
simplequeue:
enabled: True
require_auth: True
endpoint_hostname: ${ANCHORE_ENGINE_SERVICE_SERVICE_HOST}
listen: '0.0.0.0'
port: 8083
external_tls: ${ANCHORE_ENABLE_SSL}
analyzer:
enabled: True
require_auth: True
cycle_timer_seconds: '1'
max_threads: 1
analyzer_driver: 'nodocker'
endpoint_hostname: ${ANCHORE_ENGINE_SERVICE_SERVICE_HOST}
listen: '0.0.0.0'
port: 8084
policy_engine:
enabled: True
require_auth: True
endpoint_hostname: ${ANCHORE_ENGINE_SERVICE_SERVICE_HOST}
listen: '0.0.0.0'
port: 8087
external_tls: ${ANCHORE_ENABLE_SSL}
cycle_timer_seconds: '1'
cycle_timers:
feed_sync: 21600
feed_sync_checkera: 3600
view raw config.yaml hosted with ❤ by GitHub

Taking a look at the config.yaml file that I have my ECS cluster instances download from an S3 bucket you can see where each of the environment variables are used. You will also note the absence of the ANCHORE_ENGINE_SERVICES variable being used in the config file. This absence is not an oversight but rather that the Anchore Engine services themselves use this variable to determine which services to start up. Within the config file all of them have enabled set to true, but the ANCHORE_ENGINE_SERVICES environment variable overrides this and only starts the service listed in the variable. As we’re starting one service per container, this variable will only hold the name of that service.

The only other requirement for Anchore Engine is the need for a PostgreSQL database server. Within my environment I made use of an RDS PostgreSQL instance that I already had running. Unfortunately I’d stood it up manually without a CloudFormation template so I created a data/rds-legacy-postgres.yaml template that I provided the necessary values to be exported. I intend to move this PostgreSQL instance from the old Default VPC into my custom VPC and deploy it via CloudFormation, but that has not been done yet. By creating this legacy template I’ve made it easier on myself to migrate to this new RDS instance when I do. I also added some extra functionality into my ecs/anchore.yaml template that I will cover in another post, which includes the Enterprise UI and it’s requirement for a Redis cluster. While this covered the corporate needs I mentioned it wasn’t required to use the open-source Anchore Engine itself so I’ve left it to be its own post and outside the scope of this one.