Contained, emacsified browsing

Table of Contents

1 Introduction

This document describes my Internet-browser setup. It is centered around Fedora Linux, GNU Emacs and Stump Window Manager, but most of the concepts should apply also to other configurations. For example, the SELinux-based sandboxing I'm using could be changed to another application sandboxing setup (for example Firejail which is based on Linux kernel system call filtering seccomp) without changes to overall concept, which is to run Internet-browser in a locked-down environment while maintaining a decent level of usability.

So this setup won't give me absolute privacy (for that I'd need to plug off the Internet), but it is a decent compromise between security and usability, and most important for me, it provides a good performance and familiar user interface. Over the course of years I've tried several things, including running several separate machines (virtual and physical), but those practices never stick, which make them in effect non-secure solutions. Security that doesn't stick, is no security. I had to find a way that sticks, and this container-based setup seems to be quite sticky.

HTML theme is modified readtheorg theme. I just changed the code block colors to match my Emacs theme 'Wombat'.

2 Running containers

2.1 Starting containers

I run several different containers for several different functions, such Internet browsing, online banking etc. In general, I run all non-trusted binaries in the containers. They are all started in a similar fashion, using a very simple shell script (on-liner basically). For example, here's how I start my Chrome-container:

#!/bin/bash

sandbox -X -w 1600x1200 -W /usr/local/bin/stumpwm -H $HOME/chrome_home \
    -T $HOME/chrome_tmp -t sandbox_web_t google-chrome --no-sandbox $START_PAGE

What's important to note down here is that the container runs its own X-server and Window manager. I'm using the same WM in container as in the host (Stumpwm), but in the containers the WM configuration is very simple, like this:

;; -*-lisp-*-
;;
;; Here is a sample .stumpwmrc file

(in-package :stumpwm)

;; change the prefix key to something else
(set-prefix-key (kbd "C-t"))

;; Cursor
(run-shell-command "xsetroot -cursor_name left_ptr -solid black -name root-window")

(run-shell-command "setxkbmap fi")

;; Some other basic settings
(setf *timeout-wait* 10)
(setf *message-window-gravity* :center)
(setf *input-window-gravity* :center)

(setf *mode-line-background-color* "red")
(setf *mode-line-foreground-color* "black")
(setf *screen-mode-line-format*
      (list "[%n] %W"))

;; Mark the Default group as "unsafe" with border color:
(set-border-color "red")
(set-msg-border-width 3)

(mode-line)

When the container starts, there are clear visual clues (red color in the modeline in this case) for me to distinguish the container from an ordinary frame.

The inferior X-server (Xephyr) adjusts itself actually to the frame size, so the resolution (1600x1200) makes no sense in manually tiled window manager like the Stumpwm I'm using. But after startup, Xephyr doesn't refresh the resolution dynamically, which is a bit counter-intuitive, but useful at the same time. In manually tiled desktop, that's exactly what I want, and what's more, I can make the browser full-screen inside the WM frame with <F11> to get extra space to the actual content. Running browser like Chrome in full screen is also good for the WebExtensions-based addons, such as Vimium, because they work by hijacking the current page. The moment I click for example browser URL bar, Vimium loses control. If such things are not even visible, I'm less tempted to use them.

Maybe someone is now thinking about why I run the Chrome inside SELinux-container, with --no-sandbox. That may actually make the browser less secure, who knows. I'm running all the browsers in a same fashion, Firefox, Conkeror, Qutebrowser, etc. Some of them have built-in sandboxing, some do not. What I've found out is that in general these built-in sandboxes do not play along with the SELinux sandbox, which is I have do disable the browser sandbox, if I want to run the browser in container.

2.2 Container setup

Setting up container, such as the Chrome-container above, is very simple. I just have to make sure that directories with the container name appended with _home and _tmp exist in $HOME.

2.3 Clipboard handling

Because the containers have their own private X-servers, they don't share the clipboard with the main X-server. This is good for privacy, but as always, bad for usability. Usually fully-featured VM's, like VirtualBox solve this usability problem by running special agents inside the VM guests, but in the case of SELinux container, I don't need to go that deep. Basically, the main system can access the inferior X-servers freely with utilities such as xsel. SELinux sandbox-application writes the DISPLAY-variable conveniently to container $HOME/seremote, which is going to be very useful.

Container clipboards are controlled from the main system with couple of short shell scripts that depend on the file structures set up earlier when establishing containers. These scripts can be used from command line, or from Emacs or Stumpwm. I have these script in by $HOME/bin, named as set_clip.sh, get_clip.sh and clip.sh, respectively.

