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