
Cześć, Habr!
W dzisiejszych czasach, w obliczu rosnącej roli konteneryzacji w procesach rozwoju, kluczową kwestią staje się zapewnienie bezpieczeństwa różnych etapów oraz podmiotów związanych z kontenerami. Przeprowadzanie kontroli ręcznie to czasochłonny proces, dlatego warto poczynić chociażby wstępne kroki w kierunku automatyzacji tego procesu.
W tym artykule podzielę się gotowymi skryptami do implementacji kilku narzędzi zapewniających bezpieczeństwo Dockera oraz instrukcją, jak zbudować małe demo-stanowisko do przetestowania tego procesu. Materiały te można wykorzystać, aby eksperymentować z tym, jak zorganizować proces testowania bezpieczeństwa obrazów oraz instrukcji Dockerfile. Oczywiście, infrastruktura rozwoju i wdrażania jest różna dla każdego, dlatego poniżej przedstawię kilka możliwych wariantów.
Narzędzia do sprawdzania bezpieczeństwa
Istnieje wiele różnych aplikacji pomocniczych i skryptów, które wykonują kontrole różnych aspektów infrastruktury Dockera. Część z nich została już opisana w poprzednim artykule (), a w tym materiale chciałbym skupić się na trzech z nich, które spełniają główne wymagania dotyczące bezpieczeństwa obrazów Docker, tworzonych w procesie rozwoju. Ponadto pokażę również przykład, jak te trzy narzędzia można połączyć w jeden pipeline do przeprowadzania kontroli bezpieczeństwa.
Hadolint
Bardzo prosta konsolowa aplikacja, która pomaga w wstępnej ocenie poprawności i bezpieczeństwa instrukcji Dockerfile (np. użycie tylko dozwolonych rejestrów obrazów lub użycie sudo).

Dockle
Konsolowe narzędzie, które działa z obrazem (lub z zapisanym archiwum tar obrazu), sprawdzające poprawność i bezpieczeństwo danego obrazu, analizując jego warstwy i konfigurację – jakie użytkownicy zostali utworzeni, jakie instrukcje się używa, jakie wolumeny są podłączone, obecność pustego hasła itp. Na razie liczba sprawdzeń jest stosunkowo mała i oparta na kilku własnych kontrolach oraz rekomendacjach. dla Dockera.

Trivy
To narzędzie ma na celu znajdowanie dwóch typów podatności – problemy z wersjami systemu operacyjnego (obsługiwane są Alpine, RedHat (EL), CentOS, Debian GNU, Ubuntu) oraz problemy z zależnościami (Gemfile.lock, Pipfile.lock, composer.lock, package-lock.json, yarn.lock, Cargo.lock). Trivy potrafi skanować zarówno obrazy w repozytorium, jak i lokalne obrazy, a także przeprowadzać skanowanie na podstawie przesłanego pliku .tar z obrazem Docker.

