Posted by slinky

Sandboxing a PDF Reader

Here is a short technical note about sandboxing a PDF reader (Okular) with Firejail.

Firejail is a Linux sandbox using the namespaces and seccomp-bpf features of the Linux kernel.

In my opinion, Okular is feature-wise the best PDF reader so that is what I'll use here; however, the instructions can be extended to apply to other readers such as Evince or Acrobat Reader.

The goals:

  • Okular can only see the PDF it is reading, nothing else.
  • Okular will not be able to reach the network.
  • The network will not be able to reach Okular.
  • Okular will not be able to see other processes.
  • Be able to open PDFs via the Nautilus file manager (Ubuntu).

The motivation for this is to reduce the attack surface, should a hostile PDF be opened.

What you need

Mise en place, you'll need:

The Firejail profile

To sandbox an application in Firejail you need a profile. There are built-in profiles, but let's make a custom one.

Replace YOURUSER with your username, some paths must be absolute.

I use a profile like this:

# Generic GUI application profile
include /etc/firejail/
include /etc/firejail/
include /etc/firejail/
blacklist ${HOME}/.pki/nssdb
caps.drop all
protocol unix,inet,inet6

# Okular stuff starts here
read-only /etc
read-only /dev
read-only /boot
read-only /usr
read-only /opt
read-only /bin
read-only /sbin
read-only /lib
read-only /lib32
read-only /lib64
read-only /var
read-only /media
read-only /root
read-only ${HOME}

noblacklist ${HOME}/.kde/share/config/okularpartrc
whitelist ${HOME}/.kde/share/config/okularpartrc
read-only ${HOME}/.kde/share/config/okularpartrc
# cache-* depends on the host name
noblacklist ${HOME}/.kde/cache-okular
whitelist ${HOME}/.kde/cache-okular
whitelist ${HOME}/.kde
whitelist /tmp/.X11-unix/X0


# the fonts stuff is needed for proper display of PDFs
private-etc passwd,groups,fonts/fonts.conf,fonts/conf.d,fonts/conf.avail

# no ${HOME} supported here apparently
netfilter /home/YOURUSER/.config/firejail/


hostname okular
name okular

Basically a lot of paths are set as read-only and the Okular configuration, a cache and the X11 socket are specifically whitelisted. The latter is problematic, and is a hole in the application isolation. It's possible to isolate X11 applications too, but that is not described here.

Custom network filtering is also set up. The "net none" directive caused problems with X11 connections.

