187 lines
8.6 KiB
Markdown
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
|
|
|
|
```
|