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:
- 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.