In an earlier blog post, I gave a quick tutorial how to set up a Clair environment fast. This small setup was suitable to introduce Clair and get acclimated with the software but was too simple for a real-world scenario.
So as I promised in the last post, I will now show a more complex setup, which represents a real-world implementation. This time you only need a working Docker and docker-compose installation. First of all, we take a look at the new docker-compose.yml. You can find the whole project here. If something is too complicated, you can always go back to the first post.

version: "3.5"

services:
  registry:
    image: registry:latest
    container_name: clair-registry
    ports:
      - "5000:5000"
    networks:
      - clair

  registry.local:
    image: abiosoft/caddy
    container_name: clair-registry.local
    volumes:
      - ./Caddyfile:/etc/Caddyfile
    ports:
      - "443:443"
      - "80:80"
    depends_on:
      - registry
    networks:
      - clair

  dind:
    image: docker:18-dind
    container_name: clair-dind
    privileged: true
    networks:
      - clair
    command: ["--insecure-registry=registry.local"]

  registry.content:
    image: docker:18-dind
    container_name: clair-registry.content
    volumes:
      - ./importImages.sh:/importImages.sh
    environment:
      DOCKER_HOST: tcp://dind:2375
    depends_on:
      - dind
      - registry
    networks:
      - clair
    command: ["/importImages.sh"]

  postgres:
    image: postgres:latest
    container_name: clair-postgres
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ""
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    networks:
      - clair

  clair:
    image: quay.io/coreos/clair:latest
    container_name: clair
    restart: unless-stopped
    depends_on:
      - postgres
    ports:
      - "6060-6061:6060-6061"
    volumes:
      - /tmp:/tmp
      - ./clair_config:/config
    networks:
      - clair
    command: ["-insecure-tls", "-config", "/config/config.yaml"]

  klar:
    build:
      context: klar
    image: klar:latest
    container_name: clair-klar
    environment:
      CLAIR_ADDR: clair:6060
      CLAIR_OUTPUT: Low
      CLAIR_THRESHOLD: 10
      REGISTRY_INSECURE: "true"
    networks:
      - clair
    command: ["$IMAGE"]
  
  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
    networks: 
      - clair

networks:
  clair:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 10.0.0.0/24

You can see at the bottom of the file right above this paragraph that we introduce a Docker network to the mix. Every container in our environment is now connected to his remaining colleagues. This also means some containers can`t be accessed from the outside.
To make an accessible container outside of this network, one can declare the ports tag in the YAML file. As always in the Docker specification, the first parameter is for the host environment, and the second parameter describes the container. For example, if one would find this construct "8080:80" (Host/Container) in a file, it would mean that the container is accessible on port 8080 on the host and the traffic goes into port 80 in the container. Now we will take an in-depth look at each container.

  postgres:
    image: postgres:latest
    container_name: clair-postgres
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ""
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    networks:
      - clair

  clair:
    image: quay.io/coreos/clair:latest
    container_name: clair
    restart: unless-stopped
    depends_on:
      - postgres
    ports:
      - "6060-6061:6060-6061"
    volumes:
      - /tmp:/tmp
      - ./clair_config:/config
    networks:
      - clair
    command: ["-insecure-tls", "-config", "/config/config.yaml"]

The two shown containers are Clair and its database. They work the same as last time. We still use Klar in this setup for the communication between the Registry and Clair, but this time the software gets its own container.

  klar:
    build:
      context: klar
    image: klar:latest
    container_name: clair-klar
    environment:
      CLAIR_ADDR: clair:6060
      CLAIR_OUTPUT: Low
      CLAIR_THRESHOLD: 10
      REGISTRY_INSECURE: "true"
    networks:
      - clair
    command: ["$IMAGE"]

Instead of using the terminal of the host, we now use this container to send Docker images from the Registry to Clair. We do this to access the Docker network we defined. With command, we set an instruction where we can define the image which should be analyzed by Clair. But this is a topic for later. We also don't fill the Registry with Docker images from our host anymore. Instead, we also use containers for this task.

  dind:
    image: docker:18-dind
    container_name: clair-dind
    privileged: true
    networks:
      - clair
    command: ["--insecure-registry=registry.local"]

  registry.content:
    image: docker:18-dind
    container_name: clair-registry.content
    volumes:
      - ./importImages.sh:/importImages.sh
    environment:
      DOCKER_HOST: tcp://dind:2375
    depends_on:
      - dind
      - registry
    networks:
      - clair
    command: ["/importImages.sh"]

To achieve our goal of stocking the Registry with images to test, we use DinD or Docker in Docker. This is used when you need to execute Docker instructions inside of a container. The service dind is used as Docker daemon for the service registry.content. The second service pushes the images through the first service onto the Registry. With the file importImages.sh we specify which images are pushed.

#!/usr/bin/env sh

set -ex

docker pull ubuntu:16.04
docker tag ubuntu:16.04 registry.local/ubuntu-test
docker push registry.local/ubuntu-test

If you want to add additional images to the Registry, which later can be analyzed, you can add them in the same manner at the end of the file. For the Registry itself, we have two containers, first the Registry and secondly a Caddy server that is used as Proxy.

  registry:
    image: registry:latest
    container_name: clair-registry
    ports:
      - "5000:5000"
    networks:
      - clair

  registry.local:
    image: abiosoft/caddy
    container_name: clair-registry.local
    volumes:
      - ./Caddyfile:/etc/Caddyfile
    ports:
      - "443:443"
      - "80:80"
    depends_on:
      - registry
    networks:
      - clair

The proxy emulates a more realistic environment where it is possible to control the traffic to the private Docker registry. You can find the config in the Caddyfile inside the project. In our case, it just sends the data to the specified routes.

https://registry.local, http://registry.local {
  tls self_signed

  proxy /v2 registry:5000 {
		transparent
	}

  proxy /v1 registry:5000 {
		transparent
	}

  log / stdout
}

Now we have examined every container needed in the process and can start analyzing our container for vulnerabilities. We begin by launching the environment. Open a terminal in the project folder and execute the following command:

$  docker-compose up -d 

When the setup was successful, you should see the following containers with:

$ docker container ls

clair-env
Some of the defined containers in the compose don't stay up and instead terminate. For example, the Klar container is started to execute one command and shuts down when finished. Now we push the images defined in the importImages.sh to our Registry. With the following directive, we start up a single service from the docker-compose file:

$ docker-compose up registry.content

Now the Registry is filled with our images, and we can finally analyze them. For this the earlier defined command for our Klar container comes in handy. Again we start a single container from the compose and give him the container name as parameter with this command:

$ IMAGE=registry.local:80/ubuntu-test docker-compose up klar

Now you should receive the results of Clair's analyzation process printed in your terminal.
Selection_002
In this blog, we learned to set up an advanced Clair environment that represents almost a production state, that you can adapt for your own projects. Again the whole source code is found here.
If you want to take a look at the database and its contents, I also added an Adminer instance you can access with the following credentials at localhost:8080.
system: PostgreSQL
server: postgres
user: postgres
password: postgres