Pular para o conteúdo principal

Hadolint: Analisador de Dockerfiles

hadolint

O Hadolint (Haskell Dockerfile Linter) é uma ferramenta que analisa Dockerfiles para detectar erros, más práticas e vulnerabilidades. Ele segue as recomendações do Docker Best Practices e do ShellCheck (para comandos RUN).

Podemos fazer esses checks de dockerfile também utilizando o Trivy, mas é bom conhecer ferramentas para ter no seu arsenal de tools.

O Hadolint tem algumas vantagens em relação a outras ferramentas de lint para Dockerfiles, como Dockle e Trivy, dependendo do contexto de uso. Aqui estão os principais diferenciais:

  1. O Hadolint é focado exclusivamente em boas práticas de escrita de Dockerfiles.
  2. Segue as recomendações oficiais do Docker, garantindo eficiência e segurança.
  3. Também usa o ShellCheck para validar comandos RUN, ajudando a evitar erros comuns de shell script.
  4. Hadolint roda localmente e rapidamente sem depender de um banco de dados externo.
  5. Pode ser facilmente integrado ao GitHub Actions, GitLab CI, Jenkins, etc., para garantir que Dockerfiles sigam padrões antes do build.
  6. Permite desativar regras específicas caso necessário.

Limitações:

  • Não detecta vulnerabilidades em pacotes ou imagens (ao contrário do Trivy e Dockle).
  • Focado apenas em Dockerfiles (se precisar de linting para docker-compose.yml, é necessário outra ferramenta).

Existe uma extensão no vscode que automaticamente detecta o dockerfile e já dá os insights, mas é necessário que o binário do hadolint esteja instalado.

Instalação

A instalação é simples, somente fazer o download do binário na página de release e colocar no path, igual a maioria dos binários de cli que temos por ai. Se tiver o brew pode utilizar o comando abaixo. Também é possível rodar usando container, isso é bom para pipeline, se vai integrar no seu vscode é necessário instalar o binário.

 brew install hadolint  

hadolint --version
Haskell Dockerfile Linter 2.12.0

hadolint - Dockerfile Linter written in Haskell

Usage: hadolint [-v|--version] [-c|--config FILENAME] [DOCKERFILE...]
[--file-path-in-report FILEPATHINREPORT] [--no-fail]
[--no-color] [-V|--verbose] [-f|--format ARG] [--error RULECODE]
[--warning RULECODE] [--info RULECODE] [--style RULECODE]
[--ignore RULECODE]
[--trusted-registry REGISTRY (e.g. docker.io)]
[--require-label LABELSCHEMA (e.g. maintainer:text)]
[--strict-labels] [--disable-ignore-pragma]
[-t|--failure-threshold THRESHOLD]

Lint Dockerfile for errors and best practices

Available options:
-h,--help Show this help text
-v,--version Show version
-c,--config FILENAME Path to the configuration file
--file-path-in-report FILEPATHINREPORT
The file path referenced in the generated report.
This only applies for the 'checkstyle' format and is
useful when running Hadolint with Docker to set the
correct file path.
--no-fail Don't exit with a failure status code when any rule
is violated
--no-color Don't colorize output
-V,--verbose Enables verbose logging of hadolint's output to
stderr
-f,--format ARG The output format for the results [tty | json |
checkstyle | codeclimate | gitlab_codeclimate | gnu |
codacy | sonarqube | sarif] (default: tty)
--error RULECODE Make the rule `RULECODE` have the level `error`
--warning RULECODE Make the rule `RULECODE` have the level `warning`
--info RULECODE Make the rule `RULECODE` have the level `info`
--style RULECODE Make the rule `RULECODE` have the level `style`
--ignore RULECODE A rule to ignore. If present, the ignore list in the
config file is ignored
--trusted-registry REGISTRY (e.g. docker.io)
A docker registry to allow to appear in FROM
instructions
--require-label LABELSCHEMA (e.g. maintainer:text)
The option --require-label=label:format makes
Hadolint check that the label `label` conforms to
format requirement `format`
--strict-labels Do not permit labels other than specified in
`label-schema`
--disable-ignore-pragma Disable inline ignore pragmas `# hadolint
ignore=DLxxxx`
-t,--failure-threshold THRESHOLD
Exit with failure code only when rules with a
severity equal to or above THRESHOLD are violated.
Accepted values: [error | warning | info | style |
ignore | none] (default: info)

O comando é simples, somente hadolint acrescido do dockerfile que deseja avaliar. A maioria das opções acima podem passadas direto no comando ou definidas em um arquivo yaml e passado no flag --config que veremos já adiante.

Vamos para um exemplo simples inicial de dockerfile.

cat <<EOF > Dockerfile
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
RUN cd /home && mkdir app
COPY app /home/app
CMD ["bash"]
EOF

hadolint Dockerfile
Dockerfile:1 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile:2 DL3009 info: Delete the apt-get lists after installing something
Dockerfile:3 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
Dockerfile:3 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
Dockerfile:3 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile:5 DL3003 warning: Use WORKDIR to switch to a directory

Esse Dockerfile funciona, mas veja que temos alguns warning e info que poderiam ser melhorados e vamos alterar.

