No network connection to download the proprietary HP plugin inside minimal template

How its going:

debian-hplip-named-diposable.sh

#!/bin/bash

################################################################################
# File Name    : debian-hplip-named-diposable.sh
# Description  : This script creates a disposable qube for HP printers via USB
#                using Debian's packaged hplip and hpli-gui with download and
#                installation of hp-plugin from Open Printing. This script
#                requires interation to accept EULA policies. Note, you must
#                use plugin version that matches Debian HPLIP version.
# Usage        : • Transfer from appvm to dom0:
#                qvm-run -p appvm 'cat ~/debian-hplip-named-diposable.sh' > ~/debian-hplip-named-diposable.sh
#                • Make executable:
#                chmod +x ~/debian-hplip-named-diposable.sh
#                • Run:
#                bash ~/debian-hplip-named-diposable.sh
# Author       : Me and the bois
# License      : Free of charge, no warranty
################################################################################

# Security first
set -e

# Configuration
BASE_TEMPLATE="debian-12-minimal"
CUSTOM_TEMPLATE="debian-hplip-template"
DISP_TEMPLATE="debian-hplip-template-dvm"
NAMED_DISP_VM="disp-hplip"

# Plugin URL for Debian HPLIP version
PLUGIN_URL="https://www.openprinting.org/download/printdriver/auxfiles/HP/plugins/hplip-3.22.10-plugin.run"

# Ensure printer is connected via USB and powered on
check_printer_attached() {
  echo -e "\nPrinter Check..."

  if qvm-usb 2>/dev/null | grep -q -i -E "03f0|Hewlett"; then
    echo "HP printer detected"
    return 0
  else
    echo "No HP printer detected"
    echo "Please connect HP printer via USB and ensure it's powered on"
    return 1
  fi
}

# Step 1: Verify and create base template
create_base_template() {
  echo -e "\nStep 1: Setting up base template $BASE_TEMPLATE..."

  if ! qvm-check "$BASE_TEMPLATE" 2>/dev/null; then
    echo "Installing $BASE_TEMPLATE..."
    sudo qubes-dom0-update "qubes-template-$BASE_TEMPLATE"
  fi

  # Ensure template is shut down before updating
  qvm-shutdown --wait "$BASE_TEMPLATE" 2>/dev/null || true

  if qvm-check "$BASE_TEMPLATE"; then
    echo "Updating $BASE_TEMPLATE..."
    sudo qubesctl --show-output --skip-dom0 --targets="$BASE_TEMPLATE" state.sls update.qubes-vm
  fi

  echo -e "\nBase template setup complete"
}

# Step 2: Create custom base template
create_custom_template() {
  echo -e "\nStep 2: Creating $CUSTOM_TEMPLATE..."

  if ! qvm-check "$CUSTOM_TEMPLATE"; then
    qvm-clone "$BASE_TEMPLATE" "$CUSTOM_TEMPLATE"
  fi

  qvm-prefs "$CUSTOM_TEMPLATE" label black
  qvm-prefs "$CUSTOM_TEMPLATE" include_in_backups false
  qvm-service "$CUSTOM_TEMPLATE" cups on
  qvm-service "$CUSTOM_TEMPLATE" avahi on
  qvm-start "$CUSTOM_TEMPLATE"
}

# Step 3: Install packages in template (change as needed)
install_system_dependencies() {
  echo -e "\nStep 3: Installing packages in $CUSTOM_TEMPLATE..."

  # Basic system configuration
  echo "Configuring system environment..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "echo 'TERM=xterm' >> /etc/environment"
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "sed -i 's/^# *\(en_US.UTF-8\)/\1/' /etc/locale.gen"
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "locale-gen en_US.UTF-8"

  # Install core packages
  echo "Installing core packages..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  apt-get install -y --no-install-recommends \
    qubes-core-agent-passwordless-root \
    qubes-core-agent-networking \
    qubes-usb-proxy \
    dbus \
    dbus-x11 \
    python3 \
    thunar \
    qubes-core-agent-thunar \
    mousepad \
    qpdfview \
    less \
    psmisc \
    bash-completion \
    xfce4-terminal
"
}

