Setting up the Docker image scan GitHub Action

wordpress-sync/feature-docker-secrets

May 19, 2023

0 mins read

Nowadays, the final product of most Git repositories is a Docker image, that is then used in a Kubernetes deployment. With security being a hot topic now (and for good reasons), it would be  scanning the Docker images you create in the CI is vital.

In this piece, I’ll use GitHub Actions to build Docker images and then scan them for security vulnerabilities. The Docker image built in the CI is also pushed to GitHub’s Docker registry.

blog-docker-scan-github-action

Creating a GitHub Actions workflow

The CI workflow we’re going to create has the following structure:

  • Test

  • Build image

  • Scan image

The image built in the second step is then pushed to the GitHub Docker registry and again pulled from it in the third stage.

Create the test job

Assuming your GitHub repo has no CI configuration, we’re going to create a file in the path `.github/workflows/ci.yaml` with the following content:

1    name: ci
2    on:
3      push:
4        branches:
5          - master
6      pull_request:
7
8    jobs:
9      test:
10        runs-on: ubuntu-latest
11
12        strategy:
13          matrix:
14            node-version: [ 18.x ]
15
16        steps:
17          - name: Checkout 🛎️
18            uses: actions/checkout@v3
19
20          - name: Setup Node environment 🧱: Node.js ${{ matrix.node-version }}
21            uses: actions/setup-node@v3
22            with:
23              node-version: ${{ matrix.node-version }}
24
25          - name: Install and test 🪲
26            run: |
27              npm ci
28              npm test

The `on` attribute specifies the events that trigger this workflow to run. In this case, any push to the branch `master` and every pull request triggers the CI workflow.

The job steps include the following:

  • Checkout 🔔: Gets the code from the repo

  • Setup Node environment 🧱: Installs Node.js 18 environment

  • Install and test 🪲: Install dependencies and runs the tests

The test job here is merely an example of running the unit tests of a Node.js project. Feel free to alter it to match your project language and structure.

Although it’s not necessary to have a test job to build the Docker image, it’s a good practice, so I stuck to it.

Create the Docker build job

To create the Docker build job, I would first create two environment variables at the beginning of the file under `on`:

1  on:
2      ...
3
4    env:
5      DOCKER_IMAGE_TAG: ${{ github.ref == 'refs/heads/master' && 'prod-' || 'dev-' }}${{ github.sha }}
6      GITHUB_REGISTRY: ghcr.io
7      GITHUB_REPOSITORY: ${{ github.repository }}

Then, let’s create the `build_docker job`:

1    jobs:
2      test:
3        ...
4
5      build_image:
6        permissions:
7          id-token: write
8          contents: read
9          packages: write
10        runs-on: ubuntu-latest
11        needs: [ test ]
12
13        steps:
14          - name: Checkout 🛎️
15            uses: actions/checkout@v2
16
17          - name: Log in to the Container registry 📦
18            uses: docker/login-action@v2
19            with:
20              registry: ${{ env.GITHUB_REGISTRY }}
21              username: ${{ github.actor }}
22              password: ${{ secrets.GITHUB_TOKEN }}
23
24          - name: Build and push Docker image 🐳
25            uses: docker/build-push-action@v3
26            with:
27              push: true
28              tags: |
29                ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_REPOSITORY }}:${{ env.DOCKER_IMAGE_TAG }}
30

Let’s look into the code. The permissions section is needed as we want to push Docker images into GitHub’s registry.

The job runs on the latest Ubuntu version and requires the test job to finish. That’s to ensure we’re only creating Docker images for functioning code. If you skipped the test job, remove the needs line.

Now to the steps:

  • Checkout 🔔: Gets the code from the repo. This is required to get the Dockerfile and its context.

  • Log in to the Container registry 📦

  • Build and push Docker image 🐳: In this step, we use the environment variables we introduced at the beginning of the file to specify the Docker image’s tag.

Create the scan job

Now, let’s add the Docker image scan job:

1    jobs:
2      test:
3        ...
4
5      build_image:
6        ...
7
8      scan_docker_image:
9        permissions:
10          id-token: read
11          contents: read
12          packages: read
13        runs-on: ubuntu-latest
14        needs: [ build_image ]
15        steps:
16          - name: Checkout 🛎️
17            uses: actions/checkout@v2
18
19          - name: Log in to the Container registry 📦
20            uses: docker/login-action@v2
21            with:
22              registry: ${{ env.GITHUB_REGISTRY }}
23              username: ${{ github.actor }}
24              password: ${{ secrets.GITHUB_TOKEN }}
25
26          - name: Scan Docker image 🐳
27            uses: snyk/actions/docker@master
28            continue-on-error: true
29            with:
30              image: ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_REPOSITORY }}:${{ env.DOCKER_IMAGE_TAG }}
31              args: --file=Dockerfile --severity-threshold=high --sarif-file-output=snyk.sarif
32            env:
33              SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
34
35          - name: Upload Snyk report as sarif 📦
36            uses: github/codeql-action/upload-sarif@v2
37            with:
38              sarif_file: snyk.sarif

This step uses Snyk, the security scanning engine behind `docker scan`. To use it, you need to create a free account and store its token in a secret:

  • Sign up here

  • Get the token as described here

  • Add it to your GitHub repo’s CI secrets with the name `SNYK_TOKEN` as explained in the appendix

Now on with the steps:

  • Checkout 🛎️: The scanner performs better if it has access to the Dockerfile as well.

  • Log in to the Container registry 📦: To get the Docker images we pushed there earlier.

  • Scan Docker image 🐳: This job scans the Docker image and reports the vulnerabilities in a file called `snyk.sarif`. This file format is recognized by GitHub and can be shown in the PR — which is why we have the next step

  • Upload Snyk report as sarif 📦: Here we upload the `sarif` file we generated in the previous step and upload it to GitHub

The vulnerabilities uploaded to GitHub show up on your PR like this:

blog-docker-scan-github-action-unchanged-files

Conclusion


In this piece, we created a GitHub Actions workflow with 3 jobs that would run the tests, build the Docker image, push it to the GitHub registry, check it for security issues, and upload the vulnerability report so that GitHub would understand and display them in the PRs.

Appendix: Add CI Secret on GitHub

In your repo:

  1. Click on the settings tab.

  2. In the menu panel on the left, from the Security section, click on Secrets and variables.

  3. Then, from the newly appeared menu items, click on Actions.

  4. On the top right, click on the green button that says New repository secret.

  5. In the Name section, write down the desired name, e.g. `SNYK_TOKEN`.

  6. In the Secret section, paste the secret, e.g. the Snyk token you copied from Snyk website.

  7. Then, click the green button that says Add secret.

blog-docker-scan-github-action-setting

Patch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo SegmentPatch Logo Segment

Snyk is a developer security platform. Integrating directly into development tools, workflows, and automation pipelines, Snyk makes it easy for teams to find, prioritize, and fix security vulnerabilities in code, dependencies, containers, and infrastructure as code. Supported by industry-leading application and security intelligence, Snyk puts security expertise in any developer’s toolkit.

Start freeBook a live demo