Alpine alebo Distroless?

Asi každý z nás sa snaží vytvoriť čo najmenší a najbezpečnejší kontajner aplikácie. Aké možnosti máme a existuje univerzálne riešenie?

Nedávno som si položil otázku či by som mal ostať pri Alpine alebo ísť cestou tzv. distroless kontajnerov. A je tu ešte nejaká ďalšia možnosť?

Alpine distribúciu asi všetci poznáte. Ale, čo to je distroless kontajner?

Začneme najprv problémom. Distribúcie ako Alpine obsahujú ďaleko viac ako len aplikáciu. A každá vec, ktorá tam je naviac, nielen zvyšuje veľkosť obrazu, ale aj bezpečnostné riziko. Preto Google prišiel so sadou tzv. distroless kontajnerov. Ich súčasťou nieje nič, len samotná aplikácia a potrebné knižnice. Žiaden bash, žiaden cp.

Použitie je veľmi jednoduché. Stačí ak FROM odkazuje na ten správny distroless kontajner.

FROM gcr.io/distroless/nodejs:16
...

Lenže, je distroless skutočne bezbečnejší a menší kontajner, ako napríklad bežne používaný Apline? Alebo ide len o hype?

Urobil som 2 experimenty. Jeden pre NodeJS a druhý pre Go aplikáciu. Pre obe aplikácie som vytvoril dvojicu kontajnerov. Jeden ako Alpine a druhý ako distroless. Následne som sa pozrel na ich veľkosť a tiež počet zraniteľnosti.

Trivy

Na detekciu zraniteľnosti som použil Trivy s aktuálnou databázou zraniteľnosti. Ide o malý užitočný scanner od spoločnosti AquaSecurity, ktorá je známa v oblasti bezpečnosti kontajnerov. Trivy prescanuje obraz kontajnera a zo svojej databázy vypíše aktuálny zoznam tzv. CVE ID. CVE je skratka pre Common Vulnerabilities and Exposures a ide o akýsi zoznam verejne dostupných bezpečnostných zraniteľnosti.

Samozrejme existujú aj lepšie nástroje. Trivy som si však zvolil kvôli jeho jednoduchosti.

Treba si uvedomiť, že zraniteľnosti sa menia v čase a pribúdajú. To, čo platilo v dobe, keď som robil tento test, vôbec nemusí platiť o týždeň.

NodeJS aplikácia

Ale späť k experimentovaniu. Vytvoril som si veľmi jednoduchú NodeJS aplikáciu:

const http = require('http');

const requestListener = function (req, res) {
  res.writeHead(200);
  res.end('Hello, World!');
}

const server = http.createServer(requestListener);
server.listen(8080);

Následne som pre aplikáciu vytvoril klasický Alpine kontajner:

Dockerfile.alpine
FROM node:16-alpine

COPY hello.js /
WORKDIR /
EXPOSE 8080
CMD ["hello.js"]
$ docker build . -f Dockerfile.alpine -t hello-alpine

Okrem toho som si vytvoril aj distroless kontajner. Ten sa líši od Alpine tým, čo ma vo FROM:

Dockerfile.distroless
FROM gcr.io/distroless/nodejs:16

COPY hello.js /
WORKDIR /
EXPOSE 8080
CMD ["hello.js"]
$ docker build . -f Dockerfile.distroless -t hello-distroless

Bohužiaľ, momentálne nebol dostupný distroless kontajner pre NodeJS 17, preto som použil to posledné, čo bolo dostupné, čiže NodeJS 16. Tú istú verziu som použil aj pre Alpine.

Takže teraz mám v systéme dvojicu obrazov kontajnerov hello-alpine a hello-distroless

Poďme sa pozrieť, ako si na tom stoja s veľkosťou. Je distroless menší?

$ docker images                                                                                                                                                                          
REPOSITORY                 TAG         IMAGE ID       CREATED              SIZE
hello-alpine               latest      7ee7f2728fcb   18 seconds ago       110MB
hello-distroless           latest      277e5bff2d6e   About a minute ago   116MB

Alpine má teda 110MB a distroless má 116MB a je zaujímavo väčší.