The contents of the network isolation script (called are as follows:

$ cat /home/YOURUSER/.config/firejail/

-A INPUT -i lo -j DROP

It just drops all incoming and outgoing traffic. Note that the X11 socket was specifically whitelisted in the profile.

Test this setup

A simple test can be done by running a shell such as Bash instead of Okular.

Note how Firejail is specifically told the profile to use, and just one file is whitelisted (as read-only). In the example, ~/Stuff contains a lot of other files:

YOURUSER@NSA-mainframe:~/Stuff$ ls -1

The funny names are straight from Citeseer. Here is what the Firejail test looks like with the --trace enabled:

YOURUSER@NSA-mainframe:~/Stuff$ firejail --trace --profile=/home/YOURUSER/.config/firejail/okular.profile --whitelist=`readlink -f` --read-only=`readlink -f` bash
Reading profile /home/YOURUSER/.config/firejail/okular.profile
Reading profile /etc/firejail/
Reading profile /etc/firejail/
Reading profile /etc/firejail/
Warning: --trace and --tracelog are mutually exclusive; --tracelog disabled
Parent pid 8232, child pid 8233

Child process initialized
11:bash:open /dev/tty:3
11:bash:open /dev/tty:3
11:bash:fopen /etc/passwd:0xaa5808
11:bash:open /etc/bash.bashrc:-1
11:bash:open /home/YOURUSER/.bashrc:-1
11:bash:open /home/YOURUSER/.bash_history:-1
11:bash:access /lib/terminfo/x/xterm-256color:0
11:bash:fopen /lib/terminfo/x/xterm-256color:0xab3808

# List the files, only the whitelisted file is seen among the trace output:

[YOURUSER@okular Stuff]$ ls -1
12:ls:fopen64 /proc/filesystems:0xc62010
12:ls:opendir .:0xc67c10

# Try the network:

[YOURUSER@okular Stuff]$ nslookup
13:nslookup:socket AF_INET SOCK_STREAM IPPROTO_IP:3
13:nslookup:socket AF_INET6 SOCK_STREAM IPPROTO_IP:3
13:nslookup:socket AF_LOCAL SOCK_STREAM IPPROTO_IP:3
13:nslookup:fopen64 /usr/lib/ssl/openssl.cnf:(nil)
13:nslookup:fopen64 /etc/resolv.conf:(nil)
13:nslookup:socket AF_INET SOCK_DGRAM IPPROTO_UDP:6
13:nslookup:bind port 0:0
13:nslookup:socket AF_INET6 SOCK_DGRAM IPPROTO_UDP:6
13:nslookup:bind :::0
;; connection timed out; no servers could be reached

[YOURUSER@okular Stuff]$ exit
11:bash:open /home/YOURUSER/.bash_history:-1
11:bash:open /home/YOURUSER/.bash_history:-1
11:bash:open /home/YOURUSER/.bash_history:-1

parent is shutting down, bye...

System-wide setup

You need to be root for this.

It's not possible to specify variable parts like the invoked file in the profile, so we'll make a wrapper instead.

$ mv /usr/bin/okular /usr/bin/okular.original
$ vim /usr/bin/okular
$ ls -l /usr/bin/okular*
-rwxr-xr-x 1 root root    97 Feb 20 19:38 /usr/bin/okular
-rwxr-xr-x 1 root root 90752 Aug  5  2014 /usr/bin/okular.original
$ cat /usr/bin/okular

# first expand to fully qualified filename
REAL_FILE=`readlink -f "$1"`

firejail --profile=/home/YOURUSER/.config/firejail/okular.profile \
   --whitelist="$REAL_FILE" --read-only="$REAL_FILE" okular.original "$REAL_FILE"

The file given as parameter is first expanded to a fully qualified absolute name, then whitelisted.

(Note that Firejail prohibits certain characters from the filename, you can easily disable or change this filter in function invalid_filename in the file util.c)

Now you might ask: "wait, we blocked the network, so can I still read documents from a network share?". That is a good question, and the answer is yes, you can. Generally, you just see a filesystem and another process takes care of reading/writing to the network share - the network stuff is not done by the Okular process.

Nautilus integration

Integration into Nautilus is very easy. Note, the "somefile.pdf" does not need to exist:

$ mimeopen -d somefile.pdf
Please choose a default application for files of type application/pdf

        1) Document Viewer  (evince)
        2) GIMP Image Editor  (gimp)
        3) Print Preview  (evince-previewer)
        4) Other...

use application #4
use command: okular

Now when right-clicking in Nautilus, the default selection is "okular" (the sandboxed one).

Gains and losses

What was gained?

Let's revisit the goals:

  • Okular can only see the PDF it is reading, nothing else. Yes!
  • Okular will not be able to reach the network. Yes!
  • The network will not be able to reach Okular. Yes!
  • Okular will not be able to see other processes. Yes!
  • Be able to open PDFs via the Nautilus file manager (Ubuntu). Yes!

Another note: one can still click on a link inside a PDF and open the link in a web browser; this is conducted via the DBus and may or may not be what you want.

What was lost?

One downside is that only one Okular instance can be open at a time. This is because the PID namespacing (CLONE_NEWPID) causes the PIDs to start from a low number inside the sandbox, and two different instances end up eventually registering themselves to DBus with the same PID, causing the latter registration to fail, and thus leading to the application stopping.

I'm not yet sure what would be the best way to work around this, perhaps by creating a full-blown (lxc) container for each Okular instance. This way each Okular would have their own DBus. Or perhaps one could run separate DBus instances without a full OS container.

Web Slinging

This is a short technical note describing how to setup aiohttp-wsgi and aiohttp to serve Django via WSGI while allowing WebSockets on the same port.

Hopefully this setup is useful to you for doing awesome things. Note, this is just a technical note to use as a starting point and I have not benchmarked the performance in any way. I work daily with (more or less) embedded systems, big and small, and all this web stuff is just a hobby, so to speak.

What you need

Firstly, mise en place. You need:

  • Python 3
  • aiohttp
  • aiohttp-wsgi
  • your Django app
  • nginx

Why do we use these

Python 3.4+ is a must since you need the asyncio support. This gives you asynchronous I/O, event loops, coroutines, and tasks. These basically let you do co-operatively scheduled single-threaded concurrent code which is quite useful when dealing with inputs/output handling from, say, the network.

