chroot during setup, then container ...

This commit is contained in:
lesion 2017-03-01 00:39:11 +01:00
parent 073a4b3704
commit 5c03ed0634
7 changed files with 233 additions and 206 deletions

172
README.md
View file

@ -1,161 +1,43 @@
## Light Hardened Container
#### Perché
Voglio esporre su internet alcuni servizi da una board (Odroid C2) e farlo a modino, ogni servizio dentro un container.
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](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 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. 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`).
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](http://sta.li)) 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](https://www.cs.nmsu.edu/~pfeiffer/fuse-tutorial/) e [https://rflament.github.io/loggedfs/](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](config.xml) filtrerá solo queste call, mostrandoci nel file di log solamente i file e i symlink a cui il nostro servizio ha avuto accesso.
Nella pratica, immaginiamo di voler far girare [caddy](https://caddyserver.com/) in questa maniera, ecco come sarebbe il setup a mano:
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](https://caddyserver.com/) 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
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=./testfs/datafs bs=1024 count=30720
losetup /dev/loop0 ./testfs/datafs
mkfs.ext4 /dev/loop0
mkdir data
mount /dev/loop0 data
```
stessa cosa per la partizione dei container (per ora non la monto in read-only)
```bash
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
```
creo le dir per il container
```bash
mkdir containers/caddy
mkdir data/caddy
```
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:
```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"]
},
```
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 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
```
Ora ci serve far partire il container direttamente con `caddy` e non con la shell
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)
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
```
### Caddy setup

160
config.json Normal file
View file

@ -0,0 +1,160 @@
{
"ociVersion": "1.0.0-rc1",
"platform": {
"os": "linux",
"arch": "x86_64"
},
"process": {
"args": ["sh"],
"terminal": false,
"tty": false,
"user": {
"uid": 1004,
"gid": 1004
},
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/",
"capabilities": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"noNewPrivileges": true
},
"root": {
"path": "/tmp/agent/agent",
"readonly": true
},
"mounts": [
{
"type": "bind",
"source": "/tmp/data/agent",
"destination": "/data",
"options": [ "rbind", "rw", "noexec" ]
},
{
"destination": "/proc",
"type": "proc",
"source": "proc"
},
{
"destination": "/dev",
"type": "tmpfs",
"source": "tmpfs",
"options": [
"nosuid",
"strictatime",
"mode=755",
"size=65536k"
]
},
{
"destination": "/dev/pts",
"type": "devpts",
"source": "devpts",
"options": [
"nosuid",
"noexec",
"newinstance",
"ptmxmode=0666",
"mode=0620",
"gid=5"
]
},
{
"destination": "/dev/shm",
"type": "tmpfs",
"source": "shm",
"options": [
"nosuid",
"noexec",
"nodev",
"mode=1777",
"size=65536k"
]
},
{
"destination": "/dev/mqueue",
"type": "mqueue",
"source": "mqueue",
"options": [
"nosuid",
"noexec",
"nodev"
]
},
{
"destination": "/sys",
"type": "sysfs",
"source": "sysfs",
"options": [
"nosuid",
"noexec",
"nodev",
"ro"
]
},
{
"destination": "/sys/fs/cgroup",
"type": "cgroup",
"source": "cgroup",
"options": [
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
]
}
],
"hooks": {},
"linux": {
"resources": {
"devices": [
{
"allow": false,
"access": "rwm"
}
]
},
"namespaces": [
{
"type": "pid"
},
{
"type": "ipc"
},
{
"type": "mount"
}
],
"maskedPaths": [
"/proc/kcore",
"/proc/latency_stats",
"/proc/timer_stats",
"/proc/sched_debug"
],
"readonlyPaths": [
"/proc/asound",
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"solaris": {
"cappedCPU": {},
"cappedMemory": {}
}
}

View file

@ -2,7 +2,7 @@
<loggedFS logEnabled="true" printProcessName="true">
<includes>
<include extension=".*" uid="*" action="open" retname=".*"/>
<include extension=".*" uid="*" action="open" retname="SUCCESS"/>
<include extension=".*" uid="*" action="readlink" retname="SUCCESS"/>
</includes>
<excludes>

View file

@ -2,9 +2,9 @@
function print_help {
echo '''
Super Mini hardened container manager using alpine and runc
Light Hardened Container / a super light hardened container using alpine and runc
v1.0
Usage: lhc-create <containername>
Usage: lhc <containername> <containerpath> <datapath>
'''
exit -1
}
@ -29,28 +29,31 @@ print_msg() {
TYPE=${msgType:-"[\e[92mInfo\e[0m]"}
echo -e $TYPE $CONTENT
}
export ARCH=$(get_arch)
print_msg "Arch: $ARCH"
## check if container's name is passed
## TODO, has to check if is not '--help' or '-h'
if [ $# -lt 1 ]
if [ $# -lt 3 ]
then
print_help
fi
export CONTAINER_NAME=$1
export FULL_CONTAINER_PATH="`pwd`/containers/$CONTAINER_NAME/"
export FULL_CONTAINER_PATH=`readlink -f $2`/$CONTAINER_NAME
export FULL_DATA_PATH=`readlink -f $3`/$CONTAINER_NAME
print_msg "Container Name: '$CONTAINER_NAME'"
print_msg "Creating directory '$FULL_CONTAINER_PATH'"
mkdir `pwd`/data/$CONTAINER_NAME
mkdir `pwd`/containers/$CONTAINER_NAME
mkdir $FULL_DATA_PATH
mkdir $FULL_CONTAINER_PATH
print_msg "Decompress alpine rootfs into '$FULL_CONTAINER_PATH'"
sudo tar xf rootfs/alpine-minirootfs-3.5.1-$ARCH.tar.gz -C $FULL_CONTAINER_PATH
sudo chmod 0755 $FULL_CONTAINER_PATH
chmod 0755 $FULL_CONTAINER_PATH
## set dns
echo "nameserver 84.200.70.40" >> $FULL_CONTAINER_PATH/etc/resolv.conf
@ -61,12 +64,13 @@ print_msg "Create user $CONTAINER_NAME"
useradd $CONTAINER_NAME --no-create-home -p=''
export CONTAINER_UID=`id $CONTAINER_NAME -u`
export CONTAINER_GID=`id $CONTAINER_NAME -g`
print_msg "Ok uid: $CONTAINER_UID gid: $CONTAINER_GID"
print_msg "uid: $CONTAINER_UID gid: $CONTAINER_GID"
print_msg "Create container $CONTAINER_NAME"
export TERMINAL=false
export DEPLOY=true
#export CAPABILITIES=', "CAP_SYS_ADMIN", "CAP_CHOWN", "CAP_FOWNER", "CAP_NET_RAW", "CAP_SETGID", "CAP_SETUID", "CAP_SYS_CHROOT"'
export CAPABILITIES=""
./runc.template > config.json
## mount with loggedfs container root
@ -74,13 +78,13 @@ loggedfs -l files_$CONTAINER_NAME.log -c config.xml -p $FULL_CONTAINER_PATH
## run chroot
print_msg "
\n
\n\n
I'm running chroot now, all opened files will be logged in $CONTAINER_NAME.log\n
\n
- Install and setup your stuff, if you need some package use 'apk update' and 'apk search'\n
- Configure your process to use /data as storage point (/ will be read-only)\n
- Clean $CONTAINER_NAME.log 'echo "" > $CONTAINER_NAME.log'\n
- Start your process, exit on done!\n\n
- Configure your process to use /data as storage path (/ will be read-only)\n
- Clean $CONTAINER_NAME.log from host machine: 'echo "" > $CONTAINER_NAME.log'\n
- Start your process and try to activate funtionality, exit on done!\n\n
"
@ -89,61 +93,45 @@ mount -t sysfs sys $FULL_CONTAINER_PATH/sys/
mount -o bind /dev $FULL_CONTAINER_PATH/dev/
chroot $FULL_CONTAINER_PATH sh
## let's copy all used files/symlink in a new shiny dir
escaped_path=$(echo $FULL_CONTAINER_PATH | sed -e 's/\//\\\//g')
echo "ESCAPED_PATH: $escaped_path"
mkdir `pwd`/containers/$CONTAINER_NAME.tmp
mkdir $FULL_CONTAINER_PATH.tmp
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`
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
for f in $files; do
echo $f
cp --parents $f ../$CONTAINER_NAME.tmp/
cp --parents $f $FULL_CONTAINER_PATH.tmp/
done
for l in $links; do
to=$(ls -la $l | sed -rn "s/.*-> (.*)/\1/p")
echo "$l -> $to"
ln -s $to ../$CONTAINER_NAME.tmp/$l
ln -s $to $FULL_CONTAINER_PATH.tmp/$l
done
cd ..
cd -
umount $FULL_CONTAINER_PATH/proc
umount $FULL_CONTAINER_PATH/dev
umount $FULL_CONTAINER_PATH/sys
umount $FULL_CONTAINER_PATH
#export TERMINAL=true
#export DEPLOY=false
#export CAPABILITIES=', "CAP_SYS_ADMIN", "CAP_CHOWN", "CAP_FOWNER", "CAP_NET_RAW", "CAP_SETGID", "CAP_SETUID", "CAP_SYS_CHROOT"'
#CONTAINER_UID=0
#CONTAINER_GID=0
#./runc.template > config.dev.json
ORIG_SIZE=`du -hs $FULL_CONTAINER_PATH`
LHC_SIZE=`du -hs $FULL_CONTAINER_PATH.tmp`
print_msg "ORIGINAL CONTAINER SIZE: `du -hs $FULL_CONTAINER_PATH` // N_FILES: `find $FULL_CONTAINER_PATH | wc -l`"
print_msg "LHC CONTAINER SIZE: `du -hs $FULL_CONTAINER_PATH.tmp` // N_FILES: `find $FULL_CONTAINER_PATH.tmp | wc -l`"
mv $FULL_CONTAINER_PATH $FULL_CONTAINER_PATH.orig
mv $FULL_CONTAINER_PATH.tmp $FULL_CONTAINER_PATH
print_msg "Place specify your init command in config.json and try to run 'runc run $CONTAINER_NAME'$"
cp config.json runc.config/$CONTAINER_NAME.json
#print_msg "Patch inittab"
## modify inittab to fix alpine tty/console issue!
## comment all ttyN respawn lines
#sudo sed -i "s/^.*respawn:\/sbin\/getty.*/#&/" $fullContainerPath/etc/inittab
## and add a line for a console
#sudo bash -c 'echo "console::respawn:/sbin/getty 38400 /dev/console" >> $fullContainerPath/etc/inittab'
#print_msg "Update package"
## update package
#sudo systemd-nspawn -D $fullContainerPath -M $containerName apk update
#print_msg "Install vim / git"
#sudo systemd-nspawn -D $fullContainerPath -M $containerName apk add vim git openrc
#print_msg "Ready"
#sudo systemd-nspawn -bD $fullContainerPath -M $containerName
##
#sed "s/.*\"\(.*\)\".*/\1/" file
###### CHECK #####

Binary file not shown.

Binary file not shown.

View file

@ -7,15 +7,13 @@ cat <<EOF
"arch": "$ARCH"
},
"process": {
"terminal": $TERMINAL,
"tty": true,
"args": ["sh"],
"terminal": false,
"tty": false,
"user": {
"uid": $CONTAINER_UID,
"gid": $CONTAINER_GID
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
@ -25,7 +23,6 @@ cat <<EOF
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
$CAPABILITIES
],
"rlimits": [
{
@ -37,13 +34,13 @@ cat <<EOF
"noNewPrivileges": true
},
"root": {
"path": "./containers/$CONTAINER_NAME",
"readonly": $DEPLOY
"path": "$FULL_CONTAINER_PATH",
"readonly": true
},
"mounts": [
{
"type": "bind",
"source": "./data/$CONTAINER_NAME",
"source": "$FULL_DATA_PATH",
"destination": "/data",
"options": [ "rbind", "rw", "noexec" ]
},