lhc/README.md

188 lines
8.6 KiB
Markdown
Raw Normal View History

2017-01-31 19:34:40 +01:00
## Light Hardened Container
2017-01-31 18:58:35 +01:00
2017-01-31 19:34:40 +01:00
#### 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.
2017-02-01 02:58:00 +01:00
#### Come
2017-01-31 19:34:40 +01:00
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).
2017-02-01 02:58:00 +01:00
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.
2017-01-31 19:34:40 +01:00
2017-02-01 02:58:00 +01:00
Nella pratica, immaginiamo di voler far girare `nginx` in questa maniera, il setup a mano sarebbe:
2017-01-31 19:34:40 +01:00
2017-02-01 02:58:00 +01:00
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
```