# Step 4: Install packages
install_hplip_from_apt() {
  echo -e "\nStep 4: Installing CUPS and essential libraries..."

  # Install CUPS and essential libraries
  echo "Installing CUPS and essential libraries..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  apt-get install -y --no-install-recommends \
  cups \
  cups-client \
  libcups2 \
  libcupsimage2 \
  libdbus-1-3 \
  libjpeg62-turbo \
  libusb-1.0-0 \
  libsane1 \
  avahi-daemon \
  avahi-utils
"

  # Install HPLIP CLI
  echo "Installing HPLIP CLI..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  apt-get install -y --no-install-recommends \
  hplip \
  python3-dbus \
  python3-pil \
  python3-reportlab
"

  # Installing HPLIP GUI
  echo "Installing HPLIP GUI..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
  curl \
  pkexec \
  polkitd \
  policykit-1 \
  cups-pk-helper \
  python3-pyqt5 \
  hplip-gui
"
}

# Step 5: Installing HP plugin from remote source
download_and_install_plugin() {
  echo -e "\nStep 5: Installing HP plugin..."

  echo "Using plugin version that matches Debian HPLIP version"
  echo "Downloading plugin from: $PLUGIN_URL"

  qvm-start --skip-if-running "$CUSTOM_TEMPLATE"

  # Attach HP printer if available (TODO: use variables for device id)
  DEVICE_ID=$(qvm-usb list | grep -i -E "03f0|Hewlett" | awk '{print $1}' | head -n 1)
  if [ -n "$DEVICE_ID" ]; then
    qvm-usb attach "$CUSTOM_TEMPLATE" "$DEVICE_ID"
    echo "Attached printer $DEVICE_ID to $CUSTOM_TEMPLATE"
  fi

  # Download plugin as root (TODO: dinamically download matching hplip version)
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  cd /tmp && curl -x http://127.0.0.1:8082/ \
  --retry 5 \
  --retry-delay 2 \
  --retry-max-time 30 \
  --connect-timeout 20 \
  --max-time 300 \
  --continue-at - \
  --output hplip-3.22.10-plugin.run \
  '$PLUGIN_URL'
"

  # Extract and install plugin with interactive prompts (TODO: noninteractive dont work, even extracting)
  echo "Installing plugin as root with interactive prompts..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  chmod +x /tmp/hplip-3.22.10-plugin.run
  DEBIAN_FRONTEND=noninteractive bash /tmp/hplip-3.22.10-plugin.run
"

  # Cleanup files
  qvm-run -p "$CUSTOM_TEMPLATE" "rm -rf /tmp/hplip-*" 2>/dev/null || true

  echo "Plugin installation completed successfully"
}

