Skip to main content

Hadolint: Analizador de Dockerfiles

hadolint

Hadolint (Haskell Dockerfile Linter) es una herramienta que analiza Dockerfiles para detectar errores, malas prácticas y vulnerabilidades. Sigue las recomendaciones de Docker Best Practices y ShellCheck (para comandos RUN).

Podemos hacer estos checks de dockerfile también utilizando Trivy, pero es bueno conocer herramientas para tener en tu arsenal de tools.

Hadolint tiene algunas ventajas en relación a otras herramientas de lint para Dockerfiles, como Dockle y Trivy, dependiendo del contexto de uso. Aquí están los principales diferenciales:

  1. Hadolint está enfocado exclusivamente en buenas prácticas de escritura de Dockerfiles.
  2. Sigue las recomendaciones oficiales de Docker, garantizando eficiencia y seguridad.
  3. También usa ShellCheck para validar comandos RUN, ayudando a evitar errores comunes de shell script.
  4. Hadolint se ejecuta localmente y rápidamente sin depender de una base de datos externa.
  5. Puede ser fácilmente integrado en GitHub Actions, GitLab CI, Jenkins, etc., para garantizar que los Dockerfiles sigan estándares antes del build.
  6. Permite desactivar reglas específicas en caso necesario.

Limitaciones:

  • No detecta vulnerabilidades en paquetes o imágenes (al contrario de Trivy y Dockle).
  • Enfocado solo en Dockerfiles (si se precisa linting para docker-compose.yml, es necesaria otra herramienta).

Existe una extensión en vscode que automáticamente detecta el dockerfile y ya da los insights, pero es necesario que el binario de hadolint esté instalado.

Instalación

La instalación es simple, solo hacer la descarga del binario en la página de release y colocar en el path, igual a la mayoría de los binarios de cli que tenemos por ahí. Si tienes brew puedes utilizar el comando abajo. También es posible ejecutar usando contenedor, esto es bueno para pipeline, si vas a integrar en tu vscode es necesario instalar el binario.

 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)

El comando es simple, solo hadolint más el dockerfile que deseas evaluar. La mayoría de las opciones arriba pueden pasarse directo en el comando o definidas en un archivo yaml y pasado en el flag --config que veremos más adelante.

Vamos para un ejemplo simple 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

Este Dockerfile funciona, pero observa que tenemos algunos warning e info que podrían ser mejorados y vamos a alterar.

# Resolviendo DL3007 warning: Pin the version explicitly to a release tag
# Forzamos la tag específica en lugar de mantener latest
FROM ubuntu:24.04
# Resolviendo
# DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
# Evita que paquetes recomendados sean instalados juntos con el paquete principal automáticamente.
# DL3008 warning: Pin versions in apt get install.
# Fijamos las versiones de los paquetes.
# DL3009 info: Delete the apt-get lists after installing something
# apt-get clean ya no es necesario cuando utilizamos rm -rf /var/lib/apt/lists/* después de la instalación de los paquetes.
# La razón es que este comando realiza una limpieza más eficaz y completa
# DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
# Colocamos todo dentro de un único RUN para evitar varias 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 a trabajar con workdir en lugar de crear una carpeta
WORKDIR /home/app
COPY app/ .
CMD ["bash"]

Si queremos mantener la instalación de curl siempre en la última versión tendríamos el error abajo, pero podemos ignorar en caso necesario.

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

También podríamos colocar ese ignore como comentario dentro del archivo Dockerfile y todo funcionaría igual sin necesidad de pasar el 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 desactivar los comentarios
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>`

A pesar de que hadolint sigue dando los ejemplos de buenas prácticas en la definición del Dockerfile él no ayuda a Hardenizar el contenedor.

Observa que permitió que ejecutáramos este contenedor como root, pues no sabe el propósito de este contenedor. Podemos verificar estos ítems de vulnerabilidad con trivy. Vamos a ejecutar con trivy para ver si detecta alguna diferencia.

 ❯ 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
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Además de no ejecutar como root trivy sugirió colocar un healthcheck. En este caso es un contenedor simple sin aplicación para verificar, pero podemos tener un healthcheck verificando si bash está activo para eliminar ese warning del tipo LOW de trivy.

Por otro lado trivy no reclamó sobre fijar la versión de los paquetes como hadolint hizo, mostrando que estas herramientas se complementan.

Entonces lo correcto sería...

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"]

Una cosa a observar la versión de curl curl=7.88.1-10ubuntu4 no existe en ubuntu 24.04 y hadolint no reclamó de eso. No builda el contenedor para verificar nada solo verifica la definición de tu dockerfile y sugiere las mejores prácticas.

Si volviéramos a nuestra definición inicial llena de problemas ¿qué diría trivy sobre esto?

 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)

# Trivy también detectó en fijar la tag de la imagen
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
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

# Trivy también detectó que deberíamos eliminar los caminos relativos y usar 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
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

# También pidió ejecutar las cosas en el mismo RUN para generar 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
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

# También pidió evitar instalar los 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
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Lo que Trivy no hizo fue pedir fijar las versiones de los paquetes y limpiar el apt-get lists después de la instalación.

Lo que podemos percibir es que estas herramientas se complementan y deben ser usadas en conjunto para garantizar un mejor análisis y creación de contenedores.