Dynamic minimal template

15 views
Skip to first unread message

qubist

unread,
Feb 26, 2026, 9:37:44 AMFeb 26
to qubes...@googlegroups.com
Hi,

[I am planning to publish this as a guide on the forum but I thought it
might be good to discuss it with the experts here and receive valuable
feedback.]

I am working on a concept of a dynamic minimal template system. It
would allow the simplicity and usability of:

- Single, yet minimal template for a distro, without the need for the
user to maintain a complex system of individual task-oriented mini
templates

- No need for update caching system

- Optimal storage space without duplication

- No extra SSD wear due to e.g. duplication of updates in many templates

- The benefits of the current minimal template


Here is how it works:

1. Start with a minimal template

2. Keep a list of every package the user installs manually in
installed_packages.list.

Example:

# cat installed_packages.list
nano
vim
xterm
less
gnupg

3. For each AppVM, set a vm-config value that stores the names of the
packages that must be available to the user in that domU

Example:

qvm-features <AppVM> vm-config.dynamite.wanted_packages 'xterm less vim'

4. Remove unused packages for a VM at boot time to reduce attack
surface. For the example above, those would be nano and gnupg.

The last step is done through a service installed and enabled in the
template that removes unused packages at boot time (as early as
possible), considering the vm-config and installed_packages.list.

Important: the "removal" is not real. Actual purging is time consuming
and results in CoW operations. I am using a trick - nullifying the
package files. This is very fast and from attack-surface perspective
has the same effect as if the package is not available.

My tests show this actually works and is fast. It adds a little to the
boot time, unfortunately. On the system I am testing on, that is ~1.3s
and that time is almost entirely due to apt-get finding dependencies.
If you can advice on a better and faster method to get that, that would
be very welcome.

Your thoughts, comments and suggestions are welcome!

Code of unit, nullifying script and package installation script:

├── etc
│   └── systemd
│   └── system
│   └── nullify-packages.service
└── opt
└── sbin
├── install-packages
└── nullify-packages

==========


[Unit]
Description=Nullify unused packages
After=local-fs.target
Before=qubes-early-vm-config.service
Requires=-.mount rw.mount
ConditionFileIsExecutable=/opt/sbin/nullify-packages
ConditionFileNotEmpty=/root/installed_packages.list
ConditionFirstBoot=yes
DefaultDependencies=no

[Service]
Type=oneshot
ExecStart=/opt/sbin/nullify-packages /root/installed_packages.list

[Install]
RequiredBy=qubes-early-vm-config.service


==========


# /opt/sbin/nullify-packages

#!/usr/bin/bash

set -euo pipefail

#-------------------------------------------------------------------------------
# Nullify all files of selected packages without uninstalling
# {@} - package names
#-------------------------------------------------------------------------------

nullify()
{
# https://manpages.debian.org/trixie/dpkg/dpkg.1#OPTIONS
# https://manpages.debian.org/trixie/debconf-doc/debconf.7.en.html

export DEBIAN_FRONTEND='noninteractive' \
DEBIAN_PRIORITY='critical' \
DEBCONF_NOWARNINGS='yes'

local -a packages
mapfile -t packages \
< <(apt-get autopurge --assume-no \
--simulate \
-- \
"${@}" \
| grep -o -- '^Purg .*\]' \
| sed -r -- 's/(^Purg )|((\s+\[[^][]*\])+$)//g')

unset DEBIAN_FRONTEND='noninteractive' \
DEBIAN_PRIORITY='critical' \
DEBCONF_NOWARNINGS='yes'

local file_list
# https://askubuntu.com/a/977067
file_list=$(dpkg --listfiles -- "${packages[@]}")

local -a paths
mapfile -t paths < <(sort -u -- <<< "${file_list}")

local -a canonical_paths
mapfile -t canonical_paths < <(readlink -m "${paths[@]}" | sort -u)

local filepath
for filepath in "${canonical_paths[@]}"; do
if [[ -f "${filepath}" ]]; then
# Nullifying a file this way does not result in CoW
# operations. It is also fast.
true > "${filepath}"
fi
done
}
#-------------------------------------------------------------------------------
log()
{
logger -t "$(basename "${0}"):" "${*}"
}
#-------------------------------------------------------------------------------
die()
{
log "${*}"
log 'Exitting'
exit 1
}
#-------------------------------------------------------------------------------
get_config_value()
{
/usr/bin/qubesdb-read "/vm-config/dynamite.${1}" 2>/dev/null \
| sed -r 's/ /\n/g' \
| sort -u
}
#-------------------------------------------------------------------------------

if ! _wanted_packages=$(get_config_value 'wanted_packages'); then
die 'No wanted packages in VM features'
fi

if (( $# < 1 )) || [[ ! -s "${1}" ]]; then
die 'Non-empty list file needed as argument.'
fi

_installed_packages=$(sed -r \
-- \
'/^[[:blank:]]*$/d' \
"${1}" \
| sort -u)

if [[ -z "${_installed_packages}" ]]; then
die 'Empty or missing list of installed packages'
fi

# Handle potential mistakes like having wanted but not installed packages
_installed_wanted_packages=$(comm -12 \
-- \
<(echo "${_wanted_packages}") \
<(echo "${_installed_packages}") \
| tr -d '[:blank:]')

# Only installed which are not wanted
_packages_to_nullify=$(comm -13 \
-- \
<(echo "${_installed_wanted_packages}") \
<(echo "${_installed_packages}") \
| tr -d '[:blank:]')

if [[ -z "${_packages_to_nullify}" ]]; then
die 'No packages to remove. Exitting.'
fi

mapfile -t _packages_to_nullify < <(echo "${_packages_to_nullify}")
log "Packages to process: $(xargs <<< "${_packages_to_nullify[@]}")"

if ! nullify "${_packages_to_nullify[@]}"; then
die 'Error occured during package nullifying. Check package list.'
fi

log 'Finished'


==========


# install-packages

#!/usr/bin/env bash

set -euo pipefail

if (( $# < 1 )); then
>&2 echo 'Supply one or more packages as argument'
exit 1
fi

apt-get install -o APT::Install-Recommends=0 \
-o APT::Install-Suggests=0 \
-- \
"${@}"

apt-get --yes autoclean

echo "${@}" \
| sed -r 's/[[:blank:]]+/\n/g; /^$/d' \
>> /root/installed_packages.list

qubist

unread,
Mar 28, 2026, 6:23:24 AM (4 days ago) Mar 28
to qubes...@googlegroups.com
After a lot of experimenting with more advanced scripts that helped
reduce service time to less than 500 ms (using a cached list of
nullified files). The result is the VM can use only the wanted packages
as if the template was minimal with only those installed.

Unfortunately, my idea doesn't work as expected: truncating files shows
the same 'usage' for root volume in qvm-volume, as for removing them
otherwise. So, that would be bad for SSD, as it would be done on each
boot.

Bummer.

Sorry for bothering!
Reply all
Reply to author
Forward
0 new messages