A Libre Software Solution for Public Computers

Introduction

At places such as libraries or internet cafés, you will often find computers that are open to the public. These computers are supplied with a set of pre-installed applications, and user data are cleared after reboot.

The software on such machines is often proprietary. However, there are also freedom-respecting solutions for public computers. As free-software activists, we should actively apply and promote these solutions among libraries, internet cafés, schools, etc.

In this article I will demonstrate a solution that sets up a computer running Debian GNU/Linux which clears all data after reboot.

Licence

All code in this article is dedicated to the public domain. Other parts of this article are under CC BY-SA 4.0.

Preparation

A computer with a hard disk (preferably an HDD) of at least 50 GiB, and a Debian GNU/Linux installation medium (not Debian Live).

Step 1: Install Debian GNU/Linux

Install Debian GNU/Linux on your computer with the following partition layout:

  • 1 GiB /dev/sda1 with a FAT filesystem, mounted at /boot/efi
  • 1 GiB /dev/sda2 with an ext4 filesystem, mounted at /boot
  • At least 40 GiB for /dev/sda3 with an ext4 filesystem, mounted at /
  • /dev/sda4 with no filesystem
  • /dev/sda5 as the swap partition

After installation, install the desired software and configure the system as you wish. Remember to set a strong root password (and a strong BIOS/UEFI password); leaving the root account open to the public is not a good idea.

Step 2: Configuration

Format /dev/sda4 with an ext4 filesystem and label it OVERLAY_RW:

# mkfs.ext4 -L OVERLAY_RW /dev/sda4

Then label /dev/sda3 as ROOT_TEMPLATE:

# e2label /dev/sda3 ROOT_TEMPLATE

Create directories:

# mkdir -p /mnt/overlay_prepare
# mount /dev/disk/by-label/OVERLAY_RW /mnt/overlay_prepare
# mkdir -p /mnt/overlay_prepare/upper-root
# mkdir -p /mnt/overlay_prepare/work-root
# chmod 0700 /mnt/overlay_prepare
# sync
# umount /mnt/overlay_prepare

Comment out the line that mounts your root filesystem in /etc/fstab, and then add this line:

LABEL=OVERLAY_RW /overlay_storage ext4 defaults,noatime 0 2

Refresh your package cache and install these packages:

# apt update
# apt install --no-install-recommends initramfs-tools busybox-static

Add these lines to /etc/initramfs-tools/modules:

overlay
ext4
jbd2
crc32c
mbcache

Create /etc/tmpfiles.d/overlay-runtime.conf with these lines:

# /etc/tmpfiles.d/overlay-runtime.conf
d /run/dbus 0755 messagebus messagebus -
d /var/lib/dbus 0755 messagebus messagebus -
d /run/NetworkManager 0755 root root -
d /run/lock 0755 root root -
d /var/log 0755 root root -
d /tmp 1777 root root -
d /var/tmp 1777 root root -

Create /etc/initramfs-tools/scripts/init-bottom/overlayroot and add the following script:

#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in
  prereqs) prereqs; exit 0;;
esac
set -eu

# If admin wants maintenance: skip overlay and let normal boot continue
if grep -q 'overlay=disabled' /proc/cmdline 2>/dev/null; then
  echo "overlay disabled via kernel cmdline" >/dev/console 2>&1
  exit 0
fi

# Try to load overlay module (best-effort)
modprobe overlay 2>/dev/null || true

# prepare mount points in initramfs
mkdir -p /overlay/lower /overlay/storage /overlay/overlay_root

# resolve device by label for the template root
DEV_LABEL=/dev/disk/by-label/ROOT_TEMPLATE
DEV_ROOT=$(readlink -f "$DEV_LABEL" 2>/dev/null || true)
if [ -z "$DEV_ROOT" ]; then
  echo "overlayroot: ERROR: ROOT_TEMPLATE label not found" >/dev/console 2>&1
  exit 1
fi
echo "overlayroot: resolved ROOT_TEMPLATE -> $DEV_ROOT" >/dev/console 2>&1

# find existing mount point for the device; fallback to using current root (/)
MOUNT_POINT=$(awk -v d="$DEV_ROOT" '($1==d){print $2; exit}' /proc/self/mounts || true)
if [ -z "$MOUNT_POINT" ]; then
  B=$(basename "$DEV_ROOT" || true)
  if [ -r "/sys/class/block/$B/dev" ]; then
    DEVNUM=$(cat /sys/class/block/$B/dev)
    MOUNT_POINT=$(awk -v num="$DEVNUM" '($3==num){print $5; exit}' /proc/self/mountinfo || true)
  fi
fi

if [ -z "$MOUNT_POINT" ]; then
  ROOT_SRC=$(awk '($2=="/"){print $1; exit}' /proc/self/mounts || true)
  if [ -n "$ROOT_SRC" ]; then
    echo "overlayroot: fall back to using current root mount ($ROOT_SRC) as lowerdir" >/dev/console 2>&1
    mount --bind / /overlay/lower 2>/dev/null || true
    MOUNT_POINT=/overlay/lower
  fi
fi