# Step 6: Service permissions and authentication configuration
setup_printer_services() {
  echo -e "\nStep 6: Configuring printer services..."

  # Create proper user
  echo "Configuring CUPS services..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  # Create cups user in correct group
  useradd -r -c 'CUPS printing service' -d /var/spool/cups -g lp -s /bin/false cups

  # Add cups user to lp group
  usermod -a -G lp cups

  # Add user to lp group for printer administration
  usermod -a -G lp user

  # Stop any running cups services
  systemctl stop cups cups.socket cups.path 2>/dev/null || true
  systemctl reset-failed cups cups.socket cups.path 2>/dev/null || true
"

  # Create cups-files.conf using mktemp
  echo "Configuring CUPS settings..."
  local cups_files_conf=$(mktemp)
  cat > "$cups_files_conf" << 'EOF'
# Default user and group for filters/backends/helper programs
User cups
Group lp

# Administrator user group
SystemGroup root

# Log file group
LogFileGroup adm

# Spool directory
RequestRoot /var/spool/cups
EOF

  qvm-copy-to-vm "$CUSTOM_TEMPLATE" "$cups_files_conf"
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "mv /home/user/QubesIncoming/dom0/$(basename "$cups_files_conf") /etc/cups/cups-files.conf"
  rm -f "$cups_files_conf"

  # Create cupsd.conf using mktemp
  echo "Configuring CUPS daemon settings..."
  local cupsd_conf=$(mktemp)
  cat > "$cupsd_conf" << 'EOF'
LogLevel warn
MaxLogSize 0
Listen localhost:631
Listen /run/cups/cups.sock
Browsing Off
DefaultAuthType Basic
WebInterface No

# Default print settings
DefaultPageSize A4


  Order allow,deny
  Allow all


  Order allow,deny
  Allow all


  AuthType Default
  Require user @SYSTEM
  Order allow,deny
  Allow all


JobPrivateAccess default
JobPrivateValues default
SubscriptionPrivateAccess default
SubscriptionPrivateValues default
  
    Order allow,deny
    Allow all
  

EOF

  qvm-copy-to-vm "$CUSTOM_TEMPLATE" "$cupsd_conf"
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "mv /home/user/QubesIncoming/dom0/$(basename "$cupsd_conf") /etc/cups/cupsd.conf"
  rm -f "$cupsd_conf"

  # Directory permissions with correct ownership
  echo "Configuring CUPS permissions and ownership..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  chmod 644 /etc/cups/cupsd.conf /etc/cups/cups-files.conf
  mkdir -p /var/log/cups /var/cache/cups /var/spool/cups
  chown -R cups:lp /var/log/cups /var/cache/cups /var/spool/cups
  chown -R root:lp /etc/cups
  chmod 755 /var/log/cups /var/cache/cups /var/spool/cups

  # Ensure cups spool directory has correct permissions
  chown cups:lp /var/spool/cups
  chmod 755 /var/spool/cups
"

  # Configure udev rules for USB printer devices using mktemp
  echo "Configuring USB permissions..."
  local udev_rules=$(mktemp)
  cat > "$udev_rules" << 'EOF'
# HP USB printer permissions
SUBSYSTEM=="usb", ATTRS{idVendor}=="03f0", GROUP="lp", MODE="0664"
SUBSYSTEM=="usb", ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="*", GROUP="lp", MODE="0664"
EOF

  qvm-copy-to-vm "$CUSTOM_TEMPLATE" "$udev_rules"
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  mv /home/user/QubesIncoming/dom0/$(basename "$udev_rules") /etc/udev/rules.d/99-hp-usb-printer.rules
  udevadm control --reload-rules
  udevadm trigger
"
  rm -f "$udev_rules"

  # Configure PolicyKit for passwordless printer administration
  echo "Configuring PolicyKit..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "mkdir -p /etc/polkit-1/rules.d"

  # Create PolicyKit rules using mktemp
  local polkit_rules=$(mktemp)
  cat > "$polkit_rules" << 'EOF'
// Allow any action for qubes users and printer groups
polkit.addRule(function(action, subject) {
    if (subject.isInGroup("user") || subject.isInGroup("lp") || subject.user == "root") {
        return polkit.Result.YES;
    }
});
EOF

  qvm-copy-to-vm "$CUSTOM_TEMPLATE" "$polkit_rules"
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  mv /home/user/QubesIncoming/dom0/$(basename "$polkit_rules") /etc/polkit-1/rules.d/99-qubes-allow-all.rules
  chmod 644 /etc/polkit-1/rules.d/99-qubes-allow-all.rules
  rm -f /home/user/QubesIncoming/dom0/$(basename "$polkit_rules")
"
  rm -f "$polkit_rules"

  # Configure and start services
  echo "Starting printer services..."
  qvm-run -p -u root "$CUSTOM_TEMPLATE" "
  # Verify configuration
  echo 'Verifying CUPS configuration...'
  if cupsd -t; then
    echo 'CUPS configuration test PASSED'
  else
    echo 'CUPS configuration test FAILED, checking details...'
    cupsd -t 2>&1
    exit 1
  fi

  # Reset any failed services
  systemctl reset-failed cups cups.socket cups.path 2>/dev/null || true

  # Enable services
  systemctl enable cups.socket
  systemctl enable cups.path
  systemctl enable avahi-daemon
  systemctl enable dbus

  # Start services in correct order
  systemctl start dbus
  systemctl start avahi-daemon
  systemctl start cups.socket
  systemctl start cups.path
  sleep 3
  systemctl start cups
  sleep 3
  systemctl status cups --no-pager -l
"

  echo "Printer services configuration completed"
}

