This guide will help you configure a hardened Debian template for QubesOS.

What version of Debian would you like to target? The latest is 12.

To get started, simply open the Q menu and type dom0 Xfce Terminal. I recommend adding the dom0 terminal to favorites (Right click > Add to favorites) since it's our main entry point for performing advanced customization of your system. Nevertheless, we'll try to avoid the dom0 terminal as much as possible in these guides, to keep things easy and friendly.

Now go ahead and type following code into your dom0 terminal (note that you can't copy-and-paste into dom0, for security reasons). This will download a minimal TemplateVM called debian-XX-minimal.

# ==================== # Note that for security reasons, you
#     dom0 > xterm     # can't paste text into dom0. You therefore
# ==================== # have to type each command manually.

qubes-dom0-update qubes-template-debian-XX-minimal

To avoid modifying the base template, let's create a clone called dXX. This clone will serve as the basis of most of our virtual machines.

# ==================== # Note that for security reasons, you
#     dom0 > xterm     # can't paste text into dom0. You therefore
# ==================== # have to type each command manually.

qvm-clone debian-XX-minimal dXX

Now open a terminal in dXX with root user permissions.

# ==================== # Note that for security reasons, you
#     dom0 > xterm     # can't paste text into dom0. You therefore
# ==================== # have to type each command manually.

qvm-run -u root dXX xterm

The following code should be executed in dXX. Its purpose is to reroute Debian-supplied updates through a random Swiss mirror (since Switzerland has very good privacy laws), and to avoid installing proprietary blobs (which represent a security risk). Re-enabling proprietary blobs is straightforward, and sometimes necessary. Functions to do this are therefore also included. After the reroute it then installs a few critical utilities, and does a full upgrade of all installed software.

The most controversial utility this installs is passwordless sudo. Many security experts would argue that installing passwordless sudo here reduces our overall security posture. However, they're objectively wrong. This is because passwordless sudo allows us to run our TemplateVMs with non-root permissions, which means that potentially dangerous commands will fail unless used with sudo. This improves the transparency and auditability of the overall process of setting up our TemplateVMs.

There are certain high-assurance VMs for which we'll ultimately uninstall passwordless sudo. This is to ensure potential attackers have the fewest possible tools available to them. But we only uninstall it AFTER setting up our system, not before, to avoid running as the root user.

# =================== # You can use SHIFT+INSERT to paste into
#     dXX > xterm     # xterm, or use the middle mouse button.
# =================== #

echo 'Defining functions for modifying apt sources'
until tee -a /etc/bash.bashrc > /dev/null <<'EOF_OUTER'
# h20-register-debian-sources
# ----------------------------
# Creates a Debian APT sources list file at a specified path, using a random
# Switzerland-based mirror and a given set of components (e.g. 'main', 'contrib non-free-firmware').
# The function tests mirrors until one works by running 'apt update'.
# Arguments:
# 1 - Path to the file to create (e.g. /etc/apt/sources.list.d/nonfree.list)
# 2 - Components string (e.g. "contrib non-free-firmware")
h20-register-debian-sources() {
    local TARGET_PATH="\$1"
    local COMPONENTS="\$2"
    if [ -z "\$VERSION_CODENAME" ]; then
        echo "Getting codename for Debian version"
        until source /etc/os-release; do sleep 1; done
    fi
    local CODENAME="\$VERSION_CODENAME"
    local MIRRORS=(
        'debian.ethz.ch'
        'linuxsoft.cern.ch'
        'mirror.iway.ch'
        'mirror.metanet.ch'
        'mirror.sinavps.ch'
        'pkg.adfinis-on-exoscale.ch'
    )
    echo "Trying random Swiss mirrors to find a working one for components: \$COMPONENTS"
    while true; do
        local MIRROR="\${MIRRORS[\$RANDOM % \${#MIRRORS[@]}]}"
        echo "Trying mirror: \$MIRROR"
        until sudo tee "\$TARGET_PATH" > /dev/null <<EOF_INNER
# Debian sources
deb     https://\$MIRROR/debian \$CODENAME \$COMPONENTS
deb-src https://\$MIRROR/debian \$CODENAME \$COMPONENTS

deb     https://\$MIRROR/debian-security \$CODENAME-security \$COMPONENTS
deb-src https://\$MIRROR/debian-security \$CODENAME-security \$COMPONENTS
EOF_INNER
        do sleep 1; done
        echo 'Running apt update...'
        if sudo apt update; then
            echo "Success with \$MIRROR"
            break
        else
            echo "apt update failed with \$MIRROR - trying another mirror in 1s..."
            sleep 1
        fi
    done
    echo "APT repository file created at \$TARGET_PATH using mirror: \$MIRROR"
    echo "Run 'sudo apt update' to refresh your APT repositories."
}

# h20-enable-nonfree-debian-sources
# -----------------------------------
# Enables access to Debian's contrib and non-free-firmware components
# by creating a new APT sources list file using a Swiss mirror.
# This is useful for systems that need non-free firmware or extra driver support.
h20-enable-nonfree-debian-sources() {
    h20-register-debian-sources /etc/apt/sources.list.d/nonfree.list "contrib non-free-firmware"
}

# h20-disable-nonfree-debian-sources
# ------------------------------------
# Disables contrib and non-free-firmware support by removing the
# previously created sources list file. This helps enforce a fully
# free and auditable system. Reminder: run 'sudo apt update' after disabling.
h20-disable-nonfree-debian-sources() {
    local TARGET_PATH="/etc/apt/sources.list.d/nonfree.list"
    echo "Disabling contrib and non-free-firmware by removing: $TARGET_PATH"
    if sudo rm -f "$TARGET_PATH"; then
        echo "Removed $TARGET_PATH"
        echo "Run 'sudo apt update' to refresh your APT sources."
    else
        echo "Failed to remove $TARGET_PATH"
        return 1
    fi
}
EOF_OUTER
do sleep 1; done

echo 'Ensuring newly defined functions are immediately available'
until source /etc/bash.bashrc; do sleep 1; done

echo 'Changing to random Swiss mirror and disabling proprietary blobs.'
h20-register-debian-sources /etc/apt/sources.list "main"

echo 'Installing key packages to ensure updates are over HTTPS'
until sudo apt install apt-transport-https ca-certificates; do sleep 1; done

echo 'Installing passwordless sudo so we can avoid running as root user'
until sudo apt install qubes-core-agent-passwordless-root; do sleep 1; done

Begin by closing the terminal in dXX with root user status. You don't have to restart the VM. Afterwards, reopen xterm in dXX via the Q menu. This has the effect of opening it without root user status, and forces potentially dangerous commands to go via the sudo escape hatch.

Once the new terminal has been opened, go ahead and paste in the following code. It updates all of your software, and then cleans up afterwards.

echo 'Performing full upgrade via the new mirror'
until sudo apt update; do sleep 1; done
until sudo apt full-upgrade; do sleep 1; done
until sudo apt autoremove; do sleep 1; done
until sudo apt clean; do sleep 1; done

For the most part, security and privacy are complementary. Unfortunately, they're actually occassionally in conflict. Logs are a common example of this. Keeping extensive logs is good for security, because they facilitate automated (and manual) intrusion detection, and assist users in reacting appropriately if there's a breach. On the other hand, they can be stolen by hackers, and recovered from hard drives. Logs are therefore a common pain point for security and privacy setups.

At Hero to Zero, we advocate using the following rules to address the tension:

Additionally, we need to make sure there's fundamentally an asymmetry between enabling logging (which should be easy) and disabling logging (which should require a password). This ensures that a malicious script cannot easily disable logging.

To achieve this, the following script makes four functions available.

NOTE: To gain this asymmetry, we have to strategically uninstall passwordless root where appropriate. We'll do that later.

For now, please run the following code in dXX.

# =================== # You can use SHIFT+INSERT to paste into
#     dXX > xterm     # xterm, or use the middle mouse button.
# =================== #

echo 'Ensuring logs are redirected to RAM and purged on shutdown'
until sudo mount -t tmpfs -o size=100M,mode=0755 tmpfs /var/log; do sleep 1; done

echo 'Recreating essential /var/log files and directories (tmpfiles.d)'
until sudo tee /etc/tmpfiles.d/h20-var-log.conf > /dev/null <<'EOF'
# h20-var-log.conf — recreate essentials when /var/log is tmpfs
d /var/log               0755 root root -
d /var/log/apt           0755 root root -
d /var/log/dpkg          0755 root root -
d /var/log/fsck          0755 root root -
d /var/log/rsyslog       0755 root root -
d /var/spool/rsyslog     0755 root root -
# classic login/accounting files
f /var/log/wtmp          0664 root utmp -
f /var/log/btmp          0600 root utmp -
f /var/log/lastlog       0644 root root -
f /var/log/faillog       0644 root root -
EOF
do sleep 1; done
until sudo systemd-tmpfiles --create /etc/tmpfiles.d/h20-var-log.conf; do sleep 1; done

echo 'Persist the mount in /etc/fstab (no idempotency, just append)'
until sudo tee -a /etc/fstab > /dev/null <<'EOF'
tmpfs /var/log tmpfs defaults,size=100m,mode=0755 0 0
EOF
do sleep 1; done

echo 'Configure systemd-journald to use volatile (RAM-only) logging'
until sudo mkdir -p /etc/systemd/journald.conf.d; do sleep 1; done
until sudo tee /etc/systemd/journald.conf.d/volatile.conf > /dev/null <<'EOF'
[Journal]
Storage=volatile
EOF
do sleep 1; done

echo 'Restarting journald to apply the volatile logging config'
until sudo systemctl restart systemd-journald; do sleep 1; done
if systemctl list-unit-files --no-legend --type=service 2>/dev/null | awk '{print $1}' | grep -Fxq rsyslog.service; then
  echo 'Restarting rsyslog to resync with journald and /var/log'
  until sudo systemctl restart rsyslog; do sleep 1; done
fi

echo 'Commenting out HIST or shopt lines in /etc/skel/.bashrc'
until sudo awk -v h="#" -f - /etc/skel/.bashrc > "/tmp/.h20.bashrc.new" <<'EOF'
/^[[:space:]]*(HIST|shopt)/ {
    sub(/^[[:space:]]*/, "&" h " ")
}
{ print }
EOF
do sleep 1; done
until sudo mv /tmp/.h20.bashrc.new /etc/skel/.bashrc; do sleep 1; done

echo 'Ensuring bash history is limited to 10 commands, stored in RAM, and purged on shutdown'
until sudo tee -a /etc/bash.bashrc > /dev/null <<'EOF'
export HISTSIZE=10
export HISTFILESIZE=0
export HISTFILE=/dev/null
EOF
do sleep 1; done

echo 'Creating root helper for h20-enable-journald'
until sudo tee /usr/sbin/h20-enable-journald-root > /dev/null <<'EOF'
#!/bin/sh
# h20-enable-journald-root
# ------------------------
# Unmasks, enables, and starts journald sockets/services, and rsyslog if present.
echo "[h20] enabling journald sockets/services"
for u in systemd-journald.socket systemd-journald-dev-log.socket systemd-journald-audit.socket systemd-journald.service; do
  if systemctl list-unit-files --no-legend --type=service --type=socket 2>/dev/null | awk '{print $1}' | grep -Fxq "$u"; then
    if systemctl is-enabled "$u" 2>&1 | grep -q masked; then
      echo "[h20] unmask $u"
      until systemctl unmask "$u"; do sleep 1; done
    fi
    echo "[h20] enable $u"
    until systemctl enable "$u"; do sleep 1; done
    echo "[h20] start $u"
    until systemctl start "$u" || systemctl restart "$u"; do sleep 1; done
  else
    echo "[h20] skip missing $u"
  fi
done
if command -v rsyslogd >/dev/null 2>&1 || [ -f /usr/sbin/rsyslogd ] || [ -f /usr/bin/rsyslogd ]; then
  echo "[h20] ensuring rsyslog.service is up"
  if systemctl is-enabled rsyslog.service 2>&1 | grep -q masked; then
    until systemctl unmask rsyslog.service; do sleep 1; done
  fi
  until systemctl enable rsyslog.service; do sleep 1; done
  until systemctl start rsyslog.service || systemctl restart rsyslog.service; do sleep 1; done
else
  echo "[h20] rsyslog not installed; skipping"
fi
echo "[h20] journald enable complete"
EOF
do sleep 1; done
until sudo chmod 0755 /usr/sbin/h20-enable-journald-root; do sleep 1; done
until sudo chown root:root /usr/sbin/h20-enable-journald-root; do sleep 1; done

echo 'Creating root helper for h20-disable-journald'
until sudo tee /usr/sbin/h20-disable-journald-root > /dev/null <<'EOF'
#!/bin/sh
# h20-disable-journald-root
# -------------------------
# Stops and disables journald sockets/services; stops/disables rsyslog if present.
echo "[h20] stopping journald sockets/services"
for u in systemd-journald.service systemd-journald.socket systemd-journald-dev-log.socket systemd-journald-audit.socket; do
  if systemctl list-unit-files --no-legend --type=service --type=socket 2>/dev/null | awk '{print $1}' | grep -Fxq "$u"; then
    until systemctl stop "$u" || systemctl try-restart "$u"; do sleep 1; done
    until systemctl disable "$u"; do sleep 1; done
  fi
done
if systemctl list-unit-files --no-legend --type=service 2>/dev/null | awk '{print $1}' | grep -Fxq rsyslog.service; then
  until systemctl stop rsyslog.service || systemctl try-restart rsyslog.service; do sleep 1; done
  until systemctl disable rsyslog.service; do sleep 1; done
fi
echo "[h20] journald mostly disabled"
EOF
do sleep 1; done
until sudo chmod 0755 /usr/sbin/h20-disable-journald-root; do sleep 1; done
until sudo chown root:root /usr/sbin/h20-disable-journald-root; do sleep 1; done

echo 'Creating root helper for h20-enable-log-exfil'
until sudo tee /usr/sbin/h20-enable-log-exfil-root > /dev/null <<'EOF'
#!/bin/sh
# h20-enable-log-exfil-root
# -------------------------
# Configures rsyslog to forward all logs to host "logaudit" over UDP 514.
AUDIT_HOST="logaudit"
AUDIT_PORT="514"
# install rsyslog if missing
if ! command -v rsyslogd >/dev/null 2>&1 && [ ! -f /usr/sbin/rsyslogd ] && [ ! -f /usr/bin/rsyslogd ]; then
  echo "[h20] installing rsyslog"
  until apt-get update; do sleep 1; done
  until DEBIAN_FRONTEND=noninteractive apt-get install -y rsyslog; do sleep 1; done
fi
# ensure imjournal follows journald
if [ -f /etc/rsyslog.conf ]; then
  if ! grep -q '^module(load="imjournal")' /etc/rsyslog.conf; then
    echo '[h20] enabling imjournal in /etc/rsyslog.conf'
    until sed -i '1imodule(load="imjournal" StateFile="/var/spool/rsyslog/imjournal.state")' /etc/rsyslog.conf; do sleep 1; done
  fi
fi
# forward everything
until mkdir -p /etc/rsyslog.d; do sleep 1; done
until tee /etc/rsyslog.d/99-h20-forward-all.conf >/dev/null <<EOC
# h20 forward all logs to logaudit
*.*  @${AUDIT_HOST}:${AUDIT_PORT}
EOC
do sleep 1; done
# start/enable rsyslog
if systemctl is-enabled rsyslog.service 2>&1 | grep -q masked; then
  until systemctl unmask rsyslog.service; do sleep 1; done
fi
until systemctl enable rsyslog.service; do sleep 1; done
until systemctl restart rsyslog.service; do sleep 1; done
echo "[h20] log exfil enabled to ${AUDIT_HOST}:${AUDIT_PORT}"
EOF
do sleep 1; done
until sudo chmod 0755 /usr/sbin/h20-enable-log-exfil-root; do sleep 1; done
until sudo chown root:root /usr/sbin/h20-enable-log-exfil-root; do sleep 1; done

echo 'Creating root helper for h20-disable-log-exfil'
until sudo tee /usr/sbin/h20-disable-log-exfil-root > /dev/null <<'EOF'
#!/bin/sh
# h20-disable-log-exfil-root
# --------------------------
# Removes the rsyslog forwarding config and restarts rsyslog.
if [ -f /etc/rsyslog.d/99-h20-forward-all.conf ]; then
  until rm -f /etc/rsyslog.d/99-h20-forward-all.conf; do sleep 1; done
fi
if systemctl list-unit-files --no-legend --type=service 2>/dev/null | awk '{print $1}' | grep -Fxq rsyslog.service; then
  until systemctl restart rsyslog.service; do sleep 1; done
fi
echo "[h20] log exfil disabled"
EOF
do sleep 1; done
until sudo chmod 0755 /usr/sbin/h20-disable-log-exfil-root; do sleep 1; done
until sudo chown root:root /usr/sbin/h20-disable-log-exfil-root; do sleep 1; done

echo 'Allowing passwordless enablement of log creation and log exfil'
until sudo tee /etc/sudoers.d/h20-logs > /dev/null <<'EOF'
Cmnd_Alias H20_ENABLES = /usr/sbin/h20-enable-journald-root, /usr/sbin/h20-enable-log-exfil-root
user ALL=(root) NOPASSWD: H20_ENABLES
Defaults!H20_ENABLES env_reset,secure_path="/usr/sbin:/usr/bin:/sbin:/bin"
EOF
do sleep 1; done
until sudo chmod 0440 /etc/sudoers.d/h20-logs; do sleep 1; done
until sudo visudo -c > /dev/null; do sleep 1; done

echo 'Creating bash wrappers for exposure to end-user'
until sudo tee -a /etc/bash.bashrc > /dev/null <<'EOF'
# h20-enable-journald
# -------------------
# Turns system logging on. Unmasks/enables/starts journald sockets/services,
# and ensures rsyslog is running if present. Can be run by non-root user
# due to a limited sudoers rule.
h20-enable-journald() {
  until sudo /usr/sbin/h20-enable-journald-root; do sleep 1; done
}

# h20-disable-journald
# --------------------
# Turns most system logging off. Stops/disables journald sockets/services
# and stops/disables rsyslog if present. Requires root/sudo privileges.
h20-disable-journald() {
  until sudo /usr/sbin/h20-disable-journald-root; do sleep 1; done
}

# h20-enable-log-exfil
# --------------------
# Forwards all logs to the "logaudit" host over UDP 514 via rsyslog.
# Installs rsyslog if missing. Can be run without a password due to
# a limited sudoers rule.
h20-enable-log-exfil() {
  until sudo /usr/sbin/h20-enable-log-exfil-root; do sleep 1; done
}

# h20-disable-log-exfil
# ---------------------
# Disables forwarding by removing the rsyslog snippet and restarting rsyslog.
# Requires root/sudo privileges.
h20-disable-log-exfil() {
  until sudo /usr/sbin/h20-disable-log-exfil-root; do sleep 1; done
}

# h20-watch-logs
# --------------
# Shows the last 200 journal lines and then follows new log entries in realtime.
# Must be run as root or with sudo.
h20-watch-logs() {
  sudo journalctl -n 200 -f --no-hostname --output=short-iso
}
EOF
do sleep 1; done

echo 'Installing 1-minute journal vacuum service and timer'
until sudo tee /etc/systemd/system/h20-journal-vacuum.service > /dev/null <<'EOF'
[Unit]
Description=H2O - vacuum systemd journal to 1 minute

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'journalctl --vacuum-time=1m || true'
EOF
do sleep 1; done

until sudo tee /etc/systemd/system/h20-journal-vacuum.timer > /dev/null <<'EOF'
[Unit]
Description=H2O - run journal vacuum every minute

[Timer]
OnBootSec=30s
OnUnitActiveSec=60s
Unit=h20-journal-vacuum.service
AccuracySec=10s

[Install]
WantedBy=timers.target
EOF
do sleep 1; done

echo 'Enabling and starting the 1-minute journal vacuum timer'
until sudo systemctl daemon-reload; do sleep 1; done
until sudo systemctl enable h20-journal-vacuum.timer; do sleep 1; done
until sudo systemctl start h20-journal-vacuum.timer; do sleep 1; done

Now run the following code. It reconfigure the overall operating system with a view towards security and the creation of verbose and maximally helpful logs.

Remember: if logs are fundamentally incompatible with your use case, the h20-disable-journald command provided by the previous script can be used to eliminate most of them.

# =================== # You can use SHIFT+INSERT to paste into
#     dXX > xterm     # xterm, or use the middle mouse button.
# =================== #

echo 'Enable extra canaries/checks for binaries using glibc malloc'
until echo 'MALLOC_CHECK_=3' | sudo tee -a /etc/environment > /dev/null; do sleep 1; done
until echo 'MALLOC_PERTURB_=153' | sudo tee -a /etc/environment > /dev/null; do sleep 1; done

echo 'Hardening sysctl for privacy and security'
until sudo tee -a /etc/sysctl.d/97-hardening.conf > /dev/null <<'EOF'
################################
#### MEMORY/DEBUG/FORENSICS ####
################################
kernel.kptr_restrict = 2             # Hide kernel pointers everywhere
kernel.dmesg_restrict = 1            # Only root can read dmesg
kernel.yama.ptrace_scope = 3         # Completely disable ptrace except direct child
kernel.sysrq = 0                     # No magic sysrq
kernel.ftrace_enabled = 0            # Block kernel tracing (anti-forensics)
kernel.perf_event_paranoid = 3       # No perf monitoring for non-root
kernel.perf_event_mlock_kb = 1       # Minimal perf event buffer
kernel.kexec_load_disabled = 1       # No kexec (rootkits/forensics)
kernel.kprobes_allow_uds = 0         # No unprivileged kprobes (if supported)
kernel.core_pattern = "|/bin/false"  # Never dump core to disk
kernel.core_pipe_limit = 0           # No core dump pipes
kernel.randomize_va_space = 2        # Full ASLR
vm.mmap_min_addr = 65536             # Block low-address mmap attacks
kernel.numa_balancing = 0            # Disable page migration/auto NUMA

#######################
#### SWAP BEHAVIOR ####
#######################
vm.swappiness = 10             # Do not use swap except under a lot of ram pressure
vm.overcommit_memory = 2       # Strict overcommit restrictions (reduce DoS/fuzz)
vm.overcommit_ratio = 400      # Gives a 400% overcommit ratio
vm.dirty_background_ratio = 7  # Reduced from a default of 10
vm.dirty_ratio = 15            # Reduced from a default of 20
vm.page-cluster = 4            # 64KiB swap clusters (be sure to use ephemeral swap)
vm.min_free_kbytes = 65536     # Extra RAM for kernel (resist memory starvation)

###########################
#### HIDE PROCESS INFO ####
###########################
kernel.pid_max = 4194304
kernel.ngroups_max = 65536
kernel.threads-max = 32768
kernel.sched_child_runs_first = 1

#######################################
#### ANTI-SURVEILLANCE / ANTI-LEAK ####
#######################################
kernel.printk = 3 3 3 3      # Reduces kernel logging verbosity - good for minimizing leakage into dmesg, journald, or serial consoles
kernel.nmi_watchdog = 0      # Disables kernel NMI watchdog - reduces noise and can prevent info leakage via debugging/traps
kernel.acpi_video_flags = 0  # Minimal ACPI logs
kernel.acpi_rsdp = 0         # Minimal ACPI root system desc
kernel.domainname = ""       # Removes legacy NIS/YP domain - safe, modern systems do not need it
# kernel.hostname = ""       # If the environment was a bare-metal setup debian, you would uncomment this. But within Qubes it creates problems, and should remain commented

###########################
#### FILESYSTEM / MISC ####
###########################
fs.suid_dumpable = 0         # No setuid core dumps
fs.protected_regular = 2     # Block regular file hardening attacks
fs.protected_symlinks = 1    # Block symlink privilege attacks
fs.protected_hardlinks = 1   # Block hardlink privilege attacks
fs.protected_fifos = 1       # Block FIFO tricks
fs.protected_readdir = 1     # Block dangerous readdir tricks (newer kernels)
fs.inode_readahead_blks = 8  # Minimal readahead
fs.pipe-user-pages-soft = 0  # Harden pipe attacks

###################################
#### USERNAMESPACES/CONTAINERS ####
###################################
kernel.unprivileged_userns_clone = 0  # Block userns (most privesc)
kernel.unprivileged_bpf_disabled = 1  # Block unprivileged BPF everywhere
user.max_user_namespaces = 0          # No user namespaces for any process
user.max_mnt_namespaces = 8
user.max_pid_namespaces = 8
user.max_net_namespaces = 8
user.max_uts_namespaces = 8
user.max_ipc_namespaces = 8
user.max_cgroup_namespaces = 8
user.max_time_namespaces = 8

####################################
#### AUDIT/LOGGING MINIMIZATION ####
####################################
kernel.audit_enabled = 0     # No kernel audit (if using forensics resistance)
kernel.random.trust_cpu = 0  # Do not trust CPU for randomness

######################
#### MISC PRIVACY ####
######################
dev.tty0.autoclose = 1  # Autoclose on logout (no stray processes)
dev.tty.autoclose = 1

####################################
#### MAXIMIZE MEMORY RANDOMNESS ####
####################################
vm.mmap_rnd_bits = 32         # Maximum mmap entropy (if arch supports)
vm.mmap_rnd_compat_bits = 16  # Same for compat mode

########################################
#### ACCOMODATE HARDENED ALLOCATORS ####
########################################
vm.max_map_count = 1048576  # Multiply default value by 16x to support hardened_malloc

#################################
#### NETWORK STACK HARDENING ####
#################################
# IPv4
net.ipv4.tcp_timestamps = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 1
net.ipv4.conf.default.secure_redirects = 1
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.shared_media = 0
net.ipv4.conf.default.shared_media = 0
net.ipv4.conf.all.proxy_arp = 0
net.ipv4.conf.default.proxy_arp = 0
net.ipv4.ip_forward = 0
net.ipv4.conf.all.arp_filter = 1
net.ipv4.conf.default.arp_filter = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.default.arp_announce = 2
net.ipv4.conf.all.arp_ignore = 2
net.ipv4.conf.default.arp_ignore = 2

# IPv6
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
net.ipv6.conf.all.drop_unsolicited_na = 1
net.ipv6.conf.default.drop_unsolicited_na = 1
net.ipv6.conf.lo.drop_unsolicited_na = 1

##############################################
#### PREVENT COMPACTION OF SENSITIVE DATA ####
##############################################
vm.compact_unevictable_allowed = 0
EOF
do sleep 1; done

echo 'Reapplying .conf files'
until sysctl --system; do sleep 1; done

The majority of software vulnerabilities arise from memory management errors. We therefore need a better and more secure memory allocator.

Without closing your dXX terminal, go ahead and open another terminal within a disposable qube. It should be called dispYYYY. Please enter the YYYY value below.

In this step, we download and build the memory allocator, and then transfer it to dXX. Feel free to close dispYYYY after performing this step.

# ======================== # You can use SHIFT+INSERT to paste into
#     dispYYYY > xterm     # xterm, or use the middle mouse button.
# ======================== #

# Install download and build prerequisites
until sudo apt update; do sleep 1; done
until sudo apt install -y git build-essential cmake pkg-config; do sleep 1; done

# Clone repository
rm -rf hardened_malloc || true
until git clone https://github.com/GrapheneOS/hardened_malloc.git; do sleep 1; done

# Move into a subshell
(
    # Enter the hardened_malloc directory
    until cd hardened_malloc; do sleep 1; done

    # Build default
    until make clean; do sleep 1; done
    until make RELEASE=1; do sleep 1; done
    until cp out/libhardened_malloc.so ../libhardened_malloc_default.so; do sleep 1; done

    # Build non-clearing version
    export CONFIG_ZERO_ON_FREE=0
    export CONFIG_WRITE_AFTER_FREE_CHECK=0
    until make clean; do sleep 1; done
    until make RELEASE=1; do sleep 1; done
    until cp out/libhardened_malloc.so ../libhardened_malloc_softened.so; do sleep 1; done
)

# Package both versions
rm -f libhardened_malloc.tar.gz || true
until tar -czf libhardened_malloc.tar.gz libhardened_malloc_default.so libhardened_malloc_softened.so; do sleep 1; done

# Qubes copy to VM
until sync; do sleep 1; done
until qvm-copy libhardened_malloc.tar.gz; do sleep 1; done

Now go back to your dXX terminal. If you closed it, open a dom0 terminal, and hit the up button until you find the appropriate command. If you can't find it, use the back button on this webpage to find the appropriate dom0 command.

Go ahead and run the following script in dXX. It put libhardened malloc into the appropriate directory, and sets up some functions to make activating it straightforward.

# =================== # You can use SHIFT+INSERT to paste into
#     dXX > xterm     # xterm, or use the middle mouse button.
# =================== #

(
    # Extract the tarball from the incoming Qubes directory
    echo '[INFO] Extracting libhardened_malloc.tar.gz from QubesIncoming...'
    until cd /home/user/QubesIncoming/dispYYYY; do sleep 1; done
    until sudo tar -xzf libhardened_malloc.tar.gz; do sleep 1; done

    # Set correct permissions
    until sudo chmod 755 libhardened_malloc_default.so; do sleep 1; done
    until sudo chmod 755 libhardened_malloc_softened.so; do sleep 1; done

    # Move both .so files to /usr/lib
    until sudo mv libhardened_malloc_default.so /usr/lib/; do sleep 1; done
    until sudo mv libhardened_malloc_softened.so /usr/lib/; do sleep 1; done

    # Clean up by removing the directory that has the tarball
    cd ..
    until rm -rf dispYYYY; do sleep 1; done
)

# Add allocator switching and inspection functions to /etc/bash.bashrc
echo '[INFO] Adding allocator control functions to /etc/bash.bashrc'
until sudo tee -a /etc/bash.bashrc > /dev/null <<'EOF'
# h20-libhardened-malloc-harden
# -----------------------------------
# Enables the default version of libhardened_malloc system-wide
# by adding it to /etc/ld.so.preload. This version includes
# zero-on-free behavior for maximum memory hygiene.
h20-libhardened-malloc-harden() {
    sudo sed -i "\|/usr/lib/libhardened_malloc_.*\.so|d" /etc/ld.so.preload 2>/dev/null || true
    echo "/usr/lib/libhardened_malloc_default.so" | sudo tee -a /etc/ld.so.preload > /dev/null
    # Ensure only the correct line is present
    sudo grep -Fxq "/usr/lib/libhardened_malloc_default.so" /etc/ld.so.preload && \
    ! sudo grep -Fq "/usr/lib/libhardened_malloc_softened.so" /etc/ld.so.preload && return 0 || return 1
    echo "Hardened malloc entries written to /etc/ld.so.preload."
}

# h20-libhardened-malloc-soften
# ------------------------------------
# Enables a softened version of libhardened_malloc system-wide
# by adding it to /etc/ld.so.preload. This version disables
# zero-on-free for better compatibility with some programs.
h20-libhardened-malloc-soften() {
    sudo sed -i "\|/usr/lib/libhardened_malloc_.*\.so|d" /etc/ld.so.preload 2>/dev/null || true
    echo "/usr/lib/libhardened_malloc_softened.so" | sudo tee -a /etc/ld.so.preload > /dev/null
    # Ensure only the correct line is present
    sudo grep -Fxq "/usr/lib/libhardened_malloc_softened.so" /etc/ld.so.preload && \
    ! sudo grep -Fq "/usr/lib/libhardened_malloc_default.so" /etc/ld.so.preload && return 0 || return 1
    echo "Softened hardened malloc entries written to /etc/ld.so.preload."
}

# h20-libhardened-malloc-disable
# -------------------------------
# Disables all versions of libhardened_malloc by removing them
# from /etc/ld.so.preload. Use this if you encounter crashes
# in software that is not compatible with custom allocators.
h20-libhardened-malloc-disable() {
    echo "Disabling libhardened_malloc by filtering /etc/ld.so.preload..."
    if [ ! -f /etc/ld.so.preload ]; then
        echo "No preload file exists - nothing to do."
        return 0
    fi
    if ! grep -qE "/usr/lib/libhardened_malloc_.*\.so" /etc/ld.so.preload; then
        echo "No libhardened_malloc entries found in /etc/ld.so.preload"
        return 0
    fi
    # Remove lines matching libhardened_malloc and overwrite the file
    until sudo grep -vE "/usr/lib/libhardened_malloc_.*\.so" /etc/ld.so.preload | sudo tee /etc/ld.so.preload.tmp > /dev/null; do sleep 1; done
    until sudo mv /etc/ld.so.preload.tmp /etc/ld.so.preload; do sleep 1; done
    echo "Hardened malloc entries removed from /etc/ld.so.preload."
}

# h20-libhardened-malloc-inspect
# ----------------------
# Prints any lines in /etc/ld.so.preload that contain
# the string "alloc". Use this to check which allocator
# is currently active.
h20-libhardened-malloc-inspect() {
    echo "The lines in /etc/ld.so.preload matching .*alloc.* are as follows:"
    sudo grep ".*alloc.*" /etc/ld.so.preload 2>/dev/null
}
EOF
do sleep 1; done

Close your terminal, and open it again (note that source won't work, since ShellGuard disabled it). You don't have to shutdown the VM, just close and reopen the terminal.

Now type the following into your terminal dXX terminal. It'll activate libhardened malloc for any future programs you run.

Congratulations-you just configured a hardened Debian template!

Let us now install some small utilities that will be helpful later.

In the wonderful world of the Linux desktop experience, there's at least four library clusters that are very hard to get away from. I have in mind, in particular, the following clusters:

The vast majority of applications you install will depend on one or two of the aforementioned clusters. Unfortunately, since these libraries are very full-featured, they represent enormous attack surfaces, and have a long history of exploitable vulnerabilities. It's therefore quite handy to be able blacklist and un-blacklist these clusters.

The following script installs functions we can use later to do exactly that. It doesn't actually perform the blacklisting; rather, it provides the commands that will allow us to quickly and easily do so later.

# =================== # You can use SHIFT+INSERT to paste into
#     dXX > xterm     # xterm, or use the middle mouse button.
# =================== #

sudo tee -a /etc/bash.bashrc > /dev/null <<'OUTER_EOF'
# h20-blacklist-gtk
# ------------------
# Blacklists the GTK Toolkit in apt.
h20-blacklist-gtk() {
    sudo tee /etc/apt/preferences.d/h20-blacklist-gtk > /dev/null <<'INNER_EOF'
Package: libgtk*
Pin: release *
Pin-Priority: -1

Package: gtk*
Pin: release *
Pin-Priority: -1

Package: libgail*
Pin: release *
Pin-Priority: -1

Package: gail*
Pin: release *
Pin-Priority: -1

Package: gtk-update-icon-cache
Pin: release *
Pin-Priority: -1
INNER_EOF
    echo "Blacklisted all GTK (except gdk and existing core stack)."
}

# h20-blacklist-qt
# ------------------
# Blacklists the Qt Toolkit in apt.
h20-blacklist-qt() {
    sudo tee /etc/apt/preferences.d/h20-blacklist-qt > /dev/null <<'INNER_EOF'
Package: libqt*
Pin: release *
Pin-Priority: -1

Package: qt5-*
Pin: release *
Pin-Priority: -1

Package: qt6-*
Pin: release *
Pin-Priority: -1

Package: qtbase*
Pin: release *
Pin-Priority: -1

Package: qttools*
Pin: release *
Pin-Priority: -1

Package: qtwayland*
Pin: release *
Pin-Priority: -1

Package: qtchooser
Pin: release *
Pin-Priority: -1
INNER_EOF
    echo "Blacklisted all Qt-related packages."
}

# h20-blacklist-ffmpeg
# ---------------------
# Blacklists the FFmpeg Multimedia Framework in apt.
h20-blacklist-ffmpeg() {
    sudo tee /etc/apt/preferences.d/h20-blacklist-ffmpeg > /dev/null <<'INNER_EOF'
Package: libavcodec*
Pin: release *
Pin-Priority: -1

Package: avcodec*
Pin: release *
Pin-Priority: -1

Package: libavformat*
Pin: release *
Pin-Priority: -1

Package: avformat*
Pin: release *
Pin-Priority: -1

Package: libavutil*
Pin: release *
Pin-Priority: -1

Package: avutil*
Pin: release *
Pin-Priority: -1

Package: libavfilter*
Pin: release *
Pin-Priority: -1

Package: avfilter*
Pin: release *
Pin-Priority: -1

Package: libavdevice*
Pin: release *
Pin-Priority: -1

Package: avdevice*
Pin: release *
Pin-Priority: -1

Package: libpostproc*
Pin: release *
Pin-Priority: -1

Package: postproc*
Pin: release *
Pin-Priority: -1

Package: libswscale*
Pin: release *
Pin-Priority: -1

Package: swscale*
Pin: release *
Pin-Priority: -1

Package: libswresample*
Pin: release *
Pin-Priority: -1

Package: swresample*
Pin: release *
Pin-Priority: -1

Package: ffmpeg
Pin: release *
Pin-Priority: -1
INNER_EOF
    echo "Blacklisted all FFmpeg/libav-related packages."
}

# h20-blacklist-gstreamer
# ------------------------
# Blacklists the GStreamer Multimedia Framework in apt.
h20-blacklist-gstreamer() {
    sudo tee /etc/apt/preferences.d/h20-blacklist-gstreamer > /dev/null <<'INNER_EOF'
Package: libgstreamer*
Pin: release *
Pin-Priority: -1

Package: gstreamer*
Pin: release *
Pin-Priority: -1

Package: libgst*
Pin: release *
Pin-Priority: -1

Package: gst*
Pin: release *
Pin-Priority: -1

Package: gir1.2-gstreamer*
Pin: release *
Pin-Priority: -1
INNER_EOF
    echo "Blacklisted all GStreamer-related packages."
}

# h20-unblacklist-gtk
# --------------------
# Deletes the file which blacklists the GTK Toolkit.
h20-unblacklist-gtk() {
    sudo rm -f /etc/apt/preferences.d/h20-blacklist-gtk
    echo "GTK blacklist removed."
}

# h20-unblacklist-gtk
# --------------------
# Deletes the file which blacklists the Qt Toolkit.
h20-unblacklist-qt() {
    sudo rm -f /etc/apt/preferences.d/h20-blacklist-qt
    echo "Qt blacklist removed."
}

# h20-unblacklist-ffmpeg
# -----------------------
# Deletes the file which blacklists the FFmpeg Toolkit.
h20-unblacklist-ffmpeg() {
    sudo rm -f /etc/apt/preferences.d/h20-blacklist-ffmpeg
    echo "FFmpeg blacklist removed."
}

# h20-unblacklist-ffmpeg
# -----------------------
# Deletes the file which blacklists the FFmpeg Toolkit.
h20-unblacklist-gstreamer() {
    sudo rm -f /etc/apt/preferences.d/h20-blacklist-gstreamer
    echo "GStreamer blacklist removed."
}
OUTER_EOF
do sleep 1; done

We've been using XTerminal because it's very minimal and battle-hardened compared with other terminal software. Unfortunately, it doesn't provide the standard copy-and-paste niceties that people have come to expect from their desktop applications. To address this, I recommend running the following code, which defines a custom terminal command h20-copy to assist with copying data out of XTerminal (or any other terminal).

Security note: the function h20-copy depends on a small utility called xclip, which provides the underlying clipboard functionality. Installing xclip introduces minimal attack surface, and is widely considered safe. However, if you're feeling ultra-paranoid, feel free to skip this step.

# =================== # You can use SHIFT+INSERT to paste into
#     dXX > xterm     # xterm, or use the middle mouse button.
# =================== #

# Install xclip for clipboard bridging
until sudo apt update; do sleep 1; done
until sudo apt install --no-install-recommends -y xclip; do sleep 1; done

# Append h20-copy function to system-wide bashrc
until sudo tee -a /etc/bash.bashrc > /dev/null <<'EOF'
# h20-copy
# ---------
# A basic clipboard helper.
# - No args: copy X11 primary selection to clipboard
# - With args: run command, copy its output to clipboard
# Text can only be pasted once (or twice, on some systems) before it is cleared
# NOTE: h20-copy has known issues with pasting into Mullvad Browser (?!). Long-term
# solution might be to write an xclip alternative from scratch.
h20-copy() {
  if [[ $# -eq 0 ]]; then
    xclip -o -selection primary | xclip -i -selection clipboard -loops 2
  else
    "$@" | xclip -selection clipboard -loops 2
  fi
}
EOF
do sleep 1; done

# Make the new function available immediately
until source /etc/bash.bashrc; do sleep 1; done

Go ahead and try out the new copying functionality.

You can also use h20-copy to get the output from a command into the clipboard. For example:

# Copy file contents to clipboard.
h20-copy cat FILENAME

Known issues:

That last dot point can be quite frustrating, as Qubes doesn't currently allow the pasting of clipboard text back into the original VM from whence it came. This is a known issue, and I'm working on it.

When you're done, be sure to shutdown dXX.