if [ -n "$MOUNT_POINT" ] && [ "$MOUNT_POINT" != "/overlay/lower" ]; then
  echo "overlayroot: bind existing mountpoint $MOUNT_POINT -> /overlay/lower" >/dev/console 2>&1
  mount --bind "$MOUNT_POINT" /overlay/lower || true
fi

# if /overlay/lower still not a mountpoint, try direct ro mount (last resort)
if ! grep -q ' /overlay/lower ' /proc/self/mounts; then
  echo "overlayroot: trying direct mount of $DEV_ROOT -> /overlay/lower" >/dev/console 2>&1
  mount -o ro "$DEV_ROOT" /overlay/lower || {
    echo "overlayroot: direct mount of $DEV_ROOT failed" >/dev/console 2>&1
    exit 1
  }
fi

# mount overlay storage partition (explicit ext4)
DEV_RW=$(readlink -f /dev/disk/by-label/OVERLAY_RW 2>/dev/null || true)
if [ -z "$DEV_RW" ]; then
  echo "overlayroot: ERROR: OVERLAY_RW label not found" >/dev/console 2>&1
  exit 1
fi
echo "overlayroot: mounting OVERLAY_RW ($DEV_RW) -> /overlay/storage" >/dev/console 2>&1
mount -t ext4 "$DEV_RW" /overlay/storage || {
  echo "overlayroot: failed to mount OVERLAY_RW ($DEV_RW)" >/dev/console 2>&1
  exit 1
}

# per-boot upper/work on overlay storage
BOOTID=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || date +%s)
UPPER="/overlay/storage/upper-${BOOTID}"
WORK="/overlay/storage/work-${BOOTID}"
mkdir -p "$UPPER" "$WORK"
# make sure upper/work are owned by root and inaccessible to others
chown -R root:root "$UPPER" "$WORK" 2>/dev/null || true
chmod 0700 "$UPPER" "$WORK" 2>/dev/null || true

# mount overlay onto the location that initramfs-tools expects for the real root (/root)
mkdir -p /root
echo "overlayroot: mounting overlay lower=/overlay/lower upper=$UPPER work=$WORK -> /root" >/dev/console 2>&1
mount -t overlay overlay -o lowerdir=/overlay/lower,upperdir="$UPPER",workdir="$WORK" /root || {
  echo "overlayroot: overlay mount failed" >/dev/console 2>&1
  exit 1
}

# move overlay storage under the real root so the booted system can access/clean it later
mkdir -p /root/overlay_storage
mount --move /overlay/storage /root/overlay_storage 2>/dev/null || mount --bind /overlay/storage /root/overlay_storage || true

# --- Post-mount safety & runtime skeleton preparation ---
# Ensure the merged root is traversable by non-root users to allow services to chdir/exec
chmod 0755 /root 2>/dev/null || true

# Create essential runtime and state directories inside the merged root.
# These make sure systemd and daemons (dbus, NetworkManager, etc.) can create sockets and pidfiles.
mkdir -p /root/run /root/run/dbus /root/run/NetworkManager /root/run/lock
mkdir -p /root/var/lib/dbus /root/var/log /root/var/tmp /root/tmp

# Set permissive permissions for tmp directories and standard perms for others
chmod 0755 /root/run /root/var /root/var/log 2>/dev/null || true
chmod 1777 /root/tmp /root/var/tmp 2>/dev/null || true

# Attempt to set dbus ownership for dbus runtime dirs; ignore errors if the user does not exist in initramfs.
chown -R messagebus:messagebus /root/run/dbus /root/var/lib/dbus 2>/dev/null || true

# Ensure root owns the primary runtime dirs
chown root:root /root /root/run /root/var 2>/dev/null || true

# Ensure upper/work are secure on the merged root as well (in case overlay moved them)
# (this is best-effort; ignore failures)
[ -d "$UPPER" ] && chown -R root:root "$UPPER" 2>/dev/null || true
[ -d "$WORK" ] && chown -R root:root "$WORK" 2>/dev/null || true
chmod 0700 "$UPPER" "$WORK" 2>/dev/null || true

# Move pseudo-filesystems into the new root so the real init finds them after switch_root.
# These moves are best-effort; if they fail, systemd may still handle necessary mounts.
for P in dev proc sys run; do
  if mountpoint -q "/$P" 2>/dev/null; then
    mkdir -p /root/$P 2>/dev/null || true
    mount --move "/$P" "/root/$P" 2>/dev/null || true
  fi
done

# Leave final switch_root to initramfs /init (do not exec switch_root here).
echo "overlayroot: overlay prepared at /root; returning to initramfs /init to perform switch_root" >/dev/console 2>&1
exit 0

Make /etc/initramfs-tools/scripts/init-bottom/overlayroot executable:

# chmod +x /etc/initramfs-tools/scripts/init-bottom/overlayroot

Refresh your initramfs:

# update-initramfs -u -k all

Create /usr/local/sbin/overlay-prune.sh and add these lines:

#!/usr/bin/env bash
# overlay-prune.sh
# Prune unused overlay upper-*/work-* directories safely.
# Usage:
#   /usr/local/sbin/overlay-prune.sh [--dry-run] [--age DAYS]
# Default AGE = 7 days

