Aller au contenu principal

Wolfi OS Une distribution pour les conteneurs

· 9 minutes de lecture
Stéphane ROBERT
Consultant DevOps

Les concepteurs de Wolfi OS, porté par la société Chainguard se sont fixé comme objectif de produire des images de conteneurs qui répondent aux exigences de sécurité. Par exemple les images officielles ne doivent contenir aucune vulnérabilité critique connue.

Les spécifications de Wolfi OS

Parmi les spécifications, on retrouve que les outils de construction d'image doivent fournir une SBOM de qualité. Cette distribution fourni le support des librairies Glibc et de Musl. Le support de Musl se fait en s'appuyant sur la distribution Alpine. Du coup, le format des packages retenu est de celui de cette distribution : l'APK. Attention le gestionnaire de package n'est pas installé par défaut pour limiter la taille des images.

Il existe plusieurs façons d'installer des packages supplémentaires dans les images :

  • via un Dockerfile.
  • via des outils maison melange et apko.

Construction des images avec un Dockerfile

Je vais utiliser un simple api flask et construire l'image comme je le ferai avec donc une alpine. base d'une alpine.

Le code contenant dans le fichier main.py

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello World!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Le Dockerfile avec une image alpine :

FROM alpine:3.18 as builder
ENV PYROOT=/app/venv
ENV PYTHONUSERBASE=$PYROOT

WORKDIR /app

COPY requirements.txt .
RUN apk add python3 py3-pip python3-dev bash && pip3 install --no-cache-dir --no-compile pipenv && pipenv install -r requirements.txt && PIP_USER=1 pipenv sync --system

FROM alpine:3.18 as production
ARG version=3.11
ENV PYTHONPATH="/app/venv/lib/python3.11/site-packages/"

COPY --from=builder /app /app
COPY  main.py /app

RUN adduser -D nonroot && apk add python3 bash --no-cache  # && chown -R nonroot.nonroot /app
USER nonroot

WORKDIR /app

ENTRYPOINT [ "python3", "main.py" ]

On build et on vérifie la taille de l'image :

❯ docker build . -t test:0.1 -f Dockerfile
[+] Building 23.8s (13/13)
...

❯ docker images
REPOSITORY              TAG       IMAGE ID       CREATED         SIZE
test                    0.1       567ee7108d82   4 seconds ago   54.9MB

Je teste et cela fonctionne !

On va construire une image à base de Wolfi et vérifier la taille obtenue. J'ai dû adapter le Dockerfile car certains packages ne sont pas disponibles :

FROM cgr.dev/chainguard/wolfi-base as builder
ENV PYROOT=/app/venv
ENV PYTHONUSERBASE=$PYROOT

WORKDIR /app

COPY requirements.txt .
RUN apk add python3 py3-pip python3-dev bash && pip3 install --no-cache-dir --no-compile pipenv && pipenv install -r requirements.txt && PIP_USER=1 pipenv sync --system

FROM cgr.dev/chainguard/wolfi-base as production
ARG version=3.11
ENV PYTHONPATH="/app/venv/lib/python3.11/site-packages/"

COPY --from=builder /app /app
COPY  main.py /app

RUN adduser -D nonroot && apk add python3 bash --no-cache  # && chown -R nonroot.nonroot /app
USER nonroot

WORKDIR /app

ENTRYPOINT [ "python3", "main.py" ]

On lance le build, on teste et on vérifie la taille :

docker build . -t test:0.2 -f Dockerfile
[+] Building 34.8s (13/13) FINISHED                                                                                                                                                          ...

REPOSITORY              TAG       IMAGE ID       CREATED          SIZE
test                    0.2       4b9f14548425   14 seconds ago   61.5MB
test                    0.1       567ee7108d82   3 minutes ago    54.9MB

La taille de l'image est un peu plus importante. La raison est assez simple wolfi utilise glibc contrairement à Alpine les librairies sont un peu plus grosses.

On utilise apko et melange

La première chose est de construire le package, car on ne peut utiliser de commande dans le fichier apko.yml

package:
  name: hello-server
  version: 0.1.0
  description: friendly little webserver
  target-architecture:
    - all
  copyright:
    - license: Apache-2.0
      paths:
        - "*"
  dependencies:
    runtime:
      - python3

environment:
  contents:
    keyring:
      - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
    repositories:
    - https://packages.wolfi.dev/os
    packages:
      - python3
      - py3-pip
      - bubblewrap
      - wolfi-base
      - busybox
      - ca-certificates-bundle

