diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 33081f2..fdf28f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,30 +12,44 @@ lint: extends: .lint stage: tests -.mgmt-host-template: +.qubes-ansible-tests: stage: tests tags: - vm-kvm - after_script: - - ci/codecov-wrapper -F unittests - before_script: + + before_script: &install_qubes_ansible # install dependencies - sudo qubes-dom0-update -y - sudo qubes-dom0-update -y ansible python3-pytest python3-coverage perl-Digest-SHA # install from artifacts - find $CI_PROJECT_DIR/artifacts/repository/host-fc* -name '*.noarch.rpm' -exec sudo dnf install -y {} \+ - $CI_PROJECT_DIR/ci/install_ansible_vm.sh - script: - # run ansible's tests - - cd /usr/share/ansible && sudo coverage run --data-file=$CI_PROJECT_DIR/.coverage --include=plugins/modules/qubesos.py,plugins/connection/qubes.py -m pytest -vvv tests/qubes/ -r4.3:mgmt-host: - extends: .mgmt-host-template + after_script: + - ci/codecov-wrapper -F unittests + + variables: + VM_IMAGE: qubes_4.3_64bit_stable.qcow2 + needs: - r4.3:build:host-fc41 - r4.3:build:vm-fc42 - variables: - VM_IMAGE: qubes_4.3_64bit_stable.qcow2 + +r4.3:mgmt-host: + extends: .qubes-ansible-tests + script: + # run ansible's tests + - cd /usr/share/ansible && sudo coverage run --data-file=$CI_PROJECT_DIR/.coverage --include=plugins/modules/qubesos.py,plugins/connection/qubes.py -m pytest -vvv tests/qubes/ + +r4.3:mgmtvm: + extends: .qubes-ansible-tests + + before_script: + - *install_qubes_ansible + - $CI_PROJECT_DIR/ci/install_mgmtvm.sh + script: + # run ansible's tests + - qvm-run -p -u root mgmtvm 'cd /usr/share/ansible ; coverage run --data-file=/root/.coverage --include=plugins/modules/qubesos.py,plugins/connection/qubes.py -m pytest -vvv tests/qubes/' pages: stage: build @@ -53,3 +67,4 @@ pages: - antsibull-docs sphinx-init qubesos.* --use-current --dest-dir build/ - ANSIBLE_COLLECTIONS_PATH=$(pwd) ./build/build.sh - mv build/build/html/* public/ + diff --git a/.qubesbuilder b/.qubesbuilder index 9881782..4a8e3a3 100644 --- a/.qubesbuilder +++ b/.qubesbuilder @@ -1,7 +1,7 @@ host: rpm: build: - - rpm_spec/qubes-ansible.spec + - rpm_spec/qubes-ansible-host.spec vm: rpm: build: diff --git a/Makefile b/Makefile index 749db13..7d94805 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ VERSION := $(shell cat version) QUBE_COLLECTION_DIR := $(DESTDIR)/usr/share/ansible/collections/ansible_collections/qubesos -install-common: +_install-common: mkdir -p $(QUBE_COLLECTION_DIR)/core/plugins/connection mkdir -p $(QUBE_COLLECTION_DIR)/core/plugins/modules mkdir -p $(QUBE_COLLECTION_DIR)/core/plugins/module_utils @@ -17,7 +17,14 @@ install-common: install -m 644 plugins/modules/qubesos.py $(DESTDIR)/usr/share/ansible/plugins/modules/qubesos.py -install-dom0: +_install-dom0: + mkdir -p $(DESTDIR)/etc/qubes-rpc/ + mkdir -p $(DESTDIR)/usr/lib/qubes/qubes-rpc/ + install -m 755 qubes-rpc/qubes-ansible-manage-policies $(DESTDIR)/usr/lib/qubes/qubes-rpc/qubes-ansible-manage-policies + ln -s ../../usr/lib/qubes/qubes-rpc/qubes-ansible-manage-policies $(DESTDIR)/etc/qubes-rpc/ansible.CreateManagementPolicies + ln -s ../../usr/lib/qubes/qubes-rpc/qubes-ansible-manage-policies $(DESTDIR)/etc/qubes-rpc/ansible.RemoveManagementPolicies + +_install-security: mkdir -p $(DESTDIR)/usr/lib/qubes/ mkdir -p $(QUBE_COLLECTION_DIR)/security/plugins/callback mkdir -p $(QUBE_COLLECTION_DIR)/security/plugins/strategy @@ -32,13 +39,16 @@ install-dom0: ln -s ../../collections/ansible_collections/qubesos/security/plugins/strategy/qubes_proxy.py \ $(DESTDIR)/usr/share/ansible/plugins/strategy/qubes_proxy.py -install-tests: +_install-vm: + mkdir -p $(DESTDIR)/etc/qubes-rpc/ + install -m 755 qubes-rpc/qubes.AnsibleVM $(DESTDIR)/etc/qubes-rpc/qubes.AnsibleVM + +_install-tests: mkdir -p $(DESTDIR)/usr/share/ansible/tests/qubes install -m 644 tests/qubes/*.py $(DESTDIR)/usr/share/ansible/tests/qubes/ install -m 644 tests/*.cfg $(DESTDIR)/usr/share/ansible/tests/ -install-vm: - mkdir -p $(DESTDIR)/etc/qubes-rpc/ - install -m 755 qubes-rpc/qubes.AnsibleVM $(DESTDIR)/etc/qubes-rpc/qubes.AnsibleVM - -install-all: install-common install-dom0 install-tests install-vm +install-vm: _install-common _install-vm +install-vm-sec: install-vm _install-security +install-vm-all: install-vm-sec _install-tests +install-dom0: _install-common _install-security _install-dom0 _install-tests diff --git a/README.md b/README.md index 45b60b5..850df05 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,14 @@ will raise an error and interrupt Ansible execution. ## Installation -### AdminVM (dom0) +### Dom0 -Install the following package: ``qubes-ansible-dom0`` +Install the following package: ``qubes-ansible``. ### Management DVM The package ``qubes-ansible-vm`` (``qubes-ansible`` for Debian and Archlinux) must be installed -on templates used by your qubes management DVM (``default-mgmt-dvm`` by default). +in the templates used by your management DVM (``default-mgmt-dvm`` by default). ## Usage @@ -127,31 +127,96 @@ limitations: * Play recap will reflect the number of plays ran for each host instead of the number of tasks * Only plain text output is supported -## Management VM +## Management VM (advanced) -Usage of this module with a Management VM is not yet supported. However, the -following policies may be added if doing so: +You can use a dedicated qube to run your Ansible playbooks. Install the +package `qubes-ansible-admin`: it will deploy the `qubesos.core` collection and +all the `qubesos.security` plugins. -Append the following lines to `/etc/qubes/policy.d/include/admin-local-rwx`: +Then, you will need to write policies that fit your needs. + +First, you can add the following lines to `/etc/qubes/policy.d/include/admin-local-rwx`: ``` mgmtvm @tag:created-by-mgmtvm allow target=dom0 mgmtvm mgmtvm allow target=dom0 ``` -Append the following lines to `/etc/qubes/policy.d/include/admin-global-ro`: +And append the following lines to `/etc/qubes/policy.d/include/admin-global-ro`: ``` mgmtvm @adminvm allow target=dom0 mgmtvm @tag:created-by-mgmtvm allow target=dom0 mgmtvm mgmtvm allow target=dom0 ``` -Create a policy file at `/etc/qubes/policy.d/30-ansible.policy` with the -following content: +This lets your ManagementVM manage the qubes it creates. + +You may also want to allow your ManagementVM to read properties of sys vms and +several templates. For example, when setting a netvm to a qube, the module checks +if the target qube exits and provides network, which requires the mgmtvm to be +able to read the target qube properties: + +`/etc/qubes/policy.d/include/admin-global-ro`: +``` +mgmtvm sys-net allow target=dom0 +mgmtvm sys-firewall allow target=dom0 +mgmtvm sys-usb allow target=dom0 +mgmtvm debian-13-xfce allow target=dom0 +mgmtvm fedora-42-xfce allow target=dom0 +``` + +`/etc/qubes/policy.d/include/admin-local-ro`: +``` +mgmtvm sys-net allow target=dom0 +mgmtvm sys-firewall allow target=dom0 +mgmtvm sys-usb allow target=dom0 +mgmtvm debian-13-xfce allow target=dom0 +mgmtvm fedora-42-xfce allow target=dom0 ``` -admin.vm.Create.AppVM * mgmtvm dom0 allow -admin.vm.Create.StandaloneVM * mgmtvm dom0 allow -admin.vm.Create.TemplateVM * mgmtvm dom0 allow -admin.vm.Remove * mgmtvm @tag:created-by-mgmtvm allow target=dom0 + +Then, create a policy file at `/etc/qubes/policy.d/30-mgmtvm.policy` and adjust +its content to fit your security requirements: +``` +# ================= +# Qubes management +# ================= + +# The ManagementVM must be able to create new qubes and manage them +admin.vm.Create.AppVM * mgmtvm dom0 allow +admin.vm.Create.StandaloneVM * mgmtvm dom0 allow +admin.vm.Create.TemplateVM * mgmtvm dom0 allow + + +# You may want to allow to clone some template to create StandaloneVMs or new TemplateVMs +admin.vm.volume.CloneFrom * mgmtvm debian-13-xfce allow target=dom0 +admin.vm.volume.CloneFrom * mgmtvm fedora-42-xfce allow target=dom0 + +# And to remove created ones +admin.vm.Remove * mgmtvm @tag:created-by-mgmtvm allow target=dom0 + +# Get available devices (qubesos.core.host_devices_facs) +admin.vm.device.pci.Available * mgmtvm dom0 allow +admin.vm.device.block.Available * mgmtvm dom0 allow + +# You may want to assign devices to your qubes +admin.vm.device.pci.Assign * mgmtvm @tag:created-by-mgmtvm allow target=dom0 + +# ============= +# Proxy Plugin +# ============= + +# The proxy creates a dispvm from the management dvm of the managed qubes +# Copy these lines for each value of the management_dispvm preference used by your qubes. +admin.vm.Create.DispVM +default-mgmt-dvm mgmtvm dom0 allow +admin.vm.property.Get +label mgmtvm default-mgmt-dvm allow target=dom0 + +# Allow mgmtvm to call RPC managing dynamic policy creation allowing to run the +# connection plugin +ansible.CreateManagementPolicies * mgmtvm @tag:created-by-mgmtvm allow target=dom0 +ansible.RemoveManagementPolicies * mgmtvm @tag:created-by-mgmtvm allow target=dom0 + +# The proxy needs to copy and execute playbooks on DispVMs +qubes.AnsibleVM * mgmtvm @tag:created-by-mgmtvm allow +qubes.Filecopy * mgmtvm @tag:created-by-mgmtvm allow ``` ## Legacy module `qubesos` diff --git a/ansible_collections/qubesos/security/plugins/strategy/qubes_proxy.py b/ansible_collections/qubesos/security/plugins/strategy/qubes_proxy.py index 66ed9c3..34dc660 100644 --- a/ansible_collections/qubesos/security/plugins/strategy/qubes_proxy.py +++ b/ansible_collections/qubesos/security/plugins/strategy/qubes_proxy.py @@ -15,17 +15,14 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import asyncio -import fcntl import multiprocessing import os -import re import shutil +import subprocess import tarfile import tempfile import traceback -from contextlib import suppress from pathlib import Path import qubesadmin @@ -73,8 +70,10 @@ Path("/etc/qubes/policy.d/include/admin-local-rwx"), Path("/etc/qubes/policy.d/include/admin-global-ro"), ) -RPC_INCLUDE_POL_FILE = Path("/etc/qubes/policy.d/include/qubes-ansible") -RPC_ANSIBLE_POL_FILE = Path("/etc/qubes/policy.d/30-qubes-ansible.policy") + +QFILE_AGENT_PATH_DOM0 = "/usr/lib/qubes/qfile-dom0-agent" +QFILE_AGENT_PATH_VM = "/usr/lib/qubes/qfile-agent" + DISPVM_NAME_MAXLEN = 31 @@ -312,51 +311,33 @@ def _add_roles(self, play): role_path = Path(role.get_role_path()) shutil.copytree(role_path, dest_roles_path / role_path.name) - def _add_rpc_policies(self): - src = self.dispvm_mgmt_name - dst = self.vm.name - - while True: - with RPC_INCLUDE_POL_FILE.open("a+") as pol_file: - fcntl.lockf(pol_file.fileno(), fcntl.LOCK_EX) - try: - if os.fstat(pol_file.fileno()) != os.stat( - RPC_INCLUDE_POL_FILE - ): - continue - except FileNotFoundError: - continue - - pol_file.write(f"{src} {dst} allow target=dom0\n") - try: - shutil.chown(RPC_INCLUDE_POL_FILE, group="qubes") - except PermissionError: - pass - break - - while True: - with RPC_ANSIBLE_POL_FILE.open("a+") as pol_file: - fcntl.lockf(pol_file.fileno(), fcntl.LOCK_EX) - try: - if os.fstat(pol_file.fileno()) != os.stat( - RPC_ANSIBLE_POL_FILE - ): - continue - except FileNotFoundError: - continue - - pol_file.write( - f"qubes.Filecopy * {src} {dst} allow\n" - f"qubes.WaitForSession * {src} {dst} allow\n" - f"qubes.VMShell * {src} {dst} allow\n" - f"qubes.VMRootShell * {src} {dst} allow\n" - f"admin.vm.List * {src} dom0 allow\n" - ) - try: - shutil.chown(RPC_ANSIBLE_POL_FILE, group="qubes") - except PermissionError: - pass - break + def _add_rpc_policies(self, dispvm_name): + self._call_ansible_service_rpc( + "ansible.CreateManagementPolicies", dispvm_name + ) + + def _call_ansible_service_rpc(self, service_name, arg=None): + env = os.environ.copy() + service_with_arg = service_name + if arg: + service_with_arg += f"+{arg}" + # dom0 + if os.path.exists("/usr/bin/qrexec-client"): + env["QREXEC_REQUESTED_TARGET"] = self.vm.name + env["QREXEC_REMOTE_DOMAIN"] = "dom0" + env["QREXEC_SERVICE_FULL_NAME"] = service_with_arg + command = [f"/etc/qubes-rpc/{service_name}"] + if arg: + command.append(arg) + else: + command = [ + "/usr/bin/qrexec-client-vm", + "--", + self.vm.name, + service_with_arg, + ] + + return subprocess.check_output(command, env=env).decode() @staticmethod def _build_ansible_args(): @@ -399,45 +380,10 @@ def _get_first_play_yaml(path, start_line): "".join(playbook_file.readlines()[int(start_line) - 1 :]) )[0] - def _remove_rpc_policies(self): - src = self.dispvm_mgmt_name - dst = self.vm.name - - with RPC_INCLUDE_POL_FILE.open("a+") as pol_file: - fcntl.lockf(pol_file.fileno(), fcntl.LOCK_EX) - with suppress(FileNotFoundError): - os.stat(RPC_INCLUDE_POL_FILE) - pol_file.seek(0) - new_file_lines = [ - line - for line in pol_file.readlines() - if not re.match( - rf"^\s*{re.escape(src)}\s+{re.escape(dst)}\s+", - line, - ) - ] - pol_file.seek(0) - pol_file.truncate() - pol_file.write("".join(new_file_lines)) - pol_file.flush() - - with RPC_ANSIBLE_POL_FILE.open("a+") as pol_file: - fcntl.lockf(pol_file.fileno(), fcntl.LOCK_EX) - with suppress(FileNotFoundError): - pol_file.seek(0) - os.stat(RPC_ANSIBLE_POL_FILE) - new_file_lines = [ - line - for line in pol_file.readlines() - if not re.match( - rf"^\s*\S+\s+\S+\s+{re.escape(src)}\s+", - line, - ) - ] - pol_file.seek(0) - pol_file.truncate() - pol_file.write("".join(new_file_lines)) - pol_file.flush() + def _remove_rpc_policies(self, dispvm_name): + self._call_ansible_service_rpc( + "ansible.RemoveManagementPolicies", dispvm_name + ) def _start_mgmt_disp_vm(self): self.vvv("Lookup for dispvm_mgmt") @@ -471,7 +417,7 @@ def run(self): dispvm = self._start_mgmt_disp_vm() - self._add_rpc_policies() + self._add_rpc_policies(dispvm.name) self.temp_dir.mkdir() try: @@ -483,11 +429,15 @@ def run(self): ansible_args = self._build_ansible_args() self.vvv(f"Copying {tar_file_path} to {self.vm}") + if os.path.exists(QFILE_AGENT_PATH_DOM0): + localcmd = QFILE_AGENT_PATH_DOM0 + else: + localcmd = QFILE_AGENT_PATH_VM + + localcmd += f" {tar_file_path}" dispvm.run_service( "qubes.Filecopy", - localcmd="/usr/lib/qubes/qfile-dom0-agent {}".format( - tar_file_path - ), + localcmd=localcmd, ).wait() self.vvv(f"Running qubes.AnsibleVM on {self.vm}") @@ -515,29 +465,11 @@ def run(self): ) finally: - self._remove_rpc_policies() + self._remove_rpc_policies(dispvm.name) shutil.rmtree(self.temp_dir) if not self._dispvm_initially_running: self.vvv(f"Stopping {dispvm.name}") - dispvm.shutdown() - try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete( - asyncio.wait_for( - qubesadmin.events.utils.wait_for_domain_shutdown( - [dispvm] - ), - dispvm.shutdown_timeout, - ) - ) - - except asyncio.TimeoutError: - try: - display.warning(f" killing {dispvm.name}") - dispvm.kill() - except qubesadmin.exc.QubesVMNotStartedError: - pass + dispvm.kill() def _verbose(self, msg: str, level: int): getattr(display, "v" * level)(f"<{self.host_name}> {msg}") @@ -564,7 +496,6 @@ def vvvvvv(self, msg): class StrategyModule(LinearStrategyModule): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._setup_rpc_policies() def _new_play_iterator_with_hosts(self, iterator, play_context, hosts): new_play = iterator._play.copy() @@ -578,17 +509,6 @@ def _new_play_iterator_with_hosts(self, iterator, play_context, hosts): start_at_done=self._tqm._start_at_done, ) - @staticmethod - def _setup_rpc_policies(): - Path(RPC_INCLUDE_POL_FILE).touch() - for policy_file in RPC_SYS_POLICY_FILES: - policy_lines = [ - line.strip() for line in policy_file.read_text().split("\n") - ] - if "!include include/qubes-ansible" not in policy_lines: - with policy_file.open("a") as policy_fd: - policy_fd.write("!include include/qubes-ansible\n") - @staticmethod def collect_error(error): display.display(f"[ERROR]: {str(error)}", "red") diff --git a/archlinux/PKGBUILD.in b/archlinux/PKGBUILD.in index 61c4596..0528538 100644 --- a/archlinux/PKGBUILD.in +++ b/archlinux/PKGBUILD.in @@ -13,6 +13,5 @@ sha256sums=(SKIP) package() { cd "${_pkgnvr}" - make install-common DESTDIR="$pkgdir/" make install-vm DESTDIR="$pkgdir/" } diff --git a/ci/install_ansible_vm.sh b/ci/install_ansible_vm.sh index 00c2de0..5c8231b 100755 --- a/ci/install_ansible_vm.sh +++ b/ci/install_ansible_vm.sh @@ -16,7 +16,7 @@ repo_dir="artifacts/repository/vm-fc${fedora_ver}" [[ -d "$repo_dir" ]] || err_ex "'$repo_dir' not found" find $repo_dir -name 'qubes-ansible-*.noarch.rpm' \ - -not -name 'qubes-ansible-dom0*.noarch.rpm' \ + -not -name 'qubes-ansible-admin*.noarch.rpm' \ -not -name 'qubes-ansible-tests*.noarch.rpm' \ -exec qvm-copy-to-vm "$default_mgmt_dispvm_template" "{}" \; diff --git a/ci/install_mgmtvm.sh b/ci/install_mgmtvm.sh new file mode 100755 index 0000000..afe3124 --- /dev/null +++ b/ci/install_mgmtvm.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +set -e + +function err_ex() { + echo "$1" >&2 + exit 1 +} + +default_mgmt_dispvm="$(qubes-prefs management_dispvm)" +default_mgmt_dispvm_template="$(qvm-prefs "$default_mgmt_dispvm" template)" +[[ "$default_mgmt_dispvm_template" == fedora-* ]] || err_ex "unsupported template $default_mgmt_dispvm_template" +fedora_ver="$(echo "$default_mgmt_dispvm_template" | cut -d- -f2)" + +repo_dir="artifacts/repository/vm-fc${fedora_ver}" +[[ -d "$repo_dir" ]] || err_ex "'$repo_dir' not found" + +qvm-create --class AppVM --property maxmem=800 --label black --template "$default_mgmt_dispvm_template" mgmtvm + +find $repo_dir -name 'qubes-ansible-*.noarch.rpm' \ + -not -name 'qubes-ansible-vm*.noarch.rpm' \ + -exec qvm-copy-to-vm mgmtvm "{}" \; + + +fedora_ver="$(echo "$default_mgmt_dispvm_template" | cut -d- -f2)" + +repo_dir="artifacts/repository/vm-fc${fedora_ver}" +[[ -d "$repo_dir" ]] || err_ex "'$repo_dir' not found" + +qvm-run -p -u root --no-gui mgmtvm "dnf update -y" +qvm-run -p -u root --no-gui mgmtvm "dnf install -y python3-coverage /home/user/QubesIncoming/dom0/*.rpm" + +cat << EOF >> /etc/qubes/policy.d/include/admin-local-rwx +mgmtvm @tag:created-by-mgmtvm allow target=dom0 +mgmtvm mgmtvm allow target=dom0 +EOF + +cat << EOF >> /etc/qubes/policy.d/include/admin-global-ro +mgmtvm @adminvm allow target=dom0 +mgmtvm @tag:created-by-mgmtvm allow target=dom0 +mgmtvm mgmtvm allow target=dom0 +mgmtvm sys-net allow target=dom0 +mgmtvm sys-firewall allow target=dom0 +mgmtvm sys-usb allow target=dom0 +mgmtvm fedora-42-xfce allow target=dom0 +mgmtvm debian-13-xfce allow target=dom0 +EOF + +cat << EOF >> /etc/qubes/policy.d/include/admin-local-ro +mgmtvm sys-net allow target=dom0 +mgmtvm sys-firewall allow target=dom0 +mgmtvm sys-usb allow target=dom0 +mgmtvm fedora-42-xfce allow target=dom0 +mgmtvm debian-13-xfce allow target=dom0 +EOF + +cat << EOF > /etc/qubes/policy.d/30-mgmtvm.policy +# ================= +# Qubes management +# ================= + +# The ManagementVM must be able to create new qubes and manage them +admin.vm.Create.AppVM * mgmtvm dom0 allow +admin.vm.Create.StandaloneVM * mgmtvm dom0 allow +admin.vm.Create.TemplateVM * mgmtvm dom0 allow + +# You may want to allow the ManagementVM to clone some templates to create StandaloneVMs or new TemplateVMs +admin.vm.volume.CloneFrom * mgmtvm debian-13-xfce allow target=dom0 +admin.vm.volume.CloneFrom * mgmtvm fedora-42-xfce allow target=dom0 + +# And to remove created ones +admin.vm.Remove * mgmtvm @tag:created-by-mgmtvm allow target=dom0 + +# Get available devices +admin.vm.device.pci.Available * mgmtvm dom0 allow +admin.vm.device.block.Available * mgmtvm dom0 allow + + +# You may want to assign devices to your qubes +admin.vm.device.pci.Assign * mgmtvm @tag:created-by-mgmtvm allow target=dom0 + +# ============= +# Proxy Plugin +# ============= + +# The proxy creates a dispvm from the management dvm of the managed qubes +# Copy these lines for each value of the management_dispvm preference used by your qubes. +admin.vm.Create.DispVM +default-mgmt-dvm mgmtvm dom0 allow +admin.vm.property.Get +label mgmtvm default-mgmt-dvm allow target=dom0 + +# Allow mgmtvm to call RPC managing dynamic policy creation allowing to run the +# connection plugin +ansible.CreateManagementPolicies * mgmtvm @tag:created-by-mgmtvm allow target=dom0 +ansible.RemoveManagementPolicies * mgmtvm @tag:created-by-mgmtvm allow target=dom0 + +# The proxy needs to copy and execute playbooks on DispVMs +qubes.AnsibleVM * mgmtvm @tag:created-by-mgmtvm allow +qubes.Filecopy * mgmtvm @tag:created-by-mgmtvm allow + +# For tests that are using linear strategy instead of qubes proxy +qubes.VMShell * mgmtvm @tag:created-by-mgmtvm allow +qubes.VMRootShell * mgmtvm @tag:created-by-mgmtvm allow + +# Some tests need to access dom0 properties +admin.vm.property.Get * mgmtvm dom0 allow +EOF diff --git a/debian/changelog b/debian/changelog index 78c3ce3..acf3994 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +qubes-ansible (1.4.1-1) unstable; urgency=medium + + * Rename source package from qubes-ansible-vm to qubes-ansible + * ManagementVM support + + -- Guillaume Chinal Wed, 06 May 2026 00:00:00 +0200 + qubes-ansible-vm (1.4.0-1) unstable; urgency=medium [ Guillaume Chinal ] diff --git a/debian/control b/debian/control index 8801d4b..f62605d 100644 --- a/debian/control +++ b/debian/control @@ -1,4 +1,4 @@ -Source: qubes-ansible-vm +Source: qubes-ansible Section: admin Priority: optional Maintainer: Marek Marczykowski-Górecki @@ -6,11 +6,36 @@ Standards-Version: 4.7.0 Build-Depends: debhelper-compat (= 13) Homepage: https://www.qubes-os.org/ -Package: qubes-ansible-vm +Package: qubes-ansible Architecture: any Depends: ansible, qubes-core-admin-client, ${python3:Depends}, ${misc:Depends} +Recommends: qubes-ansible-vm +Breaks: qubes-ansible-vm (<< 1.4.1) +Replaces: qubes-ansible-vm (<< 1.4.1) +Description: Ansible plugins and collection to manage QubesOS + This package provides the Ansible connection plugin and the qubesos.core + collection to manage QubesOS virtual machines. + +Package: qubes-ansible-vm +Architecture: all +Depends: + qubes-ansible, + ${misc:Depends} Description: qubes-ansible RPC script to run Ansible on a disposable VM + +Package: qubes-ansible-admin +Architecture: all +Depends: + qubes-ansible, + ${misc:Depends} +Description: Security plugins to manage QubesOS from a ManagementVM + This package contains the qubesos.security collection allowing to run Ansible + playbooks on a ManagementVM securely by proxifying plays execution on + disposables. + . + Installing this package edits /etc/ansible/ansible.cfg to set qubes_proxy as + the default strategy. diff --git a/debian/install b/debian/install deleted file mode 100644 index c5a0c2a..0000000 --- a/debian/install +++ /dev/null @@ -1,13 +0,0 @@ -/etc/qubes-rpc/qubes.AnsibleVM -/usr/share/ansible/plugins/modules/qubesos.py -/usr/share/ansible/plugins/connection/qubes.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/connection/qubes.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/module_utils/qubes_helper.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/module_utils/qubes_module_command.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/module_utils/qubes_module_host_devices_facts.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/module_utils/qubes_module_qube.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/modules/command.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/modules/host_devices_facts.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/modules/qube.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/modules/qube_facts.py -/usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/modules/qubes_dom0_update.py diff --git a/debian/qubes-ansible-admin.install b/debian/qubes-ansible-admin.install new file mode 100644 index 0000000..5432f4c --- /dev/null +++ b/debian/qubes-ansible-admin.install @@ -0,0 +1,5 @@ +usr/share/ansible/plugins/callback/qubesos_strategy_guard.py +usr/share/ansible/plugins/strategy/qubes_proxy.py +usr/lib/qubes/update-ansible-default-strategy +usr/share/ansible/collections/ansible_collections/qubesos/security/plugins/callback/qubesos_strategy_guard.py +usr/share/ansible/collections/ansible_collections/qubesos/security/plugins/strategy/qubes_proxy.py diff --git a/debian/qubes-ansible-admin.postinst b/debian/qubes-ansible-admin.postinst new file mode 100644 index 0000000..b18f7b1 --- /dev/null +++ b/debian/qubes-ansible-admin.postinst @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +case "$1" in + configure|triggered) + /usr/lib/qubes/update-ansible-default-strategy + ;; +esac + +#DEBHELPER# diff --git a/debian/qubes-ansible-admin.triggers b/debian/qubes-ansible-admin.triggers new file mode 100644 index 0000000..6df2ca1 --- /dev/null +++ b/debian/qubes-ansible-admin.triggers @@ -0,0 +1 @@ +interest-noawait /usr/bin/ansible diff --git a/debian/qubes-ansible-vm.install b/debian/qubes-ansible-vm.install new file mode 100644 index 0000000..57cec83 --- /dev/null +++ b/debian/qubes-ansible-vm.install @@ -0,0 +1 @@ +etc/qubes-rpc/qubes.AnsibleVM diff --git a/debian/qubes-ansible.install b/debian/qubes-ansible.install new file mode 100644 index 0000000..6939af3 --- /dev/null +++ b/debian/qubes-ansible.install @@ -0,0 +1,5 @@ +usr/share/ansible/plugins/modules/qubesos.py +usr/share/ansible/plugins/connection/qubes.py +usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/connection/qubes.py +usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/module_utils/*.py +usr/share/ansible/collections/ansible_collections/qubesos/core/plugins/modules/*.py diff --git a/debian/rules b/debian/rules index 6dc8473..a168c24 100644 --- a/debian/rules +++ b/debian/rules @@ -7,5 +7,4 @@ export DESTDIR=$(shell pwd)/debian/tmp dh $@ override_dh_auto_install: - make install-common - make install-vm + make install-vm-sec diff --git a/qubes-rpc/qubes-ansible-manage-policies b/qubes-rpc/qubes-ansible-manage-policies new file mode 100755 index 0000000..de4e820 --- /dev/null +++ b/qubes-rpc/qubes-ansible-manage-policies @@ -0,0 +1,103 @@ +#!/usr/bin/python3 +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2026 Guillaume Chinal +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import fcntl +import os +import qubesadmin +import re +import sys + + +QUBES_ANSIBLE_POLICY_FILE = "/run/qubes/policy.d/30-qubes-ansible.policy" + + +def check_tag(dispvm): + app = qubesadmin.Qubes() + remote_domain = os.environ['QREXEC_REMOTE_DOMAIN'] + if remote_domain == "dom0": + return + expected_tag = f"created-by-{remote_domain}" + if not expected_tag in app.domains[dispvm].tags: + print( + f"${remote_domain} is not allowed to grant permissions to {dispvm}", + file=sys.stderr + ) + exit(1) + + +def create_policies(target_mgmt_dispvm, target): + with open(QUBES_ANSIBLE_POLICY_FILE, "a+") as pol_file: + fcntl.lockf(pol_file.fileno(), fcntl.LOCK_EX) + pol_file.write( + f"qubes.Filecopy * {target_mgmt_dispvm} {target} allow\n" + f"qubes.VMRootShell * {target_mgmt_dispvm} {target} allow\n" + f"qubes.VMShell * {target_mgmt_dispvm} {target} allow\n" + f"qubes.WaitForSession * {target_mgmt_dispvm} {target} allow\n" + f"admin.vm.CurrentState * {target_mgmt_dispvm} {target} allow target=dom0\n" + f"admin.vm.List * {target_mgmt_dispvm} {target} allow target=dom0\n" + f"admin.vm.List * {target_mgmt_dispvm} dom0 allow\n" + ) + pol_file.flush() + os.fsync(pol_file.fileno()) + + +def remove_policies(target_mgmt_dispvm): + with open(QUBES_ANSIBLE_POLICY_FILE, "a+") as pol_file: + fcntl.lockf(pol_file.fileno(), fcntl.LOCK_EX) + pol_file.seek(0) + new_file_lines = [ + line + for line in pol_file.readlines() + if not re.match( + rf"^\s*\S+\s+\S+\s+{re.escape(target_mgmt_dispvm)}\s+", + line, + ) + ] + pol_file.seek(0) + pol_file.truncate() + pol_file.write("".join(new_file_lines)) + pol_file.flush() + os.fsync(pol_file.fileno()) + + +def main(argv): + if len(argv) != 2: + print("1 argument is expected", file=sys.stderr) + exit(1) + + target = os.environ["QREXEC_REQUESTED_TARGET"] + service = os.environ["QREXEC_SERVICE_FULL_NAME"] + target_mgmt_dispvm = sys.argv[1].strip() + + check_tag(target_mgmt_dispvm) + + if service.startswith("ansible.CreateManagementPolicies"): + create_policies(target_mgmt_dispvm, target) + + elif service.startswith("ansible.RemoveManagementPolicies"): + remove_policies(target_mgmt_dispvm) + + else: + exit(1) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/qubes-rpc/qubes.AnsibleVM b/qubes-rpc/qubes.AnsibleVM old mode 100644 new mode 100755 index dfd0554..6b28f9c --- a/qubes-rpc/qubes.AnsibleVM +++ b/qubes-rpc/qubes.AnsibleVM @@ -15,7 +15,7 @@ temp_dir="$(mktemp -d)" # shellcheck disable=SC2064 trap "rm -rf '$temp_dir'" EXIT -tar_file_path="/home/user/QubesIncoming/dom0/${tar_file}" +tar_file_path="/home/user/QubesIncoming/${QREXEC_REMOTE_DOMAIN}/${tar_file}" [ -f "$tar_file_path" ] || exit 1 tar -C "$temp_dir" -xf "$tar_file_path" diff --git a/rpm_spec/qubes-ansible-host.spec.in b/rpm_spec/qubes-ansible-host.spec.in new file mode 100644 index 0000000..0896e0d --- /dev/null +++ b/rpm_spec/qubes-ansible-host.spec.in @@ -0,0 +1,73 @@ +%define qubes_collection_dir %{_datadir}/ansible/collections/ansible_collections/qubesos + +Name: qubes-ansible +Version: @VERSION@ +Release: @REL@%{?dist} +Summary: QubesOS official Ansible plugins to manage your QubesOS securely. +License: GPLv3+ +URL: https://github.com/QubesOS/qubes-ansible +BuildArch: noarch + +Source0: %{name}-%{version}.tar.gz + +Requires: ansible +Requires: qubes-core-admin-client +Obsoletes: qubes-ansible-dom0 < 1.4.1 +Provides: qubes-ansible-dom0 + +BuildRequires: make + +%description +This package provides several Ansible plugins to manage your QubesOS system. +Documentation is available at https://qubesos.gitlab.io/qubesos. + + +%package tests +Summary: Tests for the module and the connection +Requires: %{name} +Requires: python3-pytest + +%description tests +Tests for the module and the connection. + + +%prep +%autosetup + + +%install +make DESTDIR=$RPM_BUILD_ROOT install-dom0 + +%post +/usr/lib/qubes/update-ansible-default-strategy + +%triggerin -- ansible +/usr/lib/qubes/update-ansible-default-strategy + + +%files +%doc README.md LICENSE EXAMPLES.md +%{_datadir}/ansible/plugins/callback/qubesos_strategy_guard.py +%{_datadir}/ansible/plugins/connection/qubes.py +%{_datadir}/ansible/plugins/modules/qubesos.py +%{_datadir}/ansible/plugins/strategy/qubes_proxy.py + +%{qubes_collection_dir}/core/plugins/connection/qubes.py +%{qubes_collection_dir}/core/plugins/module_utils/*.py +%{qubes_collection_dir}/core/plugins/modules/*.py +%{qubes_collection_dir}/security/plugins/callback/qubesos_strategy_guard.py +%{qubes_collection_dir}/security/plugins/strategy/qubes_proxy.py + +%{_sysconfdir}/qubes-rpc/ansible.CreateManagementPolicies +%{_sysconfdir}/qubes-rpc/ansible.RemoveManagementPolicies +/usr/lib/qubes/qubes-rpc/qubes-ansible-manage-policies + +/usr/lib/qubes/update-ansible-default-strategy + +%files tests +%{_datadir}/ansible/tests/qubes/*.py +%{_datadir}/ansible/tests/*.cfg + + +%changelog +@CHANGELOG@ diff --git a/rpm_spec/qubes-ansible.spec.in b/rpm_spec/qubes-ansible.spec.in index dfa2442..afbf376 100644 --- a/rpm_spec/qubes-ansible.spec.in +++ b/rpm_spec/qubes-ansible.spec.in @@ -16,19 +16,18 @@ Requires: qubes-core-admin-client BuildRequires: make %description -qubes-ansible provides an Ansible connection plugin ("qubes") and an Ansible module ("qubesos") -to manage QubesOS virtual machines. The files are installed into the Ansible module directory so that -Ansible can automatically discover and use them. This package is intended to be installed in dom0 -(or a management qube). +This package provides several Ansible plugins to manage your QubesOS system. -%package dom0 -Summary: qubes_proxy ansible strategy to run ansible on dom0 securely -Requires: %{name} +%package admin +Obsoletes: qubes-ansible-dom0 < 1.4.1 +Provides: qubes-ansible-dom0 +Summary: Provides security plugins to manage your qubes without exposing your VM. +Requires: %{name} -%description dom0 -This package contains the qubes_proxy ansible strategy plugin allowing to run -Ansible playbooks on dom0 securely by proxifying plays execution on disposables. +%description admin +This package contains the qubesos.security collection allowing to run Ansible +playbooks on a ManagementVM securely by proxifying plays execution on disposables. Note that this package edits /etc/ansible/ansible.cfg to set qubes_proxy as the default strategy. @@ -55,12 +54,12 @@ qubes-ansible RPC script to run ansible on a disposable VM %install -make DESTDIR=$RPM_BUILD_ROOT install-all +make DESTDIR=$RPM_BUILD_ROOT install-vm-all -%post dom0 +%post admin /usr/lib/qubes/update-ansible-default-strategy -%triggerin dom0 -- ansible +%triggerin admin -- ansible /usr/lib/qubes/update-ansible-default-strategy @@ -72,7 +71,7 @@ make DESTDIR=$RPM_BUILD_ROOT install-all %{qubes_collection_dir}/core/plugins/module_utils/*.py %{qubes_collection_dir}/core/plugins/modules/*.py -%files dom0 +%files admin %{_datadir}/ansible/plugins/callback/qubesos_strategy_guard.py %{_datadir}/ansible/plugins/strategy/qubes_proxy.py /usr/lib/qubes/update-ansible-default-strategy diff --git a/tests/qubes/test_cli.py b/tests/qubes/test_cli.py index 6bf9e03..765e1f5 100644 --- a/tests/qubes/test_cli.py +++ b/tests/qubes/test_cli.py @@ -365,7 +365,7 @@ def test_ansible_doc_qubesos_module(): # Should contain expected module information assert ( - "> QUBESOS" in result.stdout + "QUBESOS" in result.stdout.upper() ), "Documentation should mention the module name" diff --git a/tests/qubes/test_legacy_cli.py b/tests/qubes/test_legacy_cli.py index 3cfa521..dde5253 100644 --- a/tests/qubes/test_legacy_cli.py +++ b/tests/qubes/test_legacy_cli.py @@ -368,7 +368,7 @@ def test_ansible_doc_qubesos_module(): # Should contain expected module information assert ( - "> QUBESOS" in result.stdout + "QUBESOS" in result.stdout.upper() ), "Documentation should mention the module name" diff --git a/update-ansible-default-strategy b/update-ansible-default-strategy index 635cdc6..a9b9c24 100755 --- a/update-ansible-default-strategy +++ b/update-ansible-default-strategy @@ -6,6 +6,7 @@ import shutil from pathlib import Path CONFIG_FILE = Path("/etc/ansible/ansible.cfg") +CONFIG_FILE.parent.mkdir(exist_ok=True) CONFIG_FILE.touch() c = configparser.ConfigParser(allow_no_value=True) c.read(CONFIG_FILE)