Dynamic minimal template

7 views
Skip to first unread message

qubist

unread,
Feb 26, 2026, 9:37:44 AM (12 days ago) Feb 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
Reply all
Reply to author
Forward
0 new messages