Jail the Puma

Objectives

My goal is to run multiple silly ruby web applications one one machine, with the following caveats:

  • I’ll use fancy libs, not available on my host packaging system (there can be only one of those)
  • Theses libs, my code using it, and Ruby itself are probably riddled with vulnerabilities
  • I want to maintain & update as little OS & environment as possible

Docker, which means “Cancer” in ancient greek, is a no-go because

  • It’s cancer

I’ve decided to go with a simple LXC for the “special environement” with the shiny new packages I can play with. In this LXC, I’ll run each of my snowflakes web app in their own sandbox, using Firejail.

All the following is running a LXC, running debian Sid. The LXC host is debian Stable running the Debian grsec kernel.

Step 1: the Ruby code

I usually write my silly webapps with Ruby + slim + sinatra. They are all nicely packaged in Debian Sid.

They look like this

$ tree /var/www/kurzy
.
├── config.ru
├── db
│   └── kurzy.sqlite
├── kurzy.rb
├── public
│   ├── jquery-3.2.1.min.js
│   └── kurzy.js
└── views
    └── main.slim

Running ruby kurzy.rb will spawn a Rack webserver listening on port 4567. The problem with that is it’s single threaded and heavy and weird.

Step 2: unleash the puma

Even though it has a .io domain name, I’ve decided to use puma for the webserving part. It advertises some multithreading capabilities, and is also nicely packaged in Debian Sid.

This is where it gets a bit hairy, or furry (lol). I’m going to deploy more than one silly webapp, and need some kind of centralized management of them so I can start/stop part or all of the apps.

Puma is easier dealt with when you use config files. I’ll store them in /etc/puma.conf.d

$ cat /etc/puma.conf.d/kurzy.conf
directory '/var/www/kurzy'
environment 'production'
daemonize
#pidfile '/var/www/kurzy/puma/puma.pid'
#state_path '/var/www/kurzy/puma/puma.state'
stdout_redirect '/var/www/kurzy/puma/stdout.log', '/var/www/kurzy/puma/stderr.log', 'true'
bind 'tcp://10.0.3.40:9290'
tag 'Kurzy'
quiet

the .pid & state files are not going to be really used there, they probably can be ignored. Don’t forget to create the puma folder in your app dir.

Your puma will listen on 9290, we’ll need this to drill holes in the host firewall and link it to nginx.

Step 3: Firejail, aka ‘bruteforcing config files & bash scripts until it works’

From here, this is mainly just madness from my part, as I don’t really know what I’m doing. You’ve been warned.

Each Puma will start in its own sandbox. Despite the fact that the website is hosted on Wordpress (srsly), Firejail is nicely packaged again, and definitely works out of the box, with caveats.

Grsecurity: Firejail does weird mount dances in chrooted environment, make sure you allow this.

The defaults for a Firejail sandbox are nice, but I wanted to disallow access to stuff below /var/www/ from /var/www/sillywebapp

This is a profile example:

