面向公共计算机的自由软件解决方案

介绍

在图书馆或网吧等场所,经常会有向公众开放的计算机。这些计算机通常预装一套应用程序,且用户数据会在重启后被清除。

此类机器上的软件通常是专有软件。但也存在尊重软件自由的公共计算机解决方案。作为自由软件活动者,我们应在图书馆、网吧、学校等场所积极应用并推广这些解决方案。

在本文中,我将展示一种在 Debian GNU/Linux 上设置计算机的方法,该方法能在重启后清除所有数据。

许可

本文中的所有代码均献给公有领域。本文的其他部分采用 CC BY-SA 4.0 许可。

准备工作

一台至少 50 GiB 的硬盘(最好为 HDD)的计算机,以及一份 Debian GNU/Linux 安装介质(非 Debian Live)。

步骤 1:安装 Debian GNU/Linux

按下列分区布局在您的计算机上安装 Debian GNU/Linux:

  • 1 GiB 的 /dev/sda1,使用 FAT 文件系统,挂载点为 /boot/efi
  • 1 GiB 的 /dev/sda2,使用 ext4 文件系统,挂载点为 /boot
  • 至少 40 GiB 的 /dev/sda3,使用 ext4 文件系统,挂载点为 /
  • /dev/sda4 无文件系统
  • /dev/sda5 作为交换分区(swap)

安装完成后,安装所需软件并按您需要配置系统。记得设定强壮的 root 密码(以及强壮的 BIOS/UEFI 密码);将 root 账户对公众开放并不是一个好主意。

步骤 2:配置

/dev/sda4 格式化为 ext4 文件系统并标记为 OVERLAY_RW

# mkfs.ext4 -L OVERLAY_RW /dev/sda4

然后将 /dev/sda3 标记为 ROOT_TEMPLATE

# e2label /dev/sda3 ROOT_TEMPLATE

创建目录:

# 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

/etc/fstab 中注释掉挂载根文件系统的那一行,然后添加以下这一行:

LABEL=OVERLAY_RW /overlay_storage ext4 defaults,noatime 0 2

刷新软件包缓存并安装这些包(这可能会移除您的 busybox;请谨慎操作):

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

将下面几行添加到 /etc/initramfs-tools/modules

overlay
ext4
jbd2
crc32c
mbcache

创建 /etc/tmpfiles.d/overlay-runtime.conf,内容如下:

# /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 -

创建 /etc/initramfs-tools/scripts/init-bottom/overlayroot 并添加下面的脚本(代码块保持原样):

#!/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

/etc/initramfs-tools/scripts/init-bottom/overlayroot 设为可执行:

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

刷新 initramfs:

# update-initramfs -u -k all

创建 /usr/local/sbin/overlay-prune.sh 并添加以下内容(代码块保持原样):

#!/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

将脚本设为可执行:

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

创建 /etc/logrotate.d/overlay-prune,内容如下:

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

创建日志文件并设置其所有权和权限:

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

创建 /etc/systemd/system/overlay-prune.service,内容如下:

[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)

创建 /etc/systemd/system/overlay-prune.timer,内容如下:

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

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

[Install]
WantedBy=timers.target

启用定时器:

# systemctl enable overlay-prune.timer

现在重启系统:

# reboot

步骤 3:测试安装

重启后,执行:

$ findmnt /

您应能看到 / 是一个 overlay 文件系统。

在不同位置创建若干文件,然后重启系统;这些文件应已消失。

如果一切正常,您已完成设置。