lhc/README.md
2017-02-01 02:58:00 +01:00

187 lines
8.6 KiB
Markdown

## 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
```