The aiohttp is an (almost) pure Python HTTP server (and client), using the asyncio support. It provides WebSocket handling, among other things.

The aiohttp-wsgi is a WSGI adapter for aiohttp. This is a bit more flexible than the built-in aiohttp.wsgi. The lack of setup instructions in aiohttp-wsgi was actually the main motivation in putting up this note.

Your Django app, this is any Django application you want to make asyncio-capable, to add WebSocket support to, etc.

Nginx is a fast HTTP and reverse proxy server. You could probably also use HAProxy or similar tools as well.

Prepare aiohttp-wsgi

Note: be sure to use version 0.1.1 or higher, as in 0.1.0 Django responses will make aiohttp-wsgi nag with a RuntimeError.

The big picture

The satellite level view is as follows:

  • The client connects to port 8000.
  • Nginx auto-redirects HTTP (port 8000) to HTTPS (port 8443).
  • SSL handshake and setup are done, further traffic to and from 8443 is SSL protected.
  • Traffic coming to 8443 is decrypted internally and routed to aiohttp WSGI front-end at
  • Here the traffic goes either to WSGI app (Django) or WebSockets.
  • Response is sent to the client.

The ports are high ports to allow an unpriviledged user to test this - no need to be root. You can easily map the ports to suitable priviledged ones like 80, 443 and so on.

The directory structure

The directory tree is set up as follows:

├── nginx
│   ├── log
│   ├── nginx.conf
│   └── ssl
└── yourdjangoapp
    └── yourdjangoapp
        ├── templates
        └── etc.

Note: it is assumed that aiohttp and aiohttp_wsgi are installed system-wide. It's an unimportant detail - you can install into virtualenv instead (I did).

Your Django app

The Django app, which is called "yourdjangoapp" from now on, is a regular Django app. In this note, the app is stupendously simple and basically routes "/" to the index-method, which serves up a templates/index.html file.

The is setup like this:

from yourdjangoapp import views