set -euo pipefail

DRY_RUN=0
AGE=7   # days
LOG="/var/log/overlay-prune.log"

while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) DRY_RUN=1; shift ;;
    --age) AGE="$2"; shift 2 ;;
    --help) echo "Usage: $0 [--dry-run] [--age DAYS]"; exit 0 ;;
    *) echo "Unknown arg: $1"; exit 2 ;;
  esac
done

now() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }

log() {
  printf '%s %s\n' "$(now)" "$*" >> "$LOG"
}

# Determine overlay storage mountpoint(s).
# Try common locations then fallback to finding mount points for device labelled OVERLAY_RW.
CANDIDATES=(
  "/root/overlay_storage"
  "/overlay_storage"
  "/overlay/merged/overlay_storage"
  "/overlay/storage"
)

STORAGE=""
for p in "${CANDIDATES[@]}"; do
  if [ -d "$p" ]; then
    # choose the first existing one that is on a separate filesystem or contains upper-* dirs
    if find "$p" -maxdepth 1 -mindepth 1 -type d -name 'upper-*' -print -quit >/dev/null 2>&1; then
      STORAGE="$p"
      break
    fi
  fi
done

# fallback: try findmnt by device label
if [ -z "$STORAGE" ]; then
  DEV=$(readlink -f /dev/disk/by-label/OVERLAY_RW 2>/dev/null || true)
  if [ -n "$DEV" ]; then
    STORAGE=$(findmnt -n -o TARGET -S "$DEV" 2>/dev/null || true)
  fi
fi

if [ -z "$STORAGE" ]; then
  echo "overlay-prune: no overlay storage found; exiting" >&2
  log "No overlay storage found; abort."
  exit 0
fi

log "Starting prune on storage: $STORAGE (age > ${AGE}d) DRY_RUN=${DRY_RUN}"

# Collect currently in-use upper/work directories by scanning /proc/mounts overlay options
mapfile -t INUSE < <(awk -F',' '/lowerdir=/{for(i=1;i<=NF;i++){if($i ~ /^upperdir=/) print substr($i,10); if($i ~ /^workdir=/) print substr($i,9)}}' /proc/mounts | sort -u)

# Helper: check if path is referenced in INUSE
is_inuse() {
  local p="$1"
  for u in "${INUSE[@]}"; do
    if [ "$u" = "$p" ]; then
      return 0
    fi
  done
  return 1
}

# Find candidate dirs named upper-* or work-*
while IFS= read -r d; do
  # normalize
  dir="$d"
  # guard: only operate under STORAGE
  case "$dir" in
    "$STORAGE"/*) ;;
    *) continue ;;
  esac

  # skip if currently in use
  if is_inuse "$dir"; then
    log "SKIP in-use: $dir"
    continue
  fi

  # skip if younger than AGE
  if [ "$(find "$dir" -maxdepth 0 -mtime -"$AGE" -print -quit)" ]; then
    log "SKIP recent: $dir"
    continue
  fi

  if [ "$DRY_RUN" -eq 1 ]; then
    echo "DRY-RUN would remove: $dir"
    log "DRY-RUN would remove: $dir"
  else
    # double-check no mount points below it
    if mountpoint -q "$dir"; then
      log "SKIP mounted: $dir"
      continue
    fi
    # safe remove
    log "REMOVING: $dir"
    rm -rf -- "$dir"
    if [ $? -eq 0 ]; then
      log "REMOVED: $dir"
    else
      log "FAILED_REMOVE: $dir"
    fi
  fi
done < <(find "$STORAGE" -maxdepth 1 -type d \( -name 'upper-*' -o -name 'work-*' \) -print | sort)

log "Prune finished."
exit 0

Make /usr/local/sbin/overlay-prune.sh executable:

# chmod +x /usr/local/sbin/overlay-prune.sh

Create /etc/logrotate.d/overlay-prune with these lines:

/var/log/overlay-prune.log {
    rotate 7
    daily
    missingok
    notifempty
    compress
    copytruncate
}

Create the log file and set its ownership and permissions:

# touch /var/log/overlay-prune.log
# chown root:root /var/log/overlay-prune.log
# chmod 0640 /var/log/overlay-prune.log

Create /etc/systemd/system/overlay-prune.service with these lines:

[Unit]
Description=Prune unused overlay upper/work directories
After=local-fs.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/overlay-prune.sh --age 7
Nice=10
# Run as root (needs to remove files)

Create /etc/systemd/system/overlay-prune.timer with these lines:

[Unit]
Description=Run overlay-prune daily (and shortly after boot)

[Timer]
OnBootSec=10min
OnUnitActiveSec=24h
Persistent=true

[Install]
WantedBy=timers.target

Enable the timer:

# systemctl enable overlay-prune.timer

Now reboot your system:

# reboot

Step 3: Test your installation

After rebooting, run:

$ findmnt /

You should see that / is an overlay filesystem.

Create several files in different locations, then reboot the system; these files should have disappeared.

If everything is OK, you’re done.