pipeline:
  - name: Build Python application
    runs: |
      EXECDIR="${{targets.destdir}}/usr/bin"
      WEBAPPDIR="${{targets.destdir}}/usr/share/webapps/hello-server"
      mkdir -p "${EXECDIR}" "${WEBAPPDIR}"
      echo "#!/usr/share/webapps/hello-server/venv/bin/python3" > "${EXECDIR}/hello-server"
      cat main.py >> "${EXECDIR}/hello-server"
      chmod +x "${EXECDIR}/hello-server"
      python3 -m venv "${WEBAPPDIR}/venv"
      sh -c "source '${WEBAPPDIR}/venv/bin/activate' && pip install -r requirements.txt"

Avant de lancer le build du package, il faut générer une clé pour signer le package :

melange keygen

On se retrouve avec deux fichiers. On peut lancer le build :

melange build melange.yaml --arch amd64 --signing-key melange.rsa

On voit apparaître un dossier packages dans lequel on retrouve notre package portant le nom défini dans le yaml de melange accompagné de l'index du dépot :

-rw-rw-r-- 1 vagrant vagrant 9384665 août   8 21:22 hello-server-0.1.0-r0.apk
-rw-rw-r-- 1 vagrant vagrant     931 août   8 21:22 APKINDEX.tar.gz
-rw-rw-r-- 1 vagrant vagrant     732 août   8 21:22 APKINDEX.json
drwxr-xr-x 2 vagrant vagrant    4096 août   8 21:22 .

Maintenant passons à la construction de l'image avec apko. Le fichier de définition :

contents:
  keyring:
      - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
  repositories:
    - https://packages.wolfi.dev/os
    - ./packages
  packages:
    - hello-server
accounts:
  groups:
    - groupname: nonroot
      gid: 65532
  users:
    - username: nonroot
      uid: 65532
  run-as: 65532
entrypoint:
  command: /usr/bin/hello-server
archs:
  - amd64

On lance le build :

apko build apko.yaml test:0.3  output.tar -k melange.rsa.pub --arch amd64

On obtient un fichier .tar et de fichiers SBOM. Chargeons les images :

docker load < output.tar

Vérifions la taille de l'image :

❯ docker images
REPOSITORY                      TAG         IMAGE ID       CREATED          SIZE
test                            0.2         4b9f14548425   37 minutes ago   61.5MB
test                            0.1         567ee7108d82   41 minutes ago   54.9MB
test                            0.3-amd64   6fd4ca756c83   5 days ago       81.2MB

Mince la taille est plus importante que les deux autres ?

Changeons la source en prenant une alpine :

contents:
  keyring:
      - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
  repositories:
    - https://dl-cdn.alpinelinux.org/alpine/edge/main
    - ./packages
  packages:
    - alpine-base
    - hello-server
accounts:
  groups:
    - groupname: nonroot
      gid: 65532
  users:
    - username: nonroot
      uid: 65532
  run-as: 65532
entrypoint:
  command: /usr/bin/hello-server
archs:
  - amd64

Si je lance le build sans modifier le package en le passant aussi à Alpine, j'obtiens cette erreur :

Error: failed to load image configuration: unable to fetch remote include from git: failed to parse git reference apko.yaml: not enough path data available
2023/08/08 21:39:20 error during command execution: failed to load image configuration: unable to fetch remote include from git: failed to parse git reference apko.yaml: not enough path data available

Il faut donc générer un package basé sur Apline, modifions en conséquence le fichier yaml :

package:
  name: hello-server
  version: 0.1.0
  description: friendly little webserver
  target-architecture:
    - all
  copyright:
    - license: Apache-2.0
      paths:
        - "*"
  dependencies:
    runtime:
      - python3

environment:
  contents:
    keyring:
      - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
    repositories:
      - https://dl-cdn.alpinelinux.org/alpine/edge/main
      - https://dl-cdn.alpinelinux.org/alpine/edge/community

    packages:
      - python3
      - py3-pip
      - bubblewrap
      - alpine-base
      - busybox
      - ca-certificates-bundle

pipeline:
  - name: Build Python application
    runs: |
      EXECDIR="${{targets.destdir}}/usr/bin"
      WEBAPPDIR="${{targets.destdir}}/usr/share/webapps/hello-server"
      mkdir -p "${EXECDIR}" "${WEBAPPDIR}"
      echo "#!/usr/share/webapps/hello-server/venv/bin/python3" > "${EXECDIR}/hello-server"
      cat main.py >> "${EXECDIR}/hello-server"
      chmod +x "${EXECDIR}/hello-server"
      python3 -m venv "${WEBAPPDIR}/venv"
      sh -c "source '${WEBAPPDIR}/venv/bin/activate' && pip install -r requirements.txt"

On relance le build du package, puis de l'image :

melange build melange.yaml --arch amd64 --signing-key melange.rsa
...
apko build apko.yaml test:0.4  output.tar -k melange.rsa.pub --arch amd64

docker load < output.tar
docker images

