rootfs | ||
config.json | ||
config.xml | ||
lhc | ||
README.md | ||
runc.template |
Light Hardened Container
Perché
Voglio esporre su internet alcuni servizi da una single 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 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 con un utente specifico per ogni servizio. 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, mi buchi ma vuoi usare ls
? non puoi. vuoi usare netcat
? non c'é. ok puoi compilarlo statico per arm64
, auguri, dove lo uploadi? il fs e' read-ony, e in /data e' noexec. Insomma diventa fastidioso.
Come fare? Partendo dal binario che vogliamo lanciare (sia esso apache, nginx, dovecot o tor) prendiamo innanzitutto le librerie da cui dipende con un Ci serve una lista dei file a cui accede il nostro servizio, servirebbe un proxy fs, quale utilizzo migliore per un filesystem fuse?
Cercando in giro trovo ben due implementazioni di proxy fs, BigBrotherFS e https://rflament.github.io/loggedfs/ che supporta anche i filtri delle syscall quindi piú adatto a questo uso.
Le syscall che ci interessano sono le ldd
(anche se sarebbe stato figo usare una distro tutta statica) e poi scopriamo tutti i file da cui dipende cercando le syscall a open
mentre gira (con strace
).open
e le readlink
, quindi il file di configurazione di loggedfs filtrerá solo queste call, mostrandoci nel file di log solamente i file e i symlink a cui il nostro servizio ha avuto accesso.
Per fare tutte queste cose ho scritto uno script bash per evitare di rifare queste cose tutte le volte, vediamo come usarlo:
Uso
Immaginiamo di voler far girare caddy in questa maniera, diciamo che abbiamo una partizione cifrata dove vogliamo
mettere i nostri servizi in /var/lhc/
con dentro due directory, containers
e data
, possiamo lanciare lhc caddy /var/lhc/containers/ /var/lhc/data
.
lhc fará le seguenti operazioni.
- crea le dir
/var/lhc/containers/caddy
e/var/lhc/data/caddy
- in
/var/lhc/containers/caddy
mette la rootfs di alpine - crea un utente
caddy
nell'host - prepara un file di configurazione per lanciare
runc
con i dati di cui sopra - monta
/var/lhc/containers/caddy
con loggedfs filtrando le chiamateopen
ereadlink
- lancia una
chroot
dentro/var/lhc/containers/caddy
6a. qui si aspetta che noi installiamo e configuriamo il servizio - quando la chroot esce, prende tutti i file aperti, sposta il container originale in
/var/lhc/containers/caddy.orig
e dentro/var/lhc/containers/caddy
mette solo i file che sono stati aperti.
il grosso del lavoro che rimane é il punto 6a, ovvero l'installazione e la configurazione del servizio: