## Light Hardened Container #### Perché Voglio esporre su internet alcuni servizi da una board (Odroid C2). Giocando con i vari sistemi per creare container quello piú simile ai miei desideri é `systemd-nspawn` (si, systemd é una merda ed é il male). `systemd-nspawn` é tipo una chroot ma meglio. #### Come Uso [alpine linux](https://alpinelinux.org) come base perché la rootfs é scandalosamente piccola (1.9Mb), supporta arm64 (che é l'architettura della mia board casalinga), supporta un sistema di pacchetti degno di questo nome (apk) ed e' orientata alla sicurezza. L'assunto di base é che i servizi che girano dentro il container sono bucabili e quindi bisogna limitare i danni di un sicuro pwn. Per fare questo ho pensato di far girare la root di ogni container dentro una partizione read-only. Per i dati necessari ai vari servizi, faccio un bind di una directory da una partizione no-exec in modo che anche se il servizio viene bucato, l'attaccante puó scrivere solamente sulla partizione dei dati (da cui non puo' peró avviare niente). Per evitare di far usare interpreti e altre utilities utili all'attaccante, il container deve contenere esclusivamente i files strettamente necessari per far girare il servizio. Per fare questo, prima della fase di deploy il servizio verrá fatto girare con strace per controllare tutte le syscall a `open`: i file aperti saranno gli unici a rimanere nel container. Nella pratica, immaginiamo di voler far girare `nginx` in questa maniera, il setup a mano sarebbe: Creo una partizione per i dati di 30Mb (a titolo di esempio lo facciamo in loop) e ne faccio il mount con l'opzione noexec ```bash dd if=/dev/zero of=./datafs bs=1024 count=30720 losetup /dev/loop0 datafs mkfs.ext4 /dev/loop0 mkdir data mount -o noexec /dev/loop0 data ``` stessa cosa per la partizione dei containers (per ora non la monto in read-only) ```bash dd if=/dev/zero of=containerfs bs=1024 count=30720 losetup /dev/loop1 containerfs mkfs.ext4 /dev/loop1 mkdir containers mount /dev/loop1 containers ``` creo le dir per il container ```bash mkdir containers/nginx mkdir data/nginx ``` e ci scompatto dentro la rootfs di alpine linux: `tar xvf alpine-minirootfs-3.5.1-aarch64.tar.gz -C containers/nginx` a questo punto viene il bello, facciamo partire il container: ```bash  lhc git:(master) ✗ systemd-nspawn -D containers/nginx --bind=`pwd`/data/nginx/:/data Spawning container nginx on /data/dev/lhc/containers/nginx. Press ^] three times within 1s to kill container. Timezone Europe/Rome does not exist in container, not updating container timezone. -sh: can't access tty; job control turned off nginx:~# ``` e siamo dentro il container, quindi possiamo iniziare ad installare quello che dobbiamo: ```bash apk update apk add nginx ``` da adesso in poi, dobbiamo cercare di configurare il servizio per usare /data/ come path per tutti i file che vuole utilizzare in scrittura. Per fare ció possiamo fare in tanti modi, iniziamo a vedere cosa succede ad avviarlo manualmente: ```bash nginx: [emerg] open() "/run/nginx/nginx.pid" failed (2: No such file or directory) nginx3:~# ``` ok, /run é svuotata da `systemd-nspawn` di proposito e quindi la directory 'nginx' al suo interno non possiamo mettercela, inoltre per l'assunto di prima (ovvero che questo fs sará read-only) dobbiamo metterlo dentro /data, quindi modifichiamo `nginx.conf` per fare in modo che questo accada e quindi `vi /etc/nginx/nginx.conf`: ```nginx.conf # aggiungiamo da qualche parte se non c'é giá pid /data/nginx.pid # e giá che ci siamo cerchiamo anche gli altri path usati da nginx in scrittura e li cambiamo error_log /data/error.log; access_log /data/access.log; ``` riproviamo: ```bash nginx:/etc/nginx# nginx nginx:/etc/nginx# ps aux PID USER TIME COMMAND 1 root 0:00 -sh 26 root 0:00 nginx: master process nginx 27 nginx 0:00 nginx: worker process 28 nginx 0:00 nginx: worker process 29 nginx 0:00 nginx: worker process 30 nginx 0:00 nginx: worker process 31 nginx 0:00 nginx: worker process 32 nginx 0:00 nginx: worker process 33 nginx 0:00 nginx: worker process 34 nginx 0:00 nginx: worker process 35 root 0:00 ps aux nginx:/etc/nginx# ``` direi molto meglio, ora usciamo dal container (ctrl+] per tre volte oppure `exit`). Ora ci serve far partire il container direttamente con `nginx` e non con la shell e c'é un problema: per come é fatto systemd-nspawn il programma che prende il comando non deve uscire (é effettivamente PID 1 dentro il container) quindi bisogna dire a nginx di rimanere in foreground (basta aggiungere `daemon off;` in `containers/nginx/etc/nginx.conf`) `systemd-nspawn -D containers/nginx --bind=\`pwd\`/data/nginx/:/data nginx` per come é pacchettizzato dentro alpine linux, nginx cambia di suo l'utente con cui gira quindi per questo servizio non serve dire a systemd-nspawn di cambiare utente, negli altri casi bisogna usare il flag `-u`. Ultima cosa, non voglio avere una shell dentro il container o altri tool (metti che appunto ci bucano, mica vogliamo fargli un favore), ma come fare? ci serve qualcosa che trovi tutti i file aperti da `nginx` e quello che mi viene in mente é `strace`, quindi da dentro il container installiamo strace (`apk add strace`) e lanciamo nginx con strace redirigendo `stderr` su un file (`strace nginx 2> strace.log`). Dobbiamo cercare di "attivare" tutte le funzionalitá del servizio in modo che becchiamo tutti i file che possono essere utilizzati da `nginx` (in questo caso quello che mi viene in mente é di lanciare una richiesta HTTP a nginx per dire, tipo dall'host faccio un `wget localhost`). Ora killiamo lo strace `kill -9 strace` e cerchiamo le syscall a open dentro il log: ```bash nginx5:~# grep open strace.log open("/etc/ld-musl-x86_64.path", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/lib/libpcre.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/usr/local/lib/libpcre.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/usr/lib/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3 open("/lib/libssl.so.39", O_RDONLY|O_CLOEXEC) = 3 open("/lib/libcrypto.so.38", O_RDONLY|O_CLOEXEC) = 3 open("/lib/libz.so.1", O_RDONLY|O_CLOEXEC) = 3 open("/etc/localtime", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = -1 ENOENT (No such file or directory) open("/var/lib/nginx/logs/error.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 3 open("/etc/ssl/openssl.cnf", O_RDONLY) = -1 ENOENT (No such file or directory) open("/etc/nginx/nginx.conf", O_RDONLY) = 4 open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 5 open("/etc/group", O_RDONLY|O_CLOEXEC) = 5 open("/", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 5 open("/etc", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 6 open("/etc/nginx", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 7 open("/etc/nginx/modules", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 8 open("/etc/nginx/mime.types", O_RDONLY) = 5 open("/", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 5 open("/etc", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 6 open("/etc/nginx", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 7 open("/etc/nginx/conf.d", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 8 open("/etc/nginx/conf.d/default.conf", O_RDONLY) = 5 open("/data/error.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 4 open("/var/log/nginx/access.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 5 open("/data/nginx.pid", O_RDWR|O_CREAT|O_TRUNC, 0644) = 8 nginx5:~# ``` giá guardando gli errori (ENOENT) possiamo vedere alcune cosette, in particolare che ssl non funzionerá (probabilmente manca il pacchetto, ma a noi ora non interessa) e che mi sono dimenticato di cambiare qualche path dei log. comunque, i file mostrati qui sopra sono gli unici che devono rimanere dentro il container per fare in modo che tutti continui a funzionare, quindi estrapoliamo tutti i path: ```bash nginx:~# grep open strace.log | sed "s/.*\"\(.*\)\".*/\1/" | sort | uniq > needed_files nginx:~# cat needed_files / /data/error.log /data/nginx.pid /etc /etc/group /etc/ld-musl-x86_64.path /etc/localtime /etc/nginx /etc/nginx/conf.d /etc/nginx/conf.d/default.conf /etc/nginx/mime.types /etc/nginx/modules /etc/nginx/nginx.conf /etc/passwd /etc/ssl/openssl.cnf /lib/libcrypto.so.38 /lib/libpcre.so.1 /lib/libssl.so.39 /lib/libz.so.1 /usr/lib/libpcre.so.1 /usr/local/lib/libpcre.so.1 /var/lib/nginx/logs/error.log /var/log/nginx/access.log ``` ora, preparo un'altra directory con solo questi files `mkdir containers/nginx-prod` e copio dentro solo i files di cui sopra: ```bash CONTAINER_ROOT=`pwd`/containers/nginx for f in `cat containers/nginx/root/only_needed` ; do echo $f; if [ -f $CONTAINER_ROOT$f ]; then mkdir -p containers/nginx-prod/`dirname $f` ; cp $CONTAINER_ROOT$f containers/nginx-prod/`dirname $f`; fi; done ```