REPOSITORY                      TAG         IMAGE ID       CREATED             SIZE
test                            0.2         4b9f14548425   About an hour ago   61.5MB
test                            0.1         567ee7108d82   About an hour ago   54.9MB
alpine                          3.18        7e01a0d0a1dc   25 hours ago        7.34MB
test                            0.4-amd64   c83ab923d745   4 days ago          65MB
test                            0.3-amd64   6fd4ca756c83   5 days ago          81.2MB

La taille est redevenu plus petite mais pas au point de faire la meme taille que celle produite par buildkit.

Par contre avec docker image inspect on voit que l'image produite avec apko ne contient qu'un seul layer.

        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:2db1f875cf628b93abf8c0f669820da1ab3e78e0f5deb714d98255635bdcef3c"
            ]
        },

Alors que celle produite avec Docker en contient cinq :

        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:399ddd85d8cfda6e6cf657dcd4d29951050a16e3bcebc9408ec10a3828257da7",
                "sha256:a79c6d92fff863bcb0ea26079582276ed527ef9bc2e61accdfc9839dbab5312d",
                "sha256:27b0f7412e17ff20fbce4ac5a6a7efc1f160c348b786264c73e1a9615f2bbe77",
                "sha256:47ab6f534a71889684d37f9e9a3128f51552cb6aebdd12e965cd496e8701c701",
                "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
            ]
        },

Et les vulnérabilités ?

Je vais utiliser trivy pour scanner les images :

❯ trivy image test:0.1
2023-08-08T22:05:16.679+0200    INFO    Need to update DB
2023-08-08T22:05:16.679+0200    INFO    DB Repository: ghcr.io/aquasecurity/trivy-db
2023-08-08T22:05:16.679+0200    INFO    Downloading DB...
38.80 MiB / 38.80 MiB [----------------------------------------------------------------------------------------------------------------------------------------------------------------] 100.00% 11.88 MiB p/s 3.5s
2023-08-08T22:05:21.031+0200    INFO    Vulnerability scanning is enabled
2023-08-08T22:05:21.031+0200    INFO    Secret scanning is enabled
2023-08-08T22:05:21.031+0200    INFO    If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2023-08-08T22:05:21.031+0200    INFO    Please see also https://aquasecurity.github.io/trivy/v0.43/docs/scanner/secret/#recommendation for faster secret detection
2023-08-08T22:05:22.216+0200    INFO    Detected OS: alpine
2023-08-08T22:05:22.216+0200    INFO    Detecting Alpine vulnerabilities...
2023-08-08T22:05:22.219+0200    INFO    Number of language-specific files: 1
2023-08-08T22:05:22.219+0200    INFO    Detecting python-pkg vulnerabilities...

test:0.1 (alpine 3.18.3)

Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

2023-08-08T22:05:22.231+0200    INFO    Table result includes only package filenames. Use '--format json' option to get the full path to the package file.

Python (python-pkg)

Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0)

┌──────────────────┬────────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────┐
│     Library      │ Vulnerability  │ Severity │ Installed Version │ Fixed Version │                   Title                    │
├──────────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────┤
│ Flask (METADATA) │ CVE-2023-30861 │ HIGH     │ 2.1.2             │ 2.2.5, 2.3.2  │ Cookie header                              │
│                  │                │          │                   │               │ https://avd.aquasec.com/nvd/cve-2023-30861 │
└──────────────────┴────────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────┘

Sur celle construite avec Wolfi :

❯ trivy image test:0.3-amd64
2023-08-08T22:06:30.871+0200    INFO    Vulnerability scanning is enabled
2023-08-08T22:06:30.872+0200    INFO    Secret scanning is enabled
2023-08-08T22:06:30.872+0200    INFO    If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2023-08-08T22:06:30.872+0200    INFO    Please see also https://aquasecurity.github.io/trivy/v0.43/docs/scanner/secret/#recommendation for faster secret detection
2023-08-08T22:06:33.891+0200    INFO    Detected OS: wolfi
2023-08-08T22:06:33.891+0200    INFO    Detecting Wolfi vulnerabilities...
2023-08-08T22:06:33.892+0200    INFO    Number of language-specific files: 0

test:0.3-amd64 (wolfi 20230201)

Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

Conclusion

Pour ce qui est de l'objectif d'obtenir une image de petite taille, je n'y arrive pas vraiment. Je manque certainement de connaissances pour ce qu'il faille mettre comme paramètre dans chacun des fichiers de configuration.

Pour ce qui est des vulnérabilités mon test n'est pas suffisant pour tirer des conclusions. Je vais tester d'autres images fourni par Chainguard. Vous les trouverez ici.

Après en avoir testé quelques-unes aucune vulnérabilité remontée par trivy.

Par contre, je trouve la documentation pas assez claire. Heureusement, il suffit de se promener dans les repos pour trouver des exemples.

Je vais continuer de tester apko et melange pour construire des images. Je vais aussi regarder la partie signature des images et comment intégré la SBOM dans ma chaine de compliance.