read-write /var/www/kurzy
noblacklist /var/www/kurzy
blacklist /var/www/*
include /etc/firejail/default.profile

Store this file in your webapp dir to make it easier later.

I’m running these from a puma system user, and will start them from a init script. As seen in the Firejail profile, I allow /var/www/kurzy to be writeable. For this, /var/www/kurzy need to be owned by the puma user.

chown -R puma /var/www/kurzy

To test that everything looks nice:

su -s /bin/sh -c "firejail --profile=/var/www/kurzy/firejail.profile -- /usr/bin/puma -C /etc/puma.conf.d/kurzy.conf" puma

The server should start as user ‘puma’ and listen en port 9292. If it doesn’t, good luck. Try to append –debug everywhere and strace all the things. It’s a huge mess.

Step 4: Automate the madness

Everything is nice and tight. Repeat for all your webapps. Update the firejail profile to your needs.

Here is a simple example for a shitty /etc/init.d/puma to automate all of the start & stopping:

```bash
#! /bin/sh
### BEGIN INIT INFO
# Provides:          puma
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Puma web servers
# Description:       Start all the pumas
### END INIT INFO

# "set -e" probably breaks everything

# Classic stuff
PATH=/usr/local/bin:/usr/local/sbin/:/sbin:/usr/sbin:/bin:/usr/bin
DESC='Pumas motherboard script'
NAME=puma
SCRIPTNAME=/etc/init.d/$NAME
PID_DIR=/var/run/puma
# Specifiv coptions
FIREJAIL_BIN='/usr/bin/firejail'
FIREJAIL_OPTS='--quiet'
PUMA_BIN='/usr/bin/puma'
PUMA_OPTS='--quiet'
PUMA_USER='puma'
PUMA_CONFIG_DIR='/etc/puma.conf.d'

. /lib/init/vars.sh

. /lib/lsb/init-functions

if [ ! -d "${PUMA_CONFIG_DIR}" ]; then
    log_failure_msg "missing or empty config file $RSYNC_CONFIG_FILE"
    exit 0
fi

if [ ! -d "${PID_DIR}" ]; then
    log_warning_msg "creating ${PID_DIR}"
    mkdir "${PID_DIR}"
fi

read_puma_conf_val() {
    local readonly conf_file=$1
    local readonly key=$2
    echo $(grep "${key}" "${conf_file}" | cut -d ' ' -f 2 | sed "s/\"//g" | sed s"/'//g")
}

is_running_puma() {
    local readonly app_name=$1

    local firejail_cmd="${FIREJAIL_BIN} --quiet --list"
    su -s /bin/sh -c "${firejail_cmd}" "${PUMA_USER}" | grep -q "name=app_${app_name}"
}

do_start_one_puma() {
    local readonly app_config=$1
    local readonly app_filename=$(basename "${app_config}")
    local readonly app_name="${app_filename%.*}"
    local readonly app_workdir=$(read_puma_conf_val "${app_config}" "directory")

    if is_running_puma "${app_name}" ; then
        log_success_msg "${app_name} already running"
        return 0
    fi

    if [ ! "$(stat -c %U ${app_workdir})" = "${PUMA_USER}" ]; then
        log_failure_msg "$app_workdir should be owned by ${PUMA_USER}"
        return 1
    fi
    log_daemon_msg "Starting ${app_name}"

    local puma_cmd="${PUMA_BIN} ${PUMA_OPTS} -C ${app_config} >/dev/null &"

    if [ ! -f "${app_workdir}/firejail.profile" ]; then
        generate_firejail_profile "${app_workdir}/firejail.profile" "${app_workdir}"
    fi
    jail_opts="${FIREJAIL_OPTS} --profile=${app_workdir}/firejail.profile"

    local firejail_cmd="${FIREJAIL_BIN} ${jail_opts} --name=app_${app_name} -- ${puma_cmd}"
    su -s /bin/sh -c "${firejail_cmd}" "${PUMA_USER}"
    log_end_msg $?
}

generate_firejail_profile() {
    local readonly profile_file=$1
    local readonly app_workdir=$2
    local app_workdir_parent=$(dirname "${app_workdir}")

    cat << EOF > "${profile_file}"
# This has been autogenerated by ${SCRIPTNAME}
# Feel free to modify to your needs
read-write ${app_workdir}
noblacklist ${app_workdir}
whitelist ${app_workdir}
blacklist ${app_workdir_parent}/*
include /etc/firejail/default.profile
EOF
}

do_stop_one_puma() {
    local readonly app_config=$1
    local readonly app_filename=$(basename "${app_config}")
    local readonly app_name="${app_filename%.*}"

    if ! is_running_puma "${app_name}" ; then
        log_success_msg "${app_name} not running"
        return 0
    fi
    log_daemon_msg "Stopping ${app_name}"

    local firejail_cmd="${FIREJAIL_BIN} ${FIREJAIL_OPTS} --shutdown=app_${app_name} > /dev/null"
    su -s /bin/sh -c "${firejail_cmd}" "${PUMA_USER}"
    log_end_msg $?
}

do_start() {
    for puma_config in "${PUMA_CONFIG_DIR}"/*.conf; do
        test -f "${puma_config}" || continue
        do_start_one_puma "${puma_config}"
    done
}

do_stop() {
    for puma_config in "${PUMA_CONFIG_DIR}"/*.conf; do
        test -f "${puma_config}" || continue
        do_stop_one_puma "${puma_config}"
    done
}

do_status() {
    local firejail_cmd="${FIREJAIL_BIN} --list"
    su -s /bin/sh -c "${firejail_cmd}" "${PUMA_USER}"
}

case "$1" in
  status)
    do_status
  ;;
  start)
    echo "Starting the pumas"
    do_start
  ;;
  stop)
    echo "Stopping the pumas"
    do_stop
  ;;
  restart)
    echo "Restarting $DESC"
    do_stop && do_start
  ;;
  *)
    echo "Usage:" >&2
    echo "  $SCRIPTNAME {start|stop|restart|status}" >&2
  ;;
esac
```

Example use:

# service puma restart
Restarting Pumas motherboard script
Stopping kurzy:.
Stopping liste:.
Starting kurzy:.
Starting liste:.
# service puma status
14881:puma:/usr/bin/firejail --quiet --profile=/var/www/kurzy/firejail.profile --name=app_kurzy -- /usr/bin/puma --quiet -C /etc/puma.conf.d/kurzy.conf
14903:puma:/usr/bin/firejail --quiet --profile=/var/www/liste/firejail.profile --name=app_liste -- /usr/bin/puma --quiet -C /etc/puma.conf.d/liste.conf

Step 5: Plug this nightmare into nginx

Do the necessary firewalling holes, and add a new nginx site:

# cat /etc/nginx/sites-enabled/goto.ninja
server {
    listen 80;
    listen [::]:80;
    server_name www.goto.ninja goto.ninja;

    location /.well-known {
        alias /var/www/letsencrypt/.well-known;
    }

    location / {
        return 301 https://goto.ninja$request_uri;
    }
}

server {
    listen 443;
    listen [::]:443;
    server_name goto.ninja;

    ssl_certificate /etc/nginx/ssl/letsencrypt/goto.ninja.pem;
    ssl_certificate_key /etc/nginx/ssl/letsencrypt/goto.ninja.key;

    location / {
        proxy_pass http://10.0.3.40:9290$request_uri;
    }
}

Running everything required for kodi in a LXC WITH NO SYSTEMD BULLSHIT WHATSOEVER

But…. why?

Because:

  • why not
  • it’s kinda fun
  • you get to learn things
  • I can try bleeding edge stuff in my Debian Stable host
  • some kind of containment, but as we’ll see this implementation is FAR FAR AWAY from secure things

Do the things

Setup the Host

So I’m running this on a almost-all-you-need-in-a-motherboard ASrock J3160DC. You need a new-ish kernel so all the good stuff in the Intel chipset is loaded. Also don’t disable stuff in your BIOS.

I’ll output video & sound through the HDMI cable.

Check that the following devices exist. If you don’t, try a more recent kernel (in this example I have linux-image-4.7.0-0.bpo.1-amd64 from jessie-backports).

# ls /dev/dri/card0 
/dev/dri/card0
# ls /dev/snd/
by-path  controlC0  hwC0D0  hwC0D2  pcmC0D0c  pcmC0D0p  pcmC0D1p  pcmC0D2c  pcmC0D3p  pcmC0D7p  pcmC0D8p  seq  timer

You’ll of course need some packages for your container.

apt-get install lxc 

While you’re here, remove some crap

apt-get remove --purge  systemd systemd-shim cgmanager

Not sure about this one:

apt-get install i965-va-driver

Create the Debian Sid Guest

This is valid only if you use a LV for your rootfs. Modify accordingly.

 lxc-create -n kodi-lxc -t debian -B lvm --vgname VG00 --fssize 5G -- -r sid

Update your LXC config. This is very dirty, as it gives your guest access to your host’s hardware which basically defeats the purpose of container. Oh well.

# Common configuration
lxc.include = /usr/share/lxc/config/debian.common.conf

# HERE DO YOUR NETWORK CONFIG
#lxc.network.type = veth
lxc.network.flags = up
# that's the interface defined above in host's interfaces file
lxc.network.link = ....
lxc.network.hwaddr = ....
lxc.network.ipv4 = ....
lxc.network.ipv4.gateway = ....
lxc.rootfs = /dev/VG00/kodi-lxc
lxc.mount.entry = /dev/snd dev/snd none bind,optional,create=dir
lxc.mount.entry = /dev/dri dev/dri none bind,optional,create=dir
lxc.mount.entry = /dev/input dev/input none bind,optional,create=dir
lxc.mount.entry = /dev/tty7 dev/tty7 none bind,optional,create=file
lxc.mount = /var/lib/lxc/kodi-lxc/fstab
lxc.utsname = kodi-lxc
lxc.arch = amd64
lxc.autodev = 1
lxc.kmsg = 0

lxc.cgroup.devices.allow = c 226:0 rwm # /dev/dri/card0
lxc.cgroup.devices.allow = c 136:6 rwm # /dev/console
lxc.cgroup.devices.allow = c 116:* rwm # /dev/snd/*
lxc.cgroup.devices.allow = c 13:* rwm  # /dev/input/* input devices
lxc.cgroup.devices.allow = c 4:7 rwm   # /dev/tty7	

Do what you need to connect to your guest, then it’s time for some the usual hygiene procedure.

echo -e "Package: systemd-sysv\nPin: release o=Debian\nPin-Priority: -1" > /etc/apt/preferences.d/no-systemd
echo -n "deb http://http.debian.net/debian sid main contrib non-free" > /etc/apt/sources.list
apt-get install sysvinit-core sysvinit-utils
apt-get remove --purge  systemd systemd-shim cgmanager
apt update; apt upgrade

Not sure all these are needed, but I did the following, and it works.

apt install alsa-utils i965-va-driver kodi mesa-utils xserver-xorg xserver-xorg-input-kbd xserver-xorg-video-all 

Kodi runs nicely as a normal user (stolen from Kodi’s wiki)

adduser --disabled-password --disabled-login --gecos "" kodi
usermod -a -G cdrom,audio,video,plugdev,users,dialout,dip,input kodi

To have all the things starting up when you boot your guest, put this in ̀/root/xinit.sh:

#!/bin/bash
/bin/bash --login -c "/usr/bin/X vt7"

And in your crontab

@reboot cd /root; bash xinit.sh

Your Xorg will be sad to not have any udev/evdev to help him figure out stuff, so disable auto-device-discovery-magic in a custom /etc/X11/xorg.conf:

Section "ServerLayout"
	Identifier  "Configured"
	Option "AutoAddDevices" "false"
EndSection
																														
Section "InputDevice"
	Identifier "Keyboard0"
	Driver "kbd"
	Option "XkbLayout" "fr"
EndSection
																														
Section "Screen"
	Identifier "Default Screen"
	Device "i915"
EndSection

Then for the user kodi, I made a silly script /home/kodi/kodi.sh

logger "Trying to start kodi"
while true ; do
    if [[ `pidof kodi.bin` == "" ]]; then
        if [ -f /tmp/.X0-lock ] ; then
            logger "X is here! starting kodi"
            DISPLAY=:0 kodi-standalone
            logger "Kodi over and out"
        fi
    else
        logger "kodi is around already"
        exit
    fi 
    sleep 2
done

that is started ̀@reboot in the user’s crontab.

Try everything ! oh oh oh oh ohhhh

WARNING WARNING WARNING WARNING

If you’re like me and like umask 0077, remember to umask 0022 before starting your LXC, when ̀lxc.autodev is set to 1:

lxc-stop --kill -n kodi-lxc ; umask 0022 ; lxc-start -n kodi-lxc -d

And after a minute you should see kodi coming around on your screen/TV.

Coop's beer "adventskalendar"

792cl of swiss craft beer

Coop is offering, for December, an advent calendar tailored to beeraholics:

24th December 2015

And the last beer is the India Pale Ale from Brasserie des trois dames

Nice and very hoppy IPA, nothing more to say.

23th December 2015

Ueli bier Winterbogg from Brauerei Fischerstube

Okay beer, nothing very interesting. Maybe I shouldn’t have drunk this one before the Tempête.

22th December 2015

Tempête from Docteur Gab’s

This triple tastes strong (even if only 8%). Very “yeasty-dry”somthing after taste that I don’t enjoy very much

21th December 2015

Single Hop Black Ale from Doppelleu

Stouts are not my favorite beer, but this one is not too smoky.

20th December 2015

Old H from Bier Factory

Nice “full” ale. The honey part is a little bit in the aftertaste. I usually don’t like honey beer, but this one is okay.

19th December 2015

La Rossa from San Martino

This foaming bitch is actually a pretty decent amber ale. I like the berries aftertaste.

18th December 2015

Dinkel from Einsiedler Bier

Light amber taste with the sweetness or the “corn beer”. Is okay.

17th December 2015

La Torpille from BFM.

Nice sweet brown beer without too much extra.

16th December 2015

Zwickel Bier from Rugenbräu

“German”-like lager. Not much taste or bitterness but still kinda refreshing.

15th December 2015

Espresso Stout from Lägerebräu

A surprising combination of a beer and coffee taste. Really tastes like a mix of both. Not too bad, nice experiment.

14th December 2015

Rheintaler Maisbier from Sonnenbräu

Almost bland lager. You can maybe taste the corn a little. Pass.

13th December 2015

Little Monster Double IPA from Storm&Anchor

Great double IPA, strong, nice “citrus” hops taste. Like a Punk IPA but way better. Would totally get drunk from it.

12th December 2015

Baarer Festbier from Baarer Bier

This one is weird. smells like damp dirty hair, has a quickly disappearing old beer taste. But still kinda okay.

11th December 2015

Braumeisters Rauchbier from Stadtbühler

Weird “bum beer” after taste, maybe from the Whiskymalz. Wouldn’t drink again.

10th December 2015

Pale ale from Müller Braü

Amber-ish lager with a little bit of bitterness. Refreshing and nice.

9th December 2015

Tschlin Ambra from Biera Engiadinaisa

Friggin beer from Romansh speaking part of Switzerland!

Inbetween “amber” and “white” beer. Just a little bit sweet and “round/smooth” taste. Tons of yeast which is nice.

Would drink again.

8th December 2015

Marenghin from Surselva Bier

Very nice lager, tastes classic but “full”.

7th December 2015

WeiZZZen from Felsenau

Smooth and “round” (whatever that means) white beer, oh so lightly spiced. I taste the Belgium. Very nice!

6th December 2015

Chlausen from öufi Bier

Tastes cheap (you could actually tell it before tasting: the cap could untwist). Very sweet. Okay to drink in the summer if you’re thirsty I guess.

5th December 2015

Schwarzer Kristall from Appenzeller Bier.

I’m not a fan of Stouts, but this one has a nice chocolate, not-too-burnt flavor. Wouldn’t buy one for myself but I still enjoyed drinking it.

4th December 2015

Eidgenoss from Falken

One of those Amber which tastes like bad wine. Very yuck to me. Never again.

3rd December 2015

01 Spezial Hell from Bier Paul

Smells weird, tastes like one of those Heineken lager. A bit yuck.

# 2nd December 2015

02 Alpen Pale Ale from Lichtensteiner Brauhaus

Very nice fruity-flowery pale ale. Just a little bit bitter.

Would drink again!

1st December 2015

Vrenelisgärtli from Adler Bräu

White beer, tastes a little dry. meh.

Fix all the things!

Sometimes everything is not working like they should.

Fix the washing machine

My washing machine (Indesit WIDL 126 ) decided to stop working (all the lights set to blink). So thanks to some French site, I knew I could fix it.

  1. cut the water, unplug everything.
  2. move the heavy fucker so you can open its back
  3. extract the board
  4. replace blown capacitor (big brown one on the ugly pic)
  5. put it back all together
  6. clean your clothes

Hail the many capacitors kit!

## Fix the iStick Eleaf

Chinese lithium batteries tend to die quickly. Fortunately I had a spare one for the iStick from another one I bought just to bry open, and destroy it trying to find some JTAG pinouts, and failing.

This was quite easy. But here’s more french links for documentation.

I hope this one doesnt blow up in my hands.

Update : Still, THE RASPBERRY PI 2 WILL COMPLAIN THAT IT GETS NOT ENOUGH JUICE. Indeed the ‘fast charge’ USB ports are only rated at 1.2A, when the Pi2 requires 1.8A.

I wanted to play with my new Raspberry Pi 2, but it so happened that the “verified” D-Link 7 Port USB Hub DUB-H7 is not what it says it is.

The bastard will only output some extra power in the “Fast Charge” port IF the hub isn’t used on a PC. Unless you install some crazy exe/driver from the website. WTF

Anytime the hungry RPi2 was trying to initialize the hub, for example at boot time, the fucking hub would just switch to puny output power, and the rPi2 would reboot.

Fortunately some other French dude described how to force open the power gates, with just 2 tiny bridges added.

I’ve shamelessly copied his pic for backup purposes.