# Step 7: Create DVM template
create_dvm_template() {
  echo -e "\nStep 7: Creating DVM template..."
  qvm-shutdown --wait "$CUSTOM_TEMPLATE"

  if ! qvm-check "$DISP_TEMPLATE" 2>/dev/null; then
    qvm-create --template "$CUSTOM_TEMPLATE" --label red "$DISP_TEMPLATE"
  fi

  qvm-prefs "$DISP_TEMPLATE" template_for_dispvms True
  qvm-prefs "$DISP_TEMPLATE" include_in_backups false

  qvm-service "$DISP_TEMPLATE" cups on
  qvm-service "$DISP_TEMPLATE" avahi on
  echo -e "\nTemplate for disposables '$DISP_TEMPLATE' created successfully"
}

# Step 8: Create named disposable VM
create_named_disposable() {
  echo -e "\nStep 8: Creating named disposable VM..."

  if ! qvm-check "$NAMED_DISP_VM" 2>/dev/null; then
    qvm-create --template "$DISP_TEMPLATE" --class DispVM --label blue \
    --property netvm="" \
    --property vcpus=1 \
    --property memory=128 \
    --property maxmem=512 \
    --property autostart=False \
    --property include_in_backups=False \
    "$NAMED_DISP_VM"
  fi

  qvm-service "$NAMED_DISP_VM" cups on
  qvm-service "$NAMED_DISP_VM" avahi on

  echo -e "\nNamed disposable VM '$NAMED_DISP_VM' created successfully"
}

# Finalization
finalize() {
  echo -e "\nStep 9: Finalizing setup..."

  # Sync appmenus and shutdown
  qvm-start --skip-if-running "$CUSTOM_TEMPLATE"
  qvm-sync-appmenus "$CUSTOM_TEMPLATE"
  qvm-shutdown --wait "$CUSTOM_TEMPLATE"

  qvm-start --skip-if-running "$NAMED_DISP_VM"
  qvm-sync-appmenus "$NAMED_DISP_VM"
  qvm-shutdown --wait "$NAMED_DISP_VM"

  # Configure menu items
  qvm-features "$CUSTOM_TEMPLATE" menu-items "xfce4-terminal.desktop"
  qvm-features "$DISP_TEMPLATE" menu-items "xfce4-terminal.desktop"
  qvm-features "$NAMED_DISP_VM" menu-items "hplip.desktop thunar.desktop xfce4-terminal.desktop"
}

# Main execution
main() {
  echo "Starting HP USB Printer Setup"

  check_printer_attached
  create_base_template
  create_custom_template
  install_system_dependencies
  install_hplip_from_apt
  download_and_install_plugin
  setup_printer_services
  create_dvm_template
  create_named_disposable
  finalize

  echo -e "\nSetup completed successfully!"
}

main "$@"

debian-hplip-named-diposable.sh.log (12.9 KB)


TL;DR CUPS Permission and Ownership

Key Security Concepts

The Root Access in Qubes and Printer Context

  • Qubes RPC: direct root access via qvm-run -u root.
  • Passwordless sudo: qubes-core-agent-passwordless-root package allows passwordless sudo within the VM.
  • CUPS/HPLIP: require root privileges for installation and service configuration.
  • UDEV device controls: USB printers access.
  • PolicyKit: for specific GUI applications (HPLIP Toolbox).

