light hardened container (using runc and alpine linux)

lesion 5c03ed0634 chroot during setup, then container ... 7 years ago
rootfs 5c03ed0634 chroot during setup, then container ... 7 years ago
README.md 5c03ed0634 chroot during setup, then container ... 7 years ago
config.json 5c03ed0634 chroot during setup, then container ... 7 years ago
config.xml 5c03ed0634 chroot during setup, then container ... 7 years ago
lhc 5c03ed0634 chroot during setup, then container ... 7 years ago
runc.template 5c03ed0634 chroot during setup, then container ... 7 years ago

README.md

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 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). 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 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.

  1. crea le dir /var/lhc/containers/caddy e /var/lhc/data/caddy
  2. in /var/lhc/containers/caddy mette la rootfs di alpine
  3. crea un utente caddy nell'host
  4. prepara un file di configurazione per lanciare runc con i dati di cui sopra
  5. monta /var/lhc/containers/caddy con loggedfs filtrando le chiamate open e readlink
  6. lancia una chroot dentro /var/lhc/containers/caddy 6a. qui si aspetta che noi installiamo e configuriamo il servizio
  7. 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:

Apache setup

Caddy setup