Poďme sa pozrieť, ako si na tom stoja s bezpečnosťou? Oba obrazy preskenujem pomocou trivy:

$ trivy image hello-distroless:latest                                                                                                                                                                   
2021-11-27T12:07:53.147Z	INFO	Detected OS: debian
2021-11-27T12:07:53.147Z	INFO	Detecting Debian vulnerabilities...
2021-11-27T12:07:53.150Z	INFO	Number of language-specific files: 1
2021-11-27T12:07:53.150Z	INFO	Detecting node-pkg vulnerabilities...

hello-distroless:latest (debian 11.1)
=====================================
Total: 13 (UNKNOWN: 0, LOW: 11, MEDIUM: 0, HIGH: 1, CRITICAL: 1)

Node.js (node-pkg)
==================
Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 2, CRITICAL: 0)
$ trivy image hello-alpine                                                                                                                                                                              
2021-11-27T12:10:44.822Z	INFO	Detected OS: alpine
2021-11-27T12:10:44.822Z	INFO	Detecting Alpine vulnerabilities...
2021-11-27T12:10:44.823Z	INFO	Number of language-specific files: 1
2021-11-27T12:10:44.823Z	INFO	Detecting node-pkg vulnerabilities...

hello-alpine (alpine 3.14.3)
============================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)


Node.js (node-pkg)
==================
Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 2, CRITICAL: 0)

Ako je vidieť, Distroless má 15 zraniteľností a Alpine má 2. Ako je vidieť distroless je v tomto prípade nielen väčší, ale dokonca obsahuje viac zraniteľností.

Go aplikácia

Ten istý experiment teraz skúsim s Go aplikáciou. Opäť veľmi triviálny kód:

package main

import "fmt"
import "net/http"

func hello(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "hello\n")
}

func main() {
	http.HandleFunc("/hello", hello)
   http.ListenAnsServe(":8080",nil)
}

Opäť som si vytvoril dvojicu obrazov hello-go-alpine:

FROM golang:1.17 as build-env

WORKDIR /go/src/app
COPY *.go .
COPY go.mod .

RUN CGO_ENABLED=0 go build -o /go/bin/test

FROM alpine:3.14
EXPOSE 8080
COPY --from=build-env /go/bin/test /
CMD ["/test"]
$ docker build . -f Dockerfile.alpine -t hello-go-alpine 

A ešte hello-go-distroless:

FROM golang:1.17 as build-env

WORKDIR /go/src/app
COPY *.go .
COPY go.mod .

RUN CGO_ENABLED=0 go build -o /go/bin/test

FROM gcr.io/distroless/static
EXPOSE 8080
COPY --from=build-env /go/bin/test /
CMD ["/test"]

Podobne, ako pri NodeJS skontrolujem veľkosť obrazov jednoducho cez Docker:

$ docker images                                                                                                                                                                         
REPOSITORY                 TAG       IMAGE ID       CREATED             SIZE
hello-go-alpine            latest    94c15ff84c66   About an hour ago   11.7MB
hello-go-distroless        latest    059739d7485a   About an hour ago   8.63MB

Tu vidieť, že čísla hrajú v prospech distroless. Alpine má cca 11MB a distroless má 8MB. Ale, ako si na tom stojí s bezpečnosťou?

$ trivy image hello-go-alpine                                                                                                                                                           
2021-12-01T15:51:07.388Z	INFO	Detected OS: alpine
2021-12-01T15:51:07.388Z	INFO	Detecting Alpine vulnerabilities...
2021-12-01T15:51:07.393Z	INFO	Number of language-specific files: 0

hello-go-alpine (alpine 3.14.3)
===============================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
$ trivy image hello-go-distroless                                                                                                                                                       
2021-12-01T15:51:46.400Z	INFO	Detected OS: debian
2021-12-01T15:51:46.400Z	INFO	Detecting Debian vulnerabilities...
2021-12-01T15:51:46.400Z	INFO	Number of language-specific files: 0

hello-go-distroless (debian 11.1)
=================================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

Tu si oba obrazy stoja na rovnako. Žiadna známa zraniteľnosť.

docker-slim

Ako ďalší experiment som sa rozhodol na existujúce kontajnery použiť nástroj docker-slim. Ide o nástroj na optimalizáciu kontajnerov. Jeho cieľom je presne to, o čo sa snažíme: Vytvoriť najmenší a najbezpečnejší kontajner.

Ako si poradí s NodeJS a Go? Aplikoval som ho na moje obrazy kontajnerov:

$ docker-slim build hello-nodejs-distroless
$ docker-slim build hello-nodejs-alpine
$ docker-slim build hello-go-distroless
$ docker-slim build hello-go-alpine
`

Ako prvé som si pozrel veľkosť obrazov:

$ docker images 
hello-nodejs-alpine           latest  910726cd68ae   29 minutes ago  110MB
hello-nodejs-alpine.slim      latest  da71d65b80cf   60 seconds ago  84.2MB
hello-nodejs-distroless       latest  4f181315bc44   30 minutes ago  116MB
hello-nodejs-distroless.slim  latest  db135e6d457e   20 seconds ago  86.5MB
$ docker images
hello-go-alpine            latest   1fa283bc7deb   1 day ago      11.7MB
hello-go-alpine.slim       latest   3af934521aff   1 day ago      6.07MB
hello-go-distroless        latest   04842032767c   1 day ago      8.43MB
hello-go-distroless.slim   latest   e55051e81cc6   1 day ago      6.07MB

Nástroj vytvoril ku každému obrazu jeho .slim verziu a výsledok je celkom potešujúci. V oboch prípadoch dokázal zosekal veľkosť. U NodeJS to je cca na 84-86MB. U Go aplikácie na 6MB.

A čo na to Trivy? Ako je na tom slim verzia z pohľadu bezpečnosti?

$ trivy hello-nodejs-alpine.slim 
2021-12-03T15:59:21.706+0100 	INFO 	Number of language-specific files: 0

$ trivy hello-nodejs-distroless.slim
2021-12-03T16:00:10.902+0100	INFO	Number of language-specific files: 0

V oboch prípadoch sa stalo niečo zaujímavé. Trivy nemal čo analyzovať. Dá sa to považovať za bezpečný kontajner? Ja by som sa na to nespoliehal a zrejme by som mal zvážiť lepší, možno komerčný nástroj.

Ale, čo stojí za týmto zázrakom? Ako sa to podarilo?

Nástroj kombinuje statickú a dynamickú analýzu kontajnera. Práve dynamická analýza je zaujímavá. Nástroj spustí kontajner a pomocou rôznych sond sa pokúša analyzovať systémové volania. Práve takto sa snaží identifikovať všetký potrebné knižnice a súbory potrebné pre beh aplikácie.

A práve aj tu môže nastať kameň úrazu. Ak aplikácia robí nejaké dynamické nahrávanie a sonda nevyvolá danú vetvu kódu, potom docker-slim môže odstrániť súbor, ktorý nakoniec bude potrebný. Preto sa aj v dokumentácii uvádza, že je potrebné si kontajner pretestovať. Pozor teda na rôzne lazy loadingy.

Záver

Je lepšie Distroless? Alebo je to zbytočný hype a človek by mal ostať pri Alpine? Je docker-slim to vysnené riešenie?

Na túto otázku neexistuje univerzálna odpoveď. Ako vidieť výsledok je rôzny. Distroless nie je zárukou bezpečnosti a najmenšieho možného obrazu. Aj Alpine dokáže prekvapiť. A hoci docker-slim dokáže skutočne malý zázrak, je potrebné kontajnerizovanú aplikáciu dôkladne otestovať.

Platí len jedno. Každý obraz treba individuálne zvážiť. Treba si zmerať, ktorá technika má význam. Bez dát a meraní ide len o subjektívne tvrdenie.

Čo sa týka bezpečnosti. Ak chceme skutočne docieliť čo najbezpečnejší kontajner, je potrebné do delivery pipeline zahrnúť skenovanie obrazov práve pomocou nástrojov, ako je snyk či trivy.

<