7 Easy-to-Follow Best Practices for Writing Dockerfile

Tags: docker, security, codereview, beginners
Published: 2021-10-13 15:59:46

Dockerfile allows you to create container images in such a way as if you were writing shell scripts. This simplicity is excellent indeed, but **it does NOT mean that you don't need to learn good practices for writing Dockerfile**. Just a few practices will make your container images more optimized and secure.

**This article shows a list of 7 best practices for writing Dockerfile along with some [Shisho](https://github.com/flatt-security/shisho) rules to detect the issues**. You may not know Shisho, but it's okay because Shisho is extremely easy to use; all you need to do for checking your Dockerfile is just by running the following command:

```shell
curl https://raw.githubusercontent.com/security-aware-repo-examples/dockerfile-best-practice/master/rules/docker.shisho.yaml > docker.shisho.yaml && \
    docker run -i -v $(pwd):/workspace ghcr.io/flatt-security/shisho-cli:latest check ./docker.shisho.yaml .
```

If you're a user of GitHub Actions, you can copy [this example workflow](https://github.com/security-aware-repo-examples/dockerfile-best-practice/blob/master/.github/workflows/security.yaml) and [the rule file](https://github.com/security-aware-repo-examples/dockerfile-best-practice/blob/master/shisho.yaml) in [security-aware-repo-examples/dockerfile-best-practice](https://github.com/security-aware-repo-examples/dockerfile-best-practice) to your repository. The workflow checks your repository with the rule, and it reports them to GitHub Code Scanning. The issues will appear at `https://github.com/<your org>/<your repository>/security/code-scanning`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v71m9uscc6b492bkfrke.png) 

Let's get started!

---

## :recycle: Three Practices for Maintainability


### 1. Avoid to use `latest` tag for immutability

A `latest` tag is used to create a docker image whose base is the latest version of another image. The use of `latest` tag, however, might cause confusion and inconsistent behaviour among built images. **It is better to pin the version of your base images if possible**.

The following Shisho rule will detect the use of `latest` tag:

```yaml=
version: '1'
rules:
  - id: 'use-fixed-tag-sfor-immutablity'
    language: dockerfile
    message: |
      The use of `latest` tag might cause confusion and inconsistent behavior in automated builds. It is better to pin the version of your base images.
    patterns: 
      - pattern: FROM :[IMAGE]
      - pattern: FROM :[IMAGE] as :[ALIAS]
      - pattern: FROM :[IMAGE]:latest
      - pattern: FROM :[IMAGE]:latest as :[ALIAS]
      - pattern: FROM :[IMAGE]@:[HASH]
      - pattern: FROM :[IMAGE]@:[HASH] as :[ALIAS]
      - pattern: FROM :[IMAGE]:latest@:[HASH]
      - pattern: FROM :[IMAGE]:latest@:[HASH] as :[ALIAS]
```

[(You can see the working example of this rule at Shisho Playground)](https://play.shisho.dev/p/pCUKzhjNrCdI)


### 2. Use an idiomatic way to run `apt-get install`

You may often run `apt-get` inside your Dockerfile as follows:

```dockerfile
RUN apt-get update
RUN apt-get install -y nginx <... and some more packages>
```

However, the example above has two issues:

- When the cache image for the first `RUN` is available, the second `apt-get` might install old packages due to not running `apt-get update`.
- Some layers for this image have cache files for `apt-get`, resulting in larger image size.

A simple way to address these issues is running `apt-get update` and `apt-get install` in a single `RUN` instruction.  **The following examples show an idiomatic way to run them once while removing `/var/lib/apt/lists/*` (i.e. apt caches) to reduce the image size more**:

```dockerfile=
RUN apt-get update && \
    apt-get install -y nginx && \
    rm -rf /var/lib/apt/lists/*
```

The following Shisho rule will enforce the use of this idiom:

```yaml=
version: '1'
rules:
  - id: 'remove-cache-of-apt-get'
    language: dockerfile
    message: |
      It is better to remove cache files of `apt-get` to keep your image slim.
    pattern: |
      RUN apt-get install :[X]
    rewrite: |
        RUN apt-get update && \
            apt-get install :[X] && \
            rm -rf /var/lib/apt/lists/*
```

[(You can see the working example of this rule at Shisho Playground)](https://play.shisho.dev/p/MXSj8P0WnLff)

### 3. Use `--no-install-recommends` flag of `apt-get`

It is even better to avoid to install any unnecessary tools by `--no-install-recommends` of `apt-get` command. The following Shisho rule may help the use of this flag:

```yaml=
version: "1"
rules:
  - id: "use-no-install-recommends-flag-apt-get"
    language: dockerfile
    message: |
      You can avoid to install any unnecessary tools by `--no-install-recommends` on `apt-get`.
    patterns:
      - pattern: |
          RUN apt-get install :[...X]
      - pattern: |
          RUN :[...Y] apt-get install :[...X]
    constraints:
      - target: X
        should: not-match
        regex-pattern: ".*--no-install-recommends.*"
    rewrite_options:
      - |
        RUN :[Y] apt-get install --no-install-recommends :[X]
```

[(You can see the working example of this rule at Shisho Playground)](https://play.shisho.dev/p/61eWV60xJ6S9)

---

## :shield: Four Practices for Security

While container technology brings many advantages, such as portability or isolation, it also creates new threats from a security perspective. This section describes common docker security issues and explains how to avoid them.

### 4. Avoid to store secrets in environment variables

Hardcoded secrets in your Dockerfile will be stored in resulting images. **You should avoid to embed the secrets.** You can inject environment variables at run-time instead.

```yaml=
version: '1'
rules:
  - id: 'avoid-to-store-secrets-in-env'
    language: dockerfile
    message: |
      Hardcoded secrets in your Dockerfile will be stored in resulting images. Please consider to stop embedding the secrets. 
    pattern: |
      ENV :[...] :[KEY]=:[VALUE] :[...]
    constraints:
      - target: KEY
        should: match-any-of
        regex-patterns:
          - "[sS][eE][cC][rR][eE][tT]"
          - "[tT][oO][kK][eE][nN]"
          # ... add as you like ...
```

[(You can see the working example of this rule at Shisho Playground)](https://play.shisho.dev/p/0CzbZm6GsHjP)

### 5. Use trusted base images

A docker image consists of multiple layers, and some of them are usually derived from the base image. Here exists a risk of supply chain attacks! **It is better to use official images to reduce the risk as much as possible**.

```yaml=
version: '1'
rules:
  - id: 'use-docker-official-images'
    language: dockerfile
    message: |
      It is better to use official images to reduce the risk of supply chain attacks.
    patterns: 
      - pattern: FROM :[IMAGE]
      - pattern: FROM :[IMAGE] as :[ALIAS]
      - pattern: FROM :[IMAGE]::[TAG]
      - pattern: FROM :[IMAGE]::[TAG] as :[ALIAS]
      - pattern: FROM :[IMAGE]@:[HASH]
      - pattern: FROM :[IMAGE]@:[HASH] as :[ALIAS]
      - pattern: FROM :[IMAGE]::[TAG]@:[HASH]
      - pattern: FROM :[IMAGE]::[TAG]@:[HASH] as :[ALIAS]
    constraints:
      - target: IMAGE
        should: match
        regex-pattern: "/"
```

[(You can see the working example of this rule at Shisho Playground)](https://play.shisho.dev/p/4kK3OmX8WZ7E)

If you already have a list of trusted base images, some slight changes will let you use them:

```yaml=
version: '1'
rules:
  - id: 'use-trusted-base-images'
    language: dockerfile
    message: |
      It is better to use trusted base images to reduce the risk of supply chain attacks.
    patterns: 
      - pattern: FROM :[IMAGE]
      - pattern: FROM :[IMAGE] as :[ALIAS]
      - pattern: FROM :[IMAGE]::[TAG]
      - pattern: FROM :[IMAGE]::[TAG] as :[ALIAS]
      - pattern: FROM :[IMAGE]@:[HASH]
      - pattern: FROM :[IMAGE]@:[HASH] as :[ALIAS]
      - pattern: FROM :[IMAGE]::[TAG]@:[HASH]
      - pattern: FROM :[IMAGE]::[TAG]@:[HASH] as :[ALIAS]
    constraints:
      - target: IMAGE
        should: not-match-any-of
        regex-patterns:
            - "^[^/]+$"
            - "^image-name/you-trust$"
```

[(You can see the working example of this rule at Shisho Playground)](https://play.shisho.dev/p/VkWKIes7zyXa)

### 6. Avoid to run `curl <...> | sh`

Many shell scripts and dockerfiles use `curl <...> | sh` as an idiomatic way to run an external script inside computers/containers, but this idiom has a risk of remote code execution by attackers through MITM attacks or compromising the distributed script itself (see [the report of recent codecov incident](https://about.codecov.io/security-update/)). **It is better to check the integrity of what you download before running it as a shell script.**

```yaml=
version: "1"
rules:
  - id: "check-integrity-of-downloaded-shell-script"
    title: Check the integrity of downloaded shell scripts
    language: dockerfile
    message: |
      It is better to check the integrity of what you download before running it as a shell script.
    pattern: |
      RUN :[CMD]
    constraints:
      - target: CMD
        should: match-any-of
        regex-patterns:
          - curl[^|^>]*[|>]
          - wget[^|^>]*[|>]
```

[(You can see the working example of this rule at Shisho Playground)](https://play.shisho.dev/p/pBMNlxgalIOn)

### 7. Use `COPY` instead of `ADD`

`ADD` instruction allows us to fetch resources over the network and extract an archive, but it may cause security issues such as Meet-in-the-Middle (MITM) attacks or Zip Slip vulnerabilities. **It is better to use `COPY` instead of `ADD` if possible.**

The following Shisho rule detects the use of `ADD` instructions:

```yaml=
version: '1'
rules:
  - id: 'use-copy-instead-of-add'
    language: dockerfile
    message: |
      ADD instruction allows us to fetch resources over network and extract an archive, but it may cause security issues such as Meet-in-the-Middle (MITM) attacks or Zip Slip vulnerabilities.
    pattern: ADD :[FROM] :[TO]
    rewrite: COPY :[FROM] :[TO]
```

[(You can see the working example of this rule at Shisho Playground)](https://play.shisho.dev/p/9Y9EEnLVKiJr)

---

# Conclusion

In this article, I presented some best practices for writing Dockerfile and demonstrated how to check your Dockerfile is following the practices continuously by [Shisho](https://github.com/flatt-security/shisho). 

You can refer [security-aware-repo-examples/dockerfile-best-practice](https://github.com/security-aware-repo-examples/dockerfile-best-practice) to see the working example.

{% github security-aware-repo-examples/dockerfile-best-practice %}

If you have an interest in Shisho, I would be appreciated if you starred the following repository and gave me feedback at [discussions](https://github.com/flatt-security/shisho/discussions)!

{% github flatt-security/shisho %}

---

NOTE: I'm working on [Shisho Cloud](https://shisho.dev/?utm_source=devto&utm_medium=referral&utm_campaign=post20211014), a web service providing this kind of best practice checks for infrastructure-as-code and automated patch generation for detected issues. It's in a beta stage now and all features are available for free. Please try it!