README.md |
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 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
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)
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
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:
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:
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:
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
:
# 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:
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:
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:
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:
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