Opcje wdrożenia narzędzi
Aby spróbować w izolowanych warunkach opisywanych aplikacji, podam instrukcje dotyczące instalacji wszystkich narzędzi w ramach uproszczonego procesu.
Główna idea polega na tym, aby pokazać, jak można zaimplementować automatyczne sprawdzanie zawartości Dockerfile i obrazów Docker tworzonych w trakcie rozwoju.
Sam proces weryfikacji składa się z następujących kroków:
- Weryfikacja poprawności i bezpieczeństwa instrukcji Dockerfile – narzędziem lintera Hadolint
- Weryfikacja poprawności i bezpieczeństwa finalnych oraz pośrednich obrazów – narzędziem Dockle
- Weryfikacja obecności znanych podatności (CVE) w podstawowym obrazie oraz w szeregu zależności – narzędziem Trivy
W dalszej części artykułu przedstawię trzy opcje wdrożenia tych kroków:
Pierwsza – poprzez konfigurację pipeline'u CI/CD na przykładzie GitLab (z opisem procesu uruchamiania testowego instancji).
Druga – z użyciem skryptu shell.
Trzecia – z budowaniem obrazu Docker do skanowania obrazów Docker.
Możesz wybrać opcję, która najbardziej ci odpowiada, przenieść ją na swoją infrastrukturę i dostosować do własnych potrzeb.
Wszystkie niezbędne pliki i dodatkowe instrukcje są również dostępne w repozytorium:
Integracja z GitLab CI/CD
W pierwszej opcji omówimy, jak można wprowadzić kontrole bezpieczeństwa na przykładzie systemu repozytoriów GitLab. Przejdziemy krok po kroku i omówimy, jak zainstalować od podstaw środowisko testowe z GitLab, opracować proces skanowania i uruchomić narzędzia do sprawdzania testowego Dockerfile i losowego obrazu aplikacji JuiceShop.
Instalacja GitLab
1. Instalujemy Docker:
sudo apt-get update && sudo apt-get install docker.io2. Dodajemy aktualnego użytkownika do grupy docker, aby można było pracować z dockiem bez użycia sudo:
sudo addgroup docker3. Znajdujemy swój adres IP:
ip addr4. Instalujemy i uruchamiamy GitLab w kontenerze, zastępując adres IP w hostname swoim:
docker run --detach
--hostname 192.168.1.112
--publish 443:443 --publish 80:80
--name gitlab
--restart always
--volume /srv/gitlab/config:/etc/gitlab
--volume /srv/gitlab/logs:/var/log/gitlab
--volume /srv/gitlab/data:/var/opt/gitlab
gitlab/gitlab-ce:latestCzekamy, aż GitLab wykona wszystkie niezbędne procedury instalacji (można śledzić proces przez wyjście z pliku log: docker logs -f gitlab).
5. Otwieramy w przeglądarce nasz lokalny IP i widzimy stronę z propozycją zmiany hasła dla użytkownika root:

Ustawiamy nowe hasło i wchodzimy do GitLab.
6. Tworzymy nowy projekt, na przykład cicd-test, i inicjujemy go plikiem startowym README.md:

