Une solution logicielle libre pour des ordinateurs publics

Introduction

Dans des lieux tels que les bibliothèques ou les cafés Internet, on trouve souvent des ordinateurs mis à la disposition du public. Ces ordinateurs sont fournis avec un ensemble d’applications préinstallées, et les données des utilisateurs sont effacées après un redémarrage.

Les logiciels sur ces machines sont souvent propriétaires. Cependant, il existe aussi des solutions respectueuses de la liberté logicielle pour les ordinateurs publics. En tant qu’activistes du logiciel libre, nous devrions appliquer et promouvoir activement ces solutions dans les bibliothèques, les cafés Internet, les établissements scolaires, etc.

Dans cet article, je présenterai une solution qui installe Debian GNU/Linux sur un ordinateur et efface toutes les données après un redémarrage.

Licence

Tout le code de cet article est dédié au domaine public. Les autres parties de cet article sont sous licence CC BY-SA 4.0.

Préparation

Un ordinateur avec un disque dur (de préférence un HDD) d’au moins 50 GiB, et un média d’installation Debian GNU/Linux (pas Debian Live).

Étape 1 : Installer Debian GNU/Linux

Installez Debian GNU/Linux sur votre ordinateur avec la disposition de partitions suivante :

  • 1 GiB /dev/sda1 avec un système de fichiers FAT, monté sur /boot/efi
  • 1 GiB /dev/sda2 avec un système de fichiers ext4, monté sur /boot
  • Au moins 40 GiB pour /dev/sda3 avec un système de fichiers ext4, monté sur /
  • /dev/sda4 sans système de fichiers
  • /dev/sda5 comme partition swap

Après l’installation, installez les logiciels souhaités et configurez le système selon vos préférences. N’oubliez pas de définir un mot de passe root solide (ainsi qu’un mot de passe BIOS/UEFI robuste) ; laisser le compte root accessible au public n’est pas une bonne idée.

Étape 2 : Configuration

Formatez /dev/sda4 avec un système de fichiers ext4 et étiquetez-le OVERLAY_RW :

# mkfs.ext4 -L OVERLAY_RW /dev/sda4

Puis étiquetez /dev/sda3 comme ROOT_TEMPLATE :

# e2label /dev/sda3 ROOT_TEMPLATE

Créez les répertoires :

# 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

Commentez la ligne qui monte votre système racine dans /etc/fstab, puis ajoutez cette ligne :

LABEL=OVERLAY_RW /overlay_storage ext4 defaults,noatime 0 2

Actualisez le cache des paquets et installez ces paquets (cela peut supprimer votre busybox ; procédez avec prudence) :

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

Ajoutez ces lignes dans /etc/initramfs-tools/modules :

overlay
ext4
jbd2
crc32c
mbcache

Créez /etc/tmpfiles.d/overlay-runtime.conf avec ces lignes :

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

Créez /etc/initramfs-tools/scripts/init-bottom/overlayroot et ajoutez le script suivant :

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

Rendez /etc/initramfs-tools/scripts/init-bottom/overlayroot exécutable :

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

Actualisez votre initramfs :

# update-initramfs -u -k all

Créez /usr/local/sbin/overlay-prune.sh et ajoutez ces lignes :

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

Rendez /usr/local/sbin/overlay-prune.sh exécutable :

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

Créez /etc/logrotate.d/overlay-prune avec ces lignes :

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

Créez le fichier de log et définissez ses propriétaires et permissions :

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

Créez /etc/systemd/system/overlay-prune.service avec ces lignes :

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

Créez /etc/systemd/system/overlay-prune.timer avec ces lignes :

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

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

[Install]
WantedBy=timers.target

Activez le timer :

# systemctl enable overlay-prune.timer

Redémarrez maintenant votre système :

# reboot

Étape 3 : Testez votre installation

Après le redémarrage, exécutez :

$ findmnt /

Vous devriez voir que / est un système de fichiers overlay.

Créez plusieurs fichiers à différents emplacements, puis redémarrez le système ; ces fichiers devraient avoir disparu.

Si tout est correct, vous avez terminé.