A Libre Software Solution for Public Computers

2025-10-12T18:21:32+08:00

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:

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.