7. Teraz musimy zainstalować GitLab Runner: agenta, który będzie uruchamiać wszystkie niezbędne operacje na żądanie.
Pobieramy najnowszą wersję (w tym przypadku — pod Linux 64-bit):
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd648. Uczyniamy go wykonywalnym:
sudo chmod +x /usr/local/bin/gitlab-runner9. Dodajemy użytkownika OS dla Runnera i uruchamiamy serwis:
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner startPowinno to wyglądać mniej więcej tak:
local@osboxes:~$ sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
Runtime platform arch=amd64 os=linux pid=8438 revision=0e5417a3 version=12.0.1
local@osboxes:~$ sudo gitlab-runner start
Runtime platform arch=amd64 os=linux pid=8518 revision=0e5417a3 version=12.0.1 10. Teraz rejestrujemy Runnera, aby mógł współdziałać z naszym instancją GitLab.
W tym celu otwieramy stronę ustawień Settings-CI/CD (http://OUR_ IP_ADDRESS/root/cicd-test/-/settings/ci_cd) i na zakładce Runners znajdujemy URL i token rejestracji:

11. Rejestrujemy Runnera, podstawiając URL i token rejestracji:
sudo gitlab-runner register
--non-interactive
--url "http:///"
--registration-token ""
--executor "docker"
--docker-privileged
--docker-image alpine:latest
--description "docker-runner"
--tag-list "docker,privileged"
--run-untagged="true"
--locked="false"
--access-level="not_protected"W rezultacie otrzymujemy gotowy działający GitLab, do którego należy dodać instrukcje do uruchomienia naszych narzędzi. W tym demonstracyjnym przypadku nie mamy kroków budowania aplikacji i jej konteneryzacji, ale w rzeczywistym środowisku będą one poprzedzać kroki skanowania i tworzyć obrazy oraz Dockerfile do analizy.
Konfiguracja pipeline
1. Dodajemy do repozytorium pliki mydockerfile.df (to jest pewien testowy Dockerfile, który będziemy sprawdzać) i plik konfiguracyjny procesu GitLab CI/CD .gitlab-cicd.yml, który wymienia instrukcje dla skanerów (zwróć uwagę na kropkę w nazwie pliku).
Plik konfiguracyjny YAML zawiera instrukcje dotyczące uruchamiania trzech narzędzi (Hadolint, Dockle i Trivy), które przeanalizują wybrany Dockerfile oraz obraz określony w zmiennej DOCKERFILE. Wszystkie niezbędne pliki można pobrać z repozytorium:
Fragment z mydockerfile.df (to jest plik przykładowy zawierający zestaw dowolnych instrukcji tylko w celach demonstracyjnych). Bezpośredni link do pliku:
Zawartość mydockerfile.df
FROM amd64/node:10.16.0-alpine@sha256:f59303fb3248e5d992586c76cc83e1d3700f641cbcd7c0067bc7ad5bb2e5b489 AS tsbuild
COPY package.json .
COPY yarn.lock .
RUN yarn install
COPY lib lib
COPY tsconfig.json tsconfig.json
COPY tsconfig.app.json tsconfig.app.json
RUN yarn build
FROM amd64/ubuntu:18.04@sha256:eb70667a801686f914408558660da753cde27192cd036148e58258819b927395
LABEL maintainer="Rhys Arkins <rhys@arkins.net>"
LABEL name="renovate"
...
COPY php.ini /usr/local/etc/php/php.ini
RUN cp -a /tmp/piik/* /var/www/html/
RUN rm -rf /tmp/piwik
RUN chown -R www-data /var/www/html
ADD piwik-cli-setup /piwik-cli-setup
ADD reset.php /var/www/html/
## ENTRYPOINT ##
ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
USER rootPlik konfiguracyjny YAML wygląda następująco (sam plik można pobrać bezpośrednio stąd: ):
Zawartość .gitlab-ci.yml
zmienne:
DOCKER_HOST: "tcp://docker:2375/"
DOCKERFILE: "mydockerfile.df" # nazwa pliku Dockerfile do analizy
DOCKERIMAGE: "bkimminich/juice-shop" # nazwa obrazu Dockera do analizy
# DOCKERIMAGE: "knqyf263/cve-2018-11235" # testowy obraz Dockera z wieloma KRYTYCZNYMI CVE
SHOWSTOPPER_PRIORITY: "KRYTYCZNY" # jaki poziom krytyczności spowoduje błąd w zadaniu Trivy
TRIVYCACHE: "$CI_PROJECT_DIR/.cache" # gdzie przechowywać bazę danych luk w Trivy dla szybszego ponownego użycia
ARTIFACT_FOLDER: "$CI_PROJECT_DIR"
usługi:
- docker:dind # aby móc budować obrazy dockera wewnątrz Runnera
etapy:
- skanowanie
- raport
- publikacja
HadoLint:
# Podstawowa analiza instrukcji Dockerfile
etap: skanowanie
obraz: docker:git
after_script:
- cat $ARTIFACT_FOLDER/hadolint_results.json
skrypt:
- export VERSION=$(wget -q -O - https://api.github.com/repos/hadolint/hadolint/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*//1/')
- wget https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64 && chmod +x hadolint-Linux-x86_64
# Uwaga: hadolint zawsze zakończy się kodem wyjścia 0
- ./hadolint-Linux-x86_64 -f json $DOCKERFILE > $ARTIFACT_FOLDER/hadolint_results.json || exit 0
artefakty:
kiedy: zawsze # zwracaj artefakty nawet po niepowodzeniu zadania
ścieżki:
- $ARTIFACT_FOLDER/hadolint_results.json
Dockle:
# Analizowanie najlepszych praktyk dotyczących obrazu dockera (uprawnienia użytkowników, instrukcje przestrzegane podczas budowy obrazu itd.)
etap: skanowanie
obraz: docker:git
after_script:
- cat $ARTIFACT_FOLDER/dockle_results.json
skrypt:
- export VERSION=$(wget -q -O - https://api.github.com/repos/goodwithtech/dockle/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*//1/')
- wget https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.tar.gz && tar zxf dockle_${VERSION}_Linux-64bit.tar.gz
- ./dockle --exit-code 1 -f json --output $ARTIFACT_FOLDER/dockle_results.json $DOCKERIMAGE
artefakty:
kiedy: zawsze # zwracaj artefakty nawet po niepowodzeniu zadania
ścieżki:
- $ARTIFACT_FOLDER/dockle_results.json
Trivy:
# Analizowanie obrazu dockera i zależności pakietów w kontekście różnych baz CVE
etap: skanowanie
obraz: docker:git
skrypt:
# pobieranie najnowszego Trivy
- apk add rpm
- export VERSION=$(wget -q -O - https://api.github.com/repos/knqyf263/trivy/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*//1')
- wget https://github.com/knqyf263/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz && tar zxf trivy_${VERSION}_Linux-64bit.tar.gz
# wyświetlanie wszystkich luk bez przerywania budowy
- ./trivy -d --cache-dir $TRIVYCACHE -f json -o $ARTIFACT_FOLDER/trivy_results.json --exit-code 0 $DOCKERIMAGE
# wypisywanie informacji o lukach w formacie czytelnym dla człowieka (czytanie czystego jsona nie jest zabawne, prawda?). Możesz to usunąć, jeśli nie potrzebujesz.
- ./trivy -d --cache-dir $TRIVYCACHE --exit-code 0 $DOCKERIMAGE
# przerywanie budowy, jeśli znajdzie się priorytet SHOWSTOPPER
- ./trivy -d --cache-dir $TRIVYCACHE --exit-code 1 --severity $SHOWSTOPPER_PRIORITY --quiet $DOCKERIMAGE
artefakty:
kiedy: zawsze # zwracaj artefakty nawet po niepowodzeniu budowy
ścieżki:
- $ARTIFACT_FOLDER/trivy_results.json
cache:
ścieżki:
- .cache
Raport:
# łączenie wyjść narzędzi w jeden plik HTML
etap: raport
kiedy: zawsze
obraz: python:3.5
skrypt:
- mkdir json
- cp $ARTIFACT_FOLDER/*.json ./json/
- pip install json2html
- wget https://raw.githubusercontent.com/shad0wrunner/docker_cicd/master/convert_json_results.py
- python ./convert_json_results.py
artefakty:
ścieżki:
- results.htmlW razie potrzeby można również skanować zapisane obrazy w formie archiwum .tar (jednak będzie konieczne, aby w pliku YAML zmienić parametry wejściowe dla narzędzi)
NB: Trivy wymaga do uruchomienia zainstalowanego rpm i sshpass. W przeciwnym razie zgłosi błędy podczas skanowania obrazów opartych na RedHat i aktualizacji bazy zagrożeń.
2. Po dodaniu plików do repozytorium, zgodnie z instrukcjami w naszym pliku konfiguracyjnym, GitLab automatycznie rozpocznie proces budowy i skanowania. Na zakładce CI/CD → Pipelines będzie można zobaczyć postęp wykonywania instrukcji.
W rezultacie mamy cztery zadania. Trzy z nich zajmują się bezpośrednio skanowaniem, a ostatnie (Report) zbiera prosty raport z rozproszonych plików z wynikami skanowania.

Domyślnie Trivy przerywa swoje działanie, jeśli wykryje KRYTYCZNE luki w obrazie lub zależnościach. Z drugiej strony, Hadolint zawsze zwraca kod sukcesu, ponieważ w wyniku jego działania zawsze są uwagi, co prowadzi do zatrzymania budowy.
W zależności od konkretnych wymagań można skonfigurować kod wyjścia, aby te narzędzia w przypadku wykrycia problemów o określonym krytycznym stopniu również zatrzymywały proces budowy. W naszym przypadku budowa zatrzyma się tylko wtedy, gdy Trivy wykryje lukę o krytyczności, którą określiliśmy w zmiennej SHOWSTOPPER w .gitlab-ci.yml.

Wyniki działania każdego narzędzia można zobaczyć w logach każdego skanującego zadania, bezpośrednio w plikach json w sekcji artefakty lub w prostym raporcie HTML (o nim nieco niżej):

3. Aby przedstawić raporty narzędzi w nieco bardziej czytelnej formie, używany jest mały skrypt w Pythonie do konwersji trzech plików json w jeden plik HTML z tabelą defektów.
Ten skrypt jest uruchamiany jako osobne zadanie Report, a jego ostatecznym artefaktem jest plik HTML z raportem. Źródło skryptu również znajduje się w repozytorium i można je dostosować do własnych potrzeb, kolorów itp.

Skrypt shell
Druga opcja nadaje się do przypadków, gdy konieczne jest sprawdzenie obrazów Dockera nie w ramach systemu CI/CD lub gdy wszystkie instrukcje muszą być w formie, którą można uruchomić bezpośrednio na hoście. Ta opcja jest objęta gotowym skryptem powłoki, który można uruchomić na czystej maszynie wirtualnej (lub nawet realnej). Skrypt wykonuje te same instrukcje, co opisany powyżej gitlab-runner.
Aby skrypt działał poprawnie, system musi mieć zainstalowany Docker, a bieżący użytkownik musi być w grupie docker.
Sam skrypt można pobrać stąd:
Na początku pliku za pomocą zmiennych określa się, który obraz należy skanować i które defekty o jakiej krytyczności będą powodować wyjście z narzędzia Trivy z podanym kodem błędu.
W trakcie wykonywania skryptu wszystkie narzędzia będą pobierane do katalogu docker_tools, a wyniki ich pracy — do katalogu docker_tools/json, a raport w formacie HTML znajdzie się w pliku results.html.
Przykład wyjścia skryptu
~\/docker_cicd$ .\/docker_sec_check.sh
[+] Ustawianie zmiennych środowiskowych
[+] Instalowanie wymaganych pakietów
[+] Przygotowanie niezbędnych katalogów
[+] Pobieranie przykładowego Dockerfile
2020-10-20 10:40:00 (45.3 MB/s) - 'Dockerfile' zapisano [8071/8071]
[+] Pobieranie obrazu do skanowania
latest: Pobieranie z bkimminich/juice-shop
[+] Uruchamianie Hadolint
...
Dockerfile:205 DL3015 Unikaj dodatkowych pakietów, określając `--no-install-recommends`
Dockerfile:248 DL3002 Ostatni UŻYTKOWNIK nie powinien być root
...
[+] Uruchamianie Dockle
...
WARN - DKL-DI-0006: Unikaj etykiety latest
* Unikaj etykiety 'latest'
INFO - CIS-DI-0005: Włącz zaufanie do treści dla Dockera
* eksportuj DOCKER_CONTENT_TRUST=1 przed docker pull/build
...
[+] Uruchamianie Trivy
juice-shop/frontend/package-lock.json
=====================================
Łącznie: 3 (NIEZNANY: 0, NISKI: 1, ŚREDNI: 0, WYSOKI: 2, KRYTYCZNY: 0)
+---------------------+------------------+----------+---------+-------------------------+
| BIBLIOTEKA | IDENTYFIKATOR LUKI | POWAŻNOŚĆ | WERSJA | TYTUŁ |
+---------------------+------------------+----------+---------+-------------------------+
| object-path | CVE-2020-15256 | WYSOKA | 0.11.4 | Zanieczyszczenie prototypu w |
| | | | | object-path |
+---------------------+------------------+ +---------+-------------------------+
| tree-kill | CVE-2019-15599 | | 1.2.2 | Iniekcja kodu |
+---------------------+------------------+----------+---------+-------------------------+
| webpack-subresource | CVE-2020-15262 | NISKA | 1.4.1 | Niechronione dynamicznie |
| | | | | ładowane kawałki |
+---------------------+------------------+----------+---------+-------------------------+
juice-shop/package-lock.json
============================
Łącznie: 20 (NIEZNANY: 0, NISKI: 1, ŚREDNI: 6, WYSOKI: 8, KRYTYCZNY: 5)
...
juice-shop/package-lock.json
============================
Łącznie: 5 (KRYTYCZNY: 5)
...
[+] Usuwanie pozostałości
[+] Uatrakcyjnianie wyjścia
[+] Konwersja wyników JSON
[+] Zapis wyników w HTML
[+] Czyste zakończenie ============================================================
[+] Wszystko zrobione. Znajdź raport w formacie HTML w results.htmlObraz Docker z wszystkimi narzędziami
Jako trzecią alternatywę stworzyłem dwa proste Dockerfile do budowy obrazu z narzędziami bezpieczeństwa. Jeden Dockerfile pomoże w zbudowaniu zestawu do skanowania obrazu z repozytorium, drugi (Dockerfile_tar) – zbudować zestaw do skanowania pliku tar z obrazem.
1. Weź odpowiedni plik Docker i skrypty z repozytorium .
2. Uruchom go do budowy:
docker build -t dscan:image -f docker_security.df .
3. Po zakończeniu budowy twórz kontener z obrazu. Przekaż przy tym zmienną środowiskową DOCKERIMAGE z nazwą interesującego nas obrazu i zamontuj Dockerfile, który chcemy analizować, z naszego komputera do pliku /Dockerfile (zwróć uwagę, że potrzebna jest absolutna ścieżka do tego pliku):
docker run --rm -v $(pwd)\/results:\/results -v $(pwd)\/docker_security.df:\/Dockerfile -e DOCKERIMAGE="bkimminich\/juice-shop" dscan:image
[+] Ustawianie zmiennych środowiskowych
[+] Uruchamianie Hadolint
\/Dockerfile:3 DL3006 Zawsze oznaczaj wersję obrazu jawnie
[+] Uruchamianie Dockle
WARN - DKL-DI-0006: Unikaj tagu latest
* Unikaj tagu 'latest'
INFO - CIS-DI-0005: Włącz zaufanie do treści dla Dockera
* export DOCKER_CONTENT_TRUST=1 przed docker pull\/build
INFO - CIS-DI-0006: Dodaj instrukcję HEALTHCHECK do obrazu kontenera
* brak instrukcji HEALTHCHECK
INFO - DKL-LI-0003: Wstawiaj tylko niezbędne pliki
* zbędny plik : juice-shop\/node_modules\/sqlite3\/Dockerfile
* zbędny plik : juice-shop\/node_modules\/sqlite3\/tools\/docker\/architecture\/linux-arm64\/Dockerfile
* zbędny plik : juice-shop\/node_modules\/sqlite3\/tools\/docker\/architecture\/linux-arm\/Dockerfile
[+] Uruchamianie Trivy
...\njuice-shop\/package-lock.json
============================
Łącznie: 20 (NIEZNANE: 0, NISKIE: 1, ŚREDNIE: 6, WYSOKIE: 8, KRYTYCZNE: 5)
...
[+] Uporządkowanie wyniku
[+] Rozpoczynanie głównego modułu ============================================================
[+] Konwersja wyników JSON
[+] Zapis wyników HTML
[+] Czyste zakończenie ============================================================
[+] Wszystko gotowe. Znajdź wynikowy raport HTML w results.htmlWyniki
Rozważaliśmy tylko jeden podstawowy zestaw narzędzi do skanowania artefaktów Docker, który moim zdaniem efektywnie pokrywa znaczną część wymagań dotyczących bezpieczeństwa obrazów. Istnieje wiele płatnych i bezpłatnych narzędzi, które mogą wykonywać te same kontrole, generować ładne raporty lub działać wyłącznie w trybie konsolowym, obejmować systemy zarządzania kontenerami itd. Recenzja tych narzędzi i sposobów ich integracji może pojawić się nieco później.
Zaletą zestawu narzędzi opisanych w artykule jest to, że wszystkie są zbudowane na otwartym kodzie źródłowym, co pozwala na eksperymentowanie z nimi oraz innymi podobnymi narzędziami w celu znalezienia tego, co najlepiej spełnia Twoje wymagania i specyfikę infrastruktury. Oczywiście wszystkie znalezione podatności powinny być analizowane pod kątem zastosowania w konkretnej sytuacji, ale to temat na przyszły obszerny artykuł.
Mam nadzieję, że ta instrukcja, skrypty i narzędzia pomogą Ci i staną się punktem wyjścia do stworzenia bardziej bezpiecznej infrastruktury w obszarze związanym z konteneryzacją.
Źródło: habr.com