urlpatterns = patterns('',
    url(r'^$', 'yourdjangoapp.views.index', name='index'),

...and a is setup like this:

from django.shortcuts import render_to_response

def index(request):
    return render_to_response("templates/index.html")

Thus when "/" is accessed, the templates/index.html is sent to the client. The templates/index.html is a truly remarkable gem of state-of-the-art web design and contains:

<body bgcolor=white text=black>
<h1>This is the index...</h1>
<p>This site uses cookies. Get over it.</p>
    var socket = new WebSocket('wss://');
    socket.onopen = function(event) {
        console.log('a user connected');
        socket.send("Howdy ho");
    socket.onclose = function(event) {
        console.log('a user disconnected');
    socket.onerror = function(event) {
        console.log('a user got sour');
    socket.onmessage = function(event) {
        console.log('Server said: ' +;
        // delayed reply
        setTimeout(function() { console.log('I said: Say it again'); socket.send("Say it again"); }, 2000);

The client will ping-pong messages with the server.

Notice the address wss:// This is the IP address of the Nginx reverse proxy SSL port with the WebSocket URL (see below) with an unpriviledged port used for SSL.

Note: you must add the following to yourdjangoapp/ to use the index.html template.

TEMPLATE_DIRS = [("%s/yourdjangoapp" % BASE_DIR)]

The aiohttp WSGI front end (

The is a front-end component to manage routing to WebSockets and to Django. It contains also basic WebSockets logic used for testing.

Let's split the file up and examine the parts, going from the beginning of the file all the way to the end.

First, the basic imports:

import sys
import os
import asyncio

from aiohttp_wsgi import api
import aiohttp

Then, the WebSockets handler for aiohttp.

Note how this is an asyncio coroutine. You can also see the "yield from", which means that at such a point the execution can go elsewhere until the stuff that is yielded from finishes whatever it is doing. Once it does finish, the loop continues from the yield from line. On the next iteration, the same thing happens - there is no blocking for ws.receive_str() to finish.

Note how the send_str doesn't have a yield from.

def ws_testing(request):
    # websockets aiohttp req-resp
    ws = aiohttp.web.WebSocketResponse()

    ws.send_str("Initial server hello")
    while True:
            data = yield from ws.receive_str()
            if data == 'close':
                ws.send_str(data + ' says the server')
        except Exception as exc:
            print(exc.code, exc.message)
            return ws

Django setup for aiohttp is next. It's all in all quite simple - just 2-3 lines, thanks to aiohttp-wsgi. With the Django WSGI application instance, the aiohttp server is configured with the chosen port and host and told to run the Django WSGI application.

# Django setup
sys.path.append("%s/yourdjangoapp" % (os.getcwd()))
from yourdjangoapp import wsgi

loop = asyncio.get_event_loop()
server, app = api.configure_server(

The default routes come from the Django parts ( Let's add another route for the WebSockets. Django does not know about this routing since it happens in the aiohttp level.

# paths handled as WebSockets
app.router.add_route('GET', '/ws', ws_testing)

So what did we do so far? At this point we made "/" go to the Django app, via WSGI. The "/ws" goes to the WebSocket testing method, which simply first sends a message and then starts echoing all client messages back.

Next, a basic event loop is started to loop forever.

except KeyboardInterrupt:
    api.close_server(server, app, loop=loop)

print("All done, post cleanup stuff happens here")

There is no "post cleanup stuff", something could happen there but now it's just a placeholder.

And that's it for the

Nginx configuration

Note, here I assume your username is bob, the nginx files are located under /home/bob/nginx/ and your host IP is

Note: in case you're wondering, the "lh" in the SSL keys means "localhost".

SSL setup

If you need a refresher in how to make an openssl setup for nginx, refer to this tutorial. As a summary:

  • cd ~bob/nginx/ssl
  • openssl genrsa -des3 -out lh.key 2048
  • openssl req -new -key lh.key -out lh.csr
  • Fill in the information
  • Remove the passphrase
    • cp lh.key lh.key.orig
    • openssl rsa -in lh.key.orig -out lh.key
  • openssl x509 -req -days 1500 -in lh.csr -signkey lh.key -out lh.crt
  • Point nginx to the .key and the .crt

That's it for the SSL setup.


worker_processes  10;
error_log         /home/bob/nginx/log/error.log;
pid               /home/bob/nginx/;
worker_rlimit_nofile 8192;

events {
    worker_connections  4096;

http {
    server_tokens     off;

    upstream wsgitest {

    server {
        listen 8443;

        error_log         /home/bob/nginx/log/error-lh.log;
        access_log        /home/bob/nginx/log/access-lh.log;

        root /home/bob/nginx/siteroot;

        # django + ws upgrade
        location / {
            proxy_pass http://wsgitest/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

        ssl on;
            ssl_certificate     /home/bob/nginx/ssl/lh.crt;
            ssl_certificate_key /home/bob/nginx/ssl/lh.key;

    # redirect http --> https
    server {
        error_log         /home/bob/nginx/log/error-lh.log;
        access_log        /home/bob/nginx/log/access-lh.log;
        listen 8000;
        return 301 https://$host:8443/$request_uri;

Testing it

First, start nginx and the aiohttp WSGI front-end:

nginx -c ~bob/nginx/nginx.conf

Then point your browser to Open the developer console. In Firefox the default shortcut for this is Shift-Control-K.

You should see the HTML output from the Django app, and in the console the WebSockets output ping-ponging every 2 seconds.

What's next

  • Stress-test the WS. Thor seemed to break the sort of echoing WebSockets as we have here after a few hundred connections.
  • Stress-test, somehow, a reasonable mix of WS + HTTPS to know where we're at.
  • Profile, make it better.
  • Build something on it!

Alive, again

Without change, things stagnate. When things stagnate, they risk becoming obsolete. Change for the sake of change is inefficient and just spins around chasing its own tail - the change should have a purpose. Therefore: to avoid risk of becoming obsolete, make purposeful changes.

That said, here is some purposeful change: a revamped incarnation of my blog.

The old posts were cleaned up and many were thrown away. I kept the ones which were still interesting to me. You can find the old posts in the Old-category at the top.

Also, the old "Zenburn updated" posts are gone - any future updates will be reflected at the Zenburn repository in Github.

Also, due to massive amounts of spam, the comment system has moved to Disqus. I have kept the old comments only at the Zenburn page since the comments are valuable to a number of people.

Musings on the Future of Home Computing

Researchers at the University of Cambridge, UK, recently demonstrated printing (transparent AND flexible) graphene-based thin-film transistors with a modified ink jet printer. ("Ink-Jet Printed Graphene Electronics" at arXiv)

So what does this mean? In the future, you can download a chip design from the Internet, modify it as required, and fabricate it in your garage with a kind of an inkjet. You can essentially build an entire system by printing the sheets and then combining them with suitable cables and connectors. Maybe the result won't beat an Intel Core i7 in speed, but it will be a treasure trove for hobbyists and professionals worldwide - think today's Arduino-hacking innovators supercharged.

Longer term effects: production and innovation in computing technology manufacturing moving one step below from corporate labs and fabrication plants to homes and hackerspaces. This translates to faster turnaround times: no need to build elaborate marketing campaigns and align release times with Christmas sales, building even 2 prototype chips is feasible, the whole world's experts are available, and so on. (Having free and open (as in speech and beer) hardware will be a major factor in this development - one could close the hardware off but the development convenience would suffer and speed would slow down as a result.) The application areas will also move beyond just "cool, I just printed a tiny logic circuit" to "cool, I just printed an ARM core" and beyond. Once this speed of innovation is applied to neighbouring areas such as wireless communications technologies, then we will truly see some interesting developments.

Will this destroy massive corporate R&D projects? No, I don't think so, there won't be interference until a lot of time passes. It takes expensive and complex equipment to research and develop a memristor, for example. But the speed of remixing existing technology and improving it will increase. Also, the distribution of technology will move beyond the shackles of "the market is just 10k people, forget about it". As a summary: self-fabbing printed circuits will take care of evolutionary paths, corporate R&D of big revolutions, and meanwhile the long tail will become flatter and longer.

Principles of Ubiquitous Computing

Here's a presentation I made at the 15th Summer School of Telecommunications in 2006. The subject is "Principles of Ubiquitous Computing".

In retrospect, there are some notes to be made. Back then when I was reading the available literature and research, there was a kind of concensus that the peer-to-peer model of communication - device-to-device communication without intermediaries - would play a big role, as this would let the device deployments scale without requiring new or existing static network infrastructure. However, the bulk of the ubiquitous computing devices of today (sensors, smart phones, electrical consumption readers, etc.) rely on static communications infrastructure to function.

Also, the "Spam/Big Brother Society" is as relevant a danger as then. As I see it, the danger has merely evolved and is even more extensive today.

Today, more and more information about private individuals are collected with the justification of "with the information, we can show you more relevant advertisements". The infrastructure of knowing who you are, what you think and who you know is in place to learn what stuff or services we might be currently missing.

At the moment the Spam Society is very benign. However, once this infrastructure and data is in place, it can be hard to remove it or to escape its reach, or to prevent it from transforming into a Big Brother Society. Even if one were to vanish as the target of the data collection today, the previously obtained information would still contain a lot of data that could be misused.

For example, what can happen if a political party with a violent agenda takes power, one way or another? If your profile indicates you have been thinking wrong thoughts, instead of getting advertisements, you would get night-time visitors taking you for a long car ride that culminates in a neck-shot in the woods. Interestingly enough, there is prior art in this kind of horror scenario: the Nazi government used census data which they data mined with IBM's help to weed out people with Jewish ancestry.

As for the current state of ubiquitous computing devices, the smart phone stands as a lone king. It helps people organize their lives, entertains them, helps them keep connected with others, helps them document their lives with photographs and videos, and so on.

Although not quite as invisible as Weiser envisioned it, for those who have one, the smart phone is always present, ready to serve - and with modern UIs, it tries to not get in the way too much. I'd say at the moment the smart phone is closest to Weiser's vision of calm technology. Also, over time, the smart phone has gotten only better and I expect this trend to continue.

Generally, a big downside I see with all current smart phones is the level of trust that needs to be placed on the maintainers and owners of the smartphone ecosystem to not abuse the data they collect (the location data, contact data, calendar data, etc.).

For example, Google backs up your WLAN passwords if you enable the Backup My Data option. It's convenient in case you lose your phone, but do you know who in the end has access to the data and what they do with it? If you disable the option, the data is said to be removed. Fine; now, how will you know this to be true? You can't know this, there is no way to check, so you just have to have trust. There are technical ways to remove the reliance on trust (e.g. encrypt the backup locally with a user-given key and then upload it), but at the moment such techniques are not used.

That said, I am a happy user of an Android smart phone. Android is open enough and the phone hardware it runs on is documented enough to let a community of enthusiasts make their own aftermarket firmware. Therefore, if I ever become unhappy with the stock Android, I can always install Cyanogenmod.