Set container clipboard (set_clip.sh):

#!/bin/bash

# Get clipboard from container

if [ ! -e ~/$1_home/seremote ]; then
    echo "$1: no such guest"
    exit 1
fi

screen=`grep DISPLAY ~/$1_home/seremote | sed 's/DISPLAY=\(:.\) .*/\1/g'`
xsel_out="xsel --display $screen -b -o"

# Emacs gets confused about piping to xsel:
if [ $# -gt 1 ]; then
    $xsel_out
else
    $xsel_out | xsel -b -i
fi

exit 0

Get container clipboard (get_clip.sh):

#!/bin/sh

# Set clipboard of container and clear it after timeout
# Timeout 0 means no clearing
# NOTE: clearing is very important in case of passwords!!

if [ ! -e ~/$1_home/seremote ]; then
    echo "$1: no such guest"
    exit 1
fi

out=`mktemp`
screen=`grep DISPLAY ~/$1_home/seremote | sed 's/DISPLAY=\(:.\) .*/\1/g'`

# Set clipboard
xsel -b -o | tee $out | xsel --display $screen -b -i

# Clear clipboard (default=10s)
timeout=30
if [ ! -z "$2" ]; then
    timeout=$2
fi
if [ $timeout -ne 0 ]; then
    sleep $timeout
    echo -n | xsel --display $screen -b -i
fi

rm $out

Copy container clipboard to another container (clip.sh):

#!/bin/sh

# Backup current clipboard
xsel -b -o > /tmp/.xsel.current

# Get clipboard of source container
get_clip.sh $1

# Set clipboard of target container with timeout 0 -> no clear
set_clip.sh $2 0

# Restore current clipboard
cat /tmp/.xsel.current | xsel -b -i

With these scripts, moving data in and out of containers is very easy, but always a manual procedure, which is exactly the way I want it to be. So whenever I want to set some data to some container's clipboard, I need to decide which container. Some of the most commonly used clipboard operations I have automated in the Emacs and Stumpwm.

3 Emacs-settings

I use Emacs, a lot, and quite many of those things call for a good browser integration (for example, reading the full article of elfeed entry). Instead of just clicking or pressing ENTER, I have to do a bit more complicated things in order to reach the browser inside container. I also try to use the Emacs in editing input fields, when ever possible.

3.1 Clipboard handling

Setting/getting the clipboard in net container is mapped conveniently to <F6> and <F5>:

;; Send region to clipboard of the net container:
(defun set-clip-net ()
  (interactive)
  (clipboard-kill-ring-save (region-beginning) (region-end))
  (start-process "set-clip" "maintenance" "~/bin/set_clip.sh" "chrome" "0"))
(global-set-key (kbd "<f6>") 'set-clip-net)

;; Get clipboard from net and paste to current point:
(defun get-clip-net ()
  (interactive)
  (call-process-shell-command "~/bin/get_clip.sh chrome t" nil t nil))
(global-set-key (kbd "<f5>") 'get-clip-net)

These functions utilize shell scripts set_clip.sh and get_clip.sh defined earlier in the section Sandboxing. The output is sent to buffer maintenance.

Few more things to note: set-clip-net uses timeout 0, so it returns immediately. This means there are no extra scripts hanging around in the background, and also that the clipboard is not cleared by set_clip.sh. If I need to send some sensitive information to the browser (for example password), I do it manually from Window manager (see the section below). If I ever want to do that also from Emacs, I can just put some non-zero argument, or remove it (default is 30 seconds).

3.2 Sending URLs to container

It is happening very commonly that I need to send some URLs over to container from Emacs buffer, for example from emails or elfeed. This function can handle many such cases with a single button (F9):

;; Copy url from link and send to net
(require 'url-util)
(defun set-clip-net-shr ()
  (interactive)
  (let ((url (shr-url-at-point current-prefix-arg)))
    (if (not url)
        (setq url (url-get-url-at-point)))
    (if url
        (progn
          (kill-new url)
          (start-process "set-clip" "maintenance" "~/bin/set_clip.sh" "chrome" "0"))
      (message "No url at point!"))))
(global-set-key (kbd "<f9>") 'set-clip-net-shr)

This overrides opening of URLs:

;; Never open URL, instead try to copy it to container
(advice-add 'browse-url-default-browser :override
            (lambda (url &rest args)
                (progn
                  (kill-new url)
                  (start-process "set-clip" "maintenance" "~/bin/set_clip.sh" "chrome" "0"))))

Very useful when for example opening links in org-mode (or just press ENTER in my case). I don't want to open any URL on the host system directly.

3.3 Edit server

For convenient input-field editing, I run edit-server from ELPA. The server is started in port 8888, because that's one of the allowed "web" ports in the SELinux container profile.

;; Edit server for browsers
(require 'edit-server)
(setq edit-server-port 8888)
(setq edit-server-new-frame t)
(add-to-list 'edit-server-url-major-mode-alist
             '("^reddit.com" . markdown-mode))
(edit-server-start)

4 Browser settings

Settings described here apply to both Chrome and Firefox. Vimium used to be more stable on Chrome, but I'm not so sure about it anymore. I'm still using Chrome more in daily browsing, mainly because it supports all sorts of media out-of-the-box.

4.1 gtk-3.0 keys

I have this in the container's $HOME/.config/gtk-3.0/settings.ini:

[Settings]
gtk-key-theme-name = Emacs

4.2 Vimium

The Vimium configuration models browser tabs as Emacs buffers. So familiar key combinations, such as C-x C-f, C-x k, C-x C-b are doing more or less similar things. It also tries to map things like hinting to Conkeror-style keys, so for example f and F work the same way. I took the basic configuration from here, and then just removed something I don't use, and modified something here and there.

unmapAll # Use Emacs-style bindings only.

# Emacs `(next|previous)-line`.
map <c-n> scrollDown
map <c-p> scrollUp

# Emacs `(backward|forward)-char`.
map <c-b> scrollLeft
map <c-f> scrollRight

# Emacs `(beginning|end)-of-buffer`.
map <a-<> scrollToTop
map <a->> scrollToBottom

# Emacs `scroll-(left|right)`.
map <c-x>< scrollToLeft
map <c-x>> scrollToRight

# Emacs `scroll-(down|up)-command`.
map <a-v> scrollFullPageUp
map <c-v> scrollFullPageDown

# Emacs `scroll-(up|down)-line`
# Similar to Emacs `(backward|forward)-paragraph`.
map <a-{> scrollPageUp
map <a-}> scrollPageDown

# Emacs `find-alternate-file`.
map <c-x><c-v> reload

# Not implemented in Emacs.
map <a-s> toggleViewSource

# Emacs `kill-buffer`.
map <c-x>k removeTab

# Similar to Emacs 'A-w'
map w copyCurrentUrl

# Quick paste URL from clipboard (handy with Emacs function 'set-clip-net-shr / F9)
map <c-x>y openCopiedUrlInCurrentTab
map <c-x>Y openCopiedUrlInNewTab

# These work pretty much similar to Conkeror
map d LinkHints.activateModeToDownloadLink
map k LinkHints.activateModeToCopyLinkUrl
map f LinkHints.activateMode
map F LinkHints.activateModeToOpenInNewForegroundTab

# Emacs `find-file`.
map <c-x><c-f> Vomnibar.activateInNewTab

# Go to page on current tab
map g Vomnibar.activate
map G Vomnibar.activateEditUrl

# Similar to Emacs buffer listing
map <c-x>b Vomnibar.activateTabSelection
map <c-x><c-b> Vomnibar.activateTabSelection

# Similar to Emacs `other-frame`.
# Also similar to Emacs `other-window`.
map <c-x>o nextFrame

# Search (I actually override these with Stumpwm)
map <c-s> enterFindMode
map s performFind
map <c-r> performBackwardsFind

# Again, like Conkeror
# Usually I use the Chrome global Alt-Left, Alt-Right, because they work on all
# pages and are quite logical: Ctrl+X cycles buffers (tabs), Alt cycles pages.
map c goBack
map v goForward

# Similar to Emacs `(previous|next)-buffer`.
map <c-x><left> previousTab
map <c-x><right> nextTab
map <c-x><c-left> previousTab
map <c-x><c-right> nextTab

# Not implemented in Emacs.
map <c-x><up> firstTab
map <c-x><down> lastTab

# Similar to `clone-indirect-buffer-other-window`.
map <c-x>4 duplicateTab

# Similar to `make-frame-command`.
map <c-x>5 moveTabToNewWindow

# Rearrange tabs
map <a-B> moveTabLeft
map <a-F> moveTabRight

# Similar to Emacs help commands.
map <c-h> showHelp

One very notable limitation of my setup is that I cannot use any key combination that has Ctrl and Shift, because it will lock the input in Xephyr. This is hard-coded, so recompiling Xephyr would be needed in order to change that. I'd rather avoid that particular key combination.

Some of the Vimium-configurations are also a bit redundant in my environment. For example the search (C-s and C-r) work in more consistent way if overridden from the WM (see key re-mapping below).

4.3 Edit with Emacs

Edit with Emacs is the companion add-on for the edit-server Emacs-module. I just configured the port 8888, and that's about it.

4.4 uMatrix

If I had to choose one add-on to rule them all, it would be uMatrix. This excellent add-on enables me to choose which resources the browser is allowed to access in a fine-grained fashion. Best way to think of it is a firewall for web browser. Not traditional firewall that is only concerned of network-level resources, but higher-level firewall that is aware of web-level resources, like scripts, images etc.

The default settings are quite ok, but since the uMatrix will break by default many sites anyway, I decided to clean up the table and learn how to use it. So my default settings deny everything, and then I just train the uMatrix which sites and which resources on these sites I really need. If you are like me, it won't take too much time to train it. For me it is a matter of few weeks. After the training period, it is very rare occasion I need to step down from my full-screen mode (<F11>) to tweak some web page. Most of the time I just skip pages that won't work.

5 Window manager settings

Last piece in the puzzle is the Window manager Stumpwm, which I use to do things like key remapping. Stumpwm is written in Common Lisp (SBCL), so it is very similar to Emacs in how it works. When I'm hacking on the WM, I just open the .stumpwmrc in Emacs, enable stumpwm-mode (available in ELPA), and use the stumpish (C-x C-e) to send code in the WM. It's very nice, almost like elisp.

Here are couple of things related to my browser configuration:

5.1 Clipboard

(defun copy-clip (from to)
  "Copy clipboard from sandbox to another."
  (cond
    ((string-equal "Default" to)
     (run-shell-command (format nil "get_clip.sh ~a" from)))
    ((string-equal "Default" from)
     (run-shell-command (format nil "set_clip.sh ~a" to)))
    (t
     (run-shell-command (format nil "clip.sh ~a ~a" from to)))))

(defcommand get-clip (wfrom) ((:string "Where from: "))
            (let ((from wfrom)(to "Default"))
              (if (string-equal from "")
                  (setf from "chrome"))
              (copy-clip from to)))

(defcommand set-clip (wto) ((:string "Where to: "))
            (let ((to wto)(from "Default"))
              (if (string-equal to "")
                  (setf to "chrome"))
              (copy-clip from to)))

(defcommand set-clip-net () ()
            (copy-clip "Default" "chrome"))
(defcommand get-clip-net () ()
            (copy-clip "chrome" "Default"))

(define-key *root-map* (kbd "F5") "get-clip-net")
(define-key *root-map* (kbd "F6") "set-clip-net")

These functions can be accessed anywhere conveniently with C-:. Default container is again here chrome. I can also use shortcuts C-F5 and C-F6 for accessing my browser of the day (chrome in this case), much like Emacs configuration above. The functions are handy for applications outside Emacs, for example sending password from KeepassX to browser.

5.2 Key re-mapping

Most of the Emacs-key-mapping is done inside the Vimium, but is somewhat problematic. It is injected to the web pages inline, which means it can access only parts of the browser environment (to be fair, most of it, if run full-screen). Vimium has no access to certain hard-coded web pages, like the settings pages, and also it doesn't have access to "low-level" functionality, such as clipboard (the familiar Ctrl+C, Ctrl+V and friends). Quite often it happens, that the Vimium loses control, so here comes some handy Window manager keyboard re-mappings to rescue:

;;
;; Key remapping
;; Emacs copy/paste and search and other things for Xephyr (at least Chrome)
;; Undo/redo / ctrl-z/ctrl-y is a problem:
;;   1. C-z is Stumpwm prefix. Could be changed, but to what?
;;   2. Ctrl + Shift causes problems in Xephyr. It's hardcoded.
;;   3. Cannot remap multiple keys, like C-x u or C-x k
;;   4. Redo (Ctrl-Y) is bound to yank, which is more relevant
;;
;;   Solution: M-u sends C-z, and inferior Stumpwm uses prefix C-t. No redo.
;;
(define-remapped-keys '(("Xephyr"
                         ("C-s" . "F3")    ; Search 
                         ("C-r" . "C-G")   ; Search backwards
                         ("M-k" . "C-F4")  ; Hard kill, if C-x k doesn't work..                  
                         ("M-u" . "C-z")   ; Undo
                         ("M-w" . "C-c")   ; Copy, paste, cut
                         ("C-w" . "C-x")
                         ("C-y" . "C-v"))))

Author: Jarkko Turkulainen

Created: 2018-08-27 Mon 03:21

Validate