# Resolvendo DL3007 warning: Pin the version explicitly to a release tag
# Forçamos a tag específica ao invés de manter a latest
FROM ubuntu:24.04
# Resolvendo
# DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
# Evita que pacotes recomendados sejam instalados juntos com o pacote principal automaticamente.
# DL3008 warning: Pin versions in apt get install.
# Fixamos as versões dos pacotes.
# DL3009 info: Delete the apt-get lists after installing something
# apt-get clean não é mais necessário quando utilizamos rm -rf /var/lib/apt/lists/* após a instalação dos pacotes.
# A razão é que esse comando realiza uma limpeza mais eficaz e completa
# DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
# Colocamos tudo dentro de um único RUN para evitar várias layers.
RUN apt-get update && apt-get install -y --no-install-recommends curl=7.88.1-10ubuntu4 \
&& rm -rf /var/lib/apt/lists/*
# DL3003 warning: Use WORKDIR to switch to a directory
# Vamos trabalhar com workdir ao invés de criar uma pasta
WORKDIR /home/app
COPY app/ .
CMD ["bash"]

Se quisermos manter a instalação do curl sempre na ultima versão teríamos o erro abaixo, mas podemos ignorar caso necessário.

cat <<EOF > Dockerfile
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /home/app
COPY app/ .
CMD ["bash"]
EOF

hadolint Dockerfile
Dockerfile:2 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`

hadolint Dockerfile --ignore DL3008

Também poderíamos colocar esse ignore como comentário dentro do arquivo Dockerfile e tudo funcionaria igual sem precisar passar a flag --ignore.

cat <<EOF > Dockerfile
FROM ubuntu:24.04
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /home/app
COPY app/ .
CMD ["bash"]
EOF

hadolint Dockerfile

# Podemos desativar os comentários
hadolint Dockerfile --disable-ignore-pragma
Dockerfile:3 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`

Apesar do hadolint seguir dá os exemplos de boas práticas na definição do Dockerfile ele não ajuda a Hardenizar o container.

Observe que ele permitiu que rodassemos esse container como root, pois ele não sabe o propósito desse container. Podemos conferir esses itens de vulnerabilidade com o trivy. Vamos rodar com o trivy para ver se pega alguma diferença.

 ❯ trivy config Dockerfile
2025-02-02T02:56:18-03:00 INFO [misconfig] Misconfiguration scanning is enabled
2025-02-02T02:56:19-03:00 INFO Detected config files num=1

Dockerfile (dockerfile)

Tests: 28 (SUCCESSES: 26, FAILURES: 2)
Failures: 2 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 1, CRITICAL: 0)

AVD-DS-0002 (HIGH): Specify at least 1 USER command in Dockerfile with non-root user as argument
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.

See https://avd.aquasec.com/misconfig/ds002
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers.

See https://avd.aquasec.com/misconfig/ds026
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Além de não rodar como root o trivy sugeriu colocar um healthcheck. Nesse caso é um container simples sem aplicação para conferir, mas podemos ter um healthcheck conferindo se o bash está ativo para sumir com esse warning do tipo LOW do trivy.

Por outro lado o trivy não reclamou sobre fixar a versão dos pacotes como o hadolint fez, mostrando que essas ferramentas se completam.

Então o correto seria...

FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends curl=8.5.0-2ubuntu10.6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /home/app
COPY . app/
USER appuser
HEALTHCHECK CMD ps aux | grep -q 'bash' || exit 1
CMD ["bash"]

Uma coisa para se observar a versão do curl curl=7.88.1-10ubuntu4 não existe no ubuntu 24.04 e o hadolint não reclamou disso. Ele não builda o container para conferir nada somente confere a definição do seu dockerfile e sugere as melhores práticas.

Se voltassemos a nossa definição inicial cheia de problemas o que o trivy falaria sobre isso?

 trivy config Dockerfile
2025-02-02T03:02:35-03:00 INFO [misconfig] Misconfiguration scanning is enabled
2025-02-02T03:02:36-03:00 INFO Detected config files num=1

Dockerfile (dockerfile)

Tests: 28 (SUCCESSES: 22, FAILURES: 6)
Failures: 6 (UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 0)

# O trivy também detectou em fixar a tag da imagem
AVD-DS-0001 (MEDIUM): Specify a tag in the 'FROM' statement for image 'ubuntu'
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.

See https://avd.aquasec.com/misconfig/ds001
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Dockerfile:1
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 [ FROM ubuntu:latest
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


AVD-DS-0002 (HIGH): Specify at least 1 USER command in Dockerfile with non-root user as argument
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.

See https://avd.aquasec.com/misconfig/ds002
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

# O trivy também detectou que deveriamos remover os caminhos relativos e usar o WORKDIR
AVD-DS-0013 (MEDIUM): RUN should not be used to change directory: 'cd /home && mkdir app'. Use 'WORKDIR' statement instead.
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
Use WORKDIR instead of proliferating instructions like 'RUN cd … && do-something', which are hard to read, troubleshoot, and maintain.

See https://avd.aquasec.com/misconfig/ds013
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Dockerfile:5
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
5 [ RUN cd /home && mkdir app
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

# Também pediu para rodar as coisas no mesmo RUN para gerar menos layers

AVD-DS-0017 (HIGH): The instruction 'RUN <package-manager> update' should always be followed by '<package-manager> install' in the same RUN statement.
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
The instruction 'RUN <package-manager> update' should always be followed by '<package-manager> install' in the same RUN statement.

See https://avd.aquasec.com/misconfig/ds017
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Dockerfile:2
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2 [ RUN apt-get update
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers.

See https://avd.aquasec.com/misconfig/ds026
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

# Também pediu evitar instalar os recomendados
AVD-DS-0029 (HIGH): '--no-install-recommends' flag is missed: 'apt-get install -y curl'
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
'apt-get' install should use '--no-install-recommends' to minimize image size.

See https://avd.aquasec.com/misconfig/ds029
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Dockerfile:3
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3 [ RUN apt-get install -y curl
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

O que o Trivy não fez foi pedir para fixar as versões dos pacotes e limpar o apt-get lists depois da instalação.

O que podemos perceber é que elas essas ferramentas se completam e devem ser usadas em conjunto para garantir uma melhor analise e criação containers.