dai che magari questa cosa la finisco

This commit is contained in:
lesion 2017-02-13 13:03:53 +01:00
parent 1b82a442b7
commit 073a4b3704
2 changed files with 46 additions and 72 deletions

114
README.md
View file

@ -1,37 +1,38 @@
## 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.
Voglio esporre su internet alcuni servizi da una board (Odroid C2) e farlo a modino, ogni servizio dentro un container.
Giocando con i vari sistemi per creare container quello piú simile ai miei desideri é `runc`.
#### 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
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.
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. Come fare? Partendo dal binario che vogliamo lanciare (sia esso apache, nginx, dovecot o tor) prendiamo innanzitutto le librerie da cui dipende con un `ldd` (anche se sarebbe stato figo usare una distro [tutta statica](http://sta.li)) e poi scopriamo tutti i file da cui dipende cercando le syscall a `open` mentre gira (con `strace`).
Nella pratica, immaginiamo di voler far girare `nginx` in questa maniera, il setup a mano sarebbe:
Nella pratica, immaginiamo di voler far girare [caddy](https://caddyserver.com/) in questa maniera, ecco come sarebbe il setup a mano:
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
Creo una partizione per __i dati__ di 30Mb (a titolo di esempio lo facciamo in loop) e ne faccio il mount (per ora senza noexec)
```bash
dd if=/dev/zero of=./datafs bs=1024 count=30720
losetup /dev/loop0 datafs
dd if=/dev/zero of=./testfs/datafs bs=1024 count=30720
losetup /dev/loop0 ./testfs/datafs
mkfs.ext4 /dev/loop0
mkdir data
mount -o noexec /dev/loop0 data
mount /dev/loop0 data
```
stessa cosa per la partizione dei containers (per ora non la monto in read-only)
stessa cosa per la partizione dei container (per ora non la monto in read-only)
```bash
dd if=/dev/zero of=containerfs bs=1024 count=30720
losetup /dev/loop1 containerfs
dd if=/dev/zero of=./testfs/containerfs bs=1024 count=30720
losetup /dev/loop1 ./testfs/containerfs
mkfs.ext4 /dev/loop1
mkdir containers
mount /dev/loop1 containers
@ -40,82 +41,55 @@ mount /dev/loop1 containers
creo le dir per il container
```bash
mkdir containers/nginx
mkdir data/nginx
mkdir containers/caddy
mkdir data/caddy
```
e ci scompatto dentro la rootfs di alpine linux: `tar xvf alpine-minirootfs-3.5.1-aarch64.tar.gz -C containers/nginx`
e ci scompatto dentro la rootfs di alpine linux: `tar xvf rootfs/alpine-minirootfs-3.5.1-aarch64.tar.gz -C containers/caddy`
ora sará sufficiente `run spec` per creare un file di configurazione di runc e modificare i parametri del file che ci interessano:
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:~#
```js
// prima di tutto il path della rootfs (quindi cercando 'root')
"root": {
"path": "./containers/caddy",
// per ora non lo mettiamo read-only perché dobbiamo prima installare
// quello che ci interessa, per il deploy questo bisogna metterlo a true
"readonly": false
}
// poi dentro la voce "mounts" aggiungiamo la nostra partizione per i dati
"mounts": [
{
"type": "bind",
"destination": "/data",
"source": "./data/caddy",
"options": ["rbind", "rw", "noexec"]
},
```
e siamo dentro il container, quindi possiamo iniziare ad installare quello che dobbiamo:
```bash
a questo punto possiamo far partire il container con `runc run caddy` e siamo dentro,
non rimane che configurare il servizio che ci serve, in questo caso caddy.
Potrei fare velocemente con un `apk update` e `apk add caddy` ma il pacchettizzato
manca di alcune funzionalitá quindi scelgo di scaricarlo direttamente dal sito:
```
apk update
apk add nginx
apk add wget
wget 'https://caddyserver.com/download/build?os=linux&arch=arm64&features=git,filemanager,cors,expires,minify' -O caddy.tar.gz
```
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`:
Ora ci serve far partire il container direttamente con `caddy` e non con la shell
```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:
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 `caddy` 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 syscallscall 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)

View file

@ -93,8 +93,8 @@ escaped_path=$(echo $FULL_CONTAINER_PATH | sed -e 's/\//\\\//g')
echo "ESCAPED_PATH: $escaped_path"
mkdir `pwd`/containers/$CONTAINER_NAME.tmp
files=`sed -rn "s/.* open (readwrite |writeonly )?$escaped_path(.*) \{.*/\2/p" < files_rs.log | sort | uniq`
links=`sed -rn "s/.* readlink $escaped_path(.*) \{.*/\1/p" < files_rs.log | sort | uniq`
files=`sed -rn "s/.* open (readwrite |writeonly )?$escaped_path(.*) \{.*/\2/p" < files_$CONTAINER_NAME.log | sort | uniq`
links=`sed -rn "s/.* readlink $escaped_path(.*) \{.*/\1/p" < files_$CONTAINER_NAME.log | sort | uniq`
## ok, removing all file but ones in $CONTAINER_NAME.log
cd $FULL_CONTAINER_PATH