Service Security Model

Principle of Least Privilege:

  • Configuration files owned by root but readable by lp group

Access Control:

  • Administrative tasks: Users in root group (via SystemGroup)
  • Printer operations: Users in lp group
  • Log access: Users in adm group

USB Device Access

UDEV Rules:

# HP USB printers get lp group ownership
SUBSYSTEM=="usb", ATTRS{idVendor}=="03f0", GROUP="lp", MODE="0664"
  • Effect: Any HP printer (vendor ID 03f0) becomes accessible to lp group members
  • Permissions: Read/write for owner and group, read-only for others

PolicyKit Integration

For specific GUI applications.

Rule:

// Allow passwordless printer administration for:
polkit.addRule(function(action, subject) {
  if (subject.isInGroup("user") ||  // Regular users
  subject.isInGroup("lp") ||  // Printer group members
  subject.user == "root") {   // System administrator
  return polkit.Result.YES;   // Grant permission
  }
});

User and Group Structure

Users:

  • cups (UID 999) - Dedicated CUPS service account
    • Created: useradd -r -c 'CUPS Printing Service' -s /bin/false -d /var/spool/cups cups
    • Purpose: Runs CUPS daemon with minimal privileges (CUPS security requirement)
  • user (UID 1000) - Regular user account
    • Added to lp group for printer administration
    • Be careful to keep the file ownership to root:lp.
  • root (UID 0) - System administrator
    • Has full access via SystemGroup root

Groups:

  • lp (GID 7) - Primary printing group
    • Members: cups, user, root
    • Purpose: Allows printer access and administration
  • root - System administrators group
  • adm - Log file access group

File and Directory Permissions

CUPS Configuration:

/etc/cups/
├── cupsd.conf         (root:lp, 644)
├── cups-files.conf   (root:lp, 644)
└── [other configs]     (root:lp, 644)

Runtime Directories:

/var/log/cups/         (cups:lp, 755)   - Log files
/var/cache/cups/      (cups:lp, 755)   - Temporary files
/var/spool/cups/      (cups:lp, 755)   - Print jobs spool
References

Note: you can extract and study the installer code without running it by using, e.g.,bash /tmp/hplip-$VERSION.run --noexec. It'll extract into a directory named /tmp/hplip-$VERSION


  [user@host ~]$ chmod +x /home/user/hplip-3.25.8.run
  [user@host ~]$ /home/user/hplip-3.25.8.run --help
Makeself version 2.4.0
 1) Getting help or info about /home/user/hplip-3.25.8.run :
  /home/user/hplip-3.25.8.run --help   Print this message
  /home/user/hplip-3.25.8.run --info   Print embedded info : title, default target directory, embedded script ...
  /home/user/hplip-3.25.8.run --lsm    Print embedded lsm entry (or no LSM)
  /home/user/hplip-3.25.8.run --list   Print the list of files in the archive
  /home/user/hplip-3.25.8.run --check  Checks integrity of the archive

 2) Running /home/user/hplip-3.25.8.run :
  /home/user/hplip-3.25.8.run [options] [--] [additional arguments to embedded script]
  with following options (in that order)
  --confirm             Ask before running embedded script
  --quiet		Do not print anything except error messages
  --accept              Accept the license
  --noexec              Do not run embedded script
  --keep                Do not erase target directory after running
			the embedded script
  --noprogress          Do not show the progress during the decompression
  --nox11               Do not spawn an xterm
  --nochown             Do not give the extracted files to the current user
  --nodiskspace         Do not check for available disk space
  --target dir          Extract directly to a target directory (absolute or relative)
                        This directory may undergo recursive chown (see --nochown).
  --tar arg1 [arg2 ...] Access the contents of the archive through the tar command
  --                    Following arguments will be passed to the embedded script