From 09e6885e1732a7a6968ed27f88811058885b0fed Mon Sep 17 00:00:00 2001 From: Simon Gaiser Date: Thu, 11 Jun 2026 16:03:29 +0200 Subject: [PATCH] Implement optional admin authorization via qrexec See included README for details. Issue: QubesOS/qubes-issues#2695 --- Makefile | 2 + archlinux/PKGBUILD.in | 2 +- ...agent-passwordless-root.displace-extension | 1 - ...qubes-core-agent-passwordless-root.install | 8 +- ...ubes-core-agent-passwordless-root.postinst | 12 + debian/rules | 3 +- passwordless-root/.gitignore | 2 + passwordless-root/75-qubes-admin-authz.preset | 4 + passwordless-root/Makefile | 55 +++- passwordless-root/README.md | 88 ++++++ passwordless-root/admin-authzd.conf | 4 + .../authselect/local/password-auth | 33 ++ .../authselect/local/system-auth | 34 ++ .../authselect/nis/password-auth | 33 ++ passwordless-root/authselect/nis/system-auth | 34 ++ .../authselect/sssd/password-auth | 45 +++ passwordless-root/authselect/sssd/system-auth | 52 ++++ .../authselect/winbind/password-auth | 41 +++ .../authselect/winbind/system-auth | 42 +++ .../pam-configs-qubes-admin-authz | 7 + passwordless-root/pam-configs_su.qubes | 6 - passwordless-root/pam.d_su.qubes | 21 -- passwordless-root/pam_qubes_admin_authz.c | 235 ++++++++++++++ .../polkit-1-qubes-allow-all.rules | 2 - passwordless-root/qubes-admin-authz-common.h | 3 + passwordless-root/qubes-admin-authzd.c | 290 ++++++++++++++++++ passwordless-root/qubes-admin-authzd.service | 13 + passwordless-root/qubes.sudoers | 2 +- rpm_spec/core-agent.spec.in | 50 ++- vm-systemd/qubes-sysinit.sh | 7 + 30 files changed, 1076 insertions(+), 55 deletions(-) delete mode 100644 debian/qubes-core-agent-passwordless-root.displace-extension create mode 100644 passwordless-root/.gitignore create mode 100644 passwordless-root/75-qubes-admin-authz.preset create mode 100644 passwordless-root/README.md create mode 100644 passwordless-root/admin-authzd.conf create mode 100644 passwordless-root/authselect/local/password-auth create mode 100644 passwordless-root/authselect/local/system-auth create mode 100644 passwordless-root/authselect/nis/password-auth create mode 100644 passwordless-root/authselect/nis/system-auth create mode 100644 passwordless-root/authselect/sssd/password-auth create mode 100644 passwordless-root/authselect/sssd/system-auth create mode 100644 passwordless-root/authselect/winbind/password-auth create mode 100644 passwordless-root/authselect/winbind/system-auth create mode 100644 passwordless-root/pam-configs-qubes-admin-authz delete mode 100644 passwordless-root/pam-configs_su.qubes delete mode 100644 passwordless-root/pam.d_su.qubes create mode 100644 passwordless-root/pam_qubes_admin_authz.c delete mode 100644 passwordless-root/polkit-1-qubes-allow-all.rules create mode 100644 passwordless-root/qubes-admin-authz-common.h create mode 100644 passwordless-root/qubes-admin-authzd.c create mode 100644 passwordless-root/qubes-admin-authzd.service diff --git a/Makefile b/Makefile index d601285f..b1e4e584 100644 --- a/Makefile +++ b/Makefile @@ -24,11 +24,13 @@ all: ifeq ($(ENABLE_SELINUX),1) $(MAKE) -C selinux -f /usr/share/selinux/devel/Makefile -- $(selinux_policies) endif + $(MAKE) -C passwordless-root clean: make -C misc clean make -C qubes-rpc clean make -C doc clean + make -C passwordless-root clean rm -rf qubesagent/*.pyc qubesagent/__pycache__ rm -rf test-packages/__pycache__ rm -rf test-packages/qubesagent.egg-info diff --git a/archlinux/PKGBUILD.in b/archlinux/PKGBUILD.in index 9b93af20..a0d7ac0f 100644 --- a/archlinux/PKGBUILD.in +++ b/archlinux/PKGBUILD.in @@ -38,7 +38,7 @@ build() { # Fix for archlinux sbindir sed 's:/usr/sbin/ntpdate:/usr/bin/ntpdate:g' -i qubes-rpc/sync-ntp-clock - for dir in qubes-rpc misc; do + for dir in qubes-rpc misc passwordless-root; do make -C "$dir" VERSION=${pkgver} done } diff --git a/debian/qubes-core-agent-passwordless-root.displace-extension b/debian/qubes-core-agent-passwordless-root.displace-extension deleted file mode 100644 index 7ff75d67..00000000 --- a/debian/qubes-core-agent-passwordless-root.displace-extension +++ /dev/null @@ -1 +0,0 @@ -.qubes diff --git a/debian/qubes-core-agent-passwordless-root.install b/debian/qubes-core-agent-passwordless-root.install index e261ea67..6a58809e 100644 --- a/debian/qubes-core-agent-passwordless-root.install +++ b/debian/qubes-core-agent-passwordless-root.install @@ -1,3 +1,7 @@ -etc/polkit-1/rules.d/00-qubes-allow-all.rules +etc/qubes/admin-authzd.conf etc/sudoers.d/qubes -usr/share/pam-configs/su.qubes +usr/share/pam-configs/qubes-admin-authz +usr/bin/qubes-admin-authzd +usr/lib/*/security/pam_qubes_admin_authz.so +lib/systemd/system/qubes-admin-authzd.service +usr/share/doc/qubes-core-agent-passwordless-root/README.md diff --git a/debian/qubes-core-agent-passwordless-root.postinst b/debian/qubes-core-agent-passwordless-root.postinst index 04562d94..f8c04f40 100644 --- a/debian/qubes-core-agent-passwordless-root.postinst +++ b/debian/qubes-core-agent-passwordless-root.postinst @@ -26,6 +26,18 @@ pam-auth-update --package #DEBHELPER# +# If systemd is running try starting qubes-admin-authzd even if the user +# blocked the normal start by dh (above) via policy-rc.d. Not running it will +# break passwordless auth, so this is most likely not what the user wanted. But +# if they really want to they still can mask the service. + +if [ -e /run/systemd/system ] && + ! systemctl is-active --quiet qubes-admin-authzd.service && + systemctl is-enabled --quiet qubes-admin-authzd.service +then + systemctl start qubes-admin-authzd.service || true +fi + exit 0 # vim: set ts=4 sw=4 sts=4 et : diff --git a/debian/rules b/debian/rules index ae54a824..329da2e2 100755 --- a/debian/rules +++ b/debian/rules @@ -37,7 +37,8 @@ override_dh_fixperms: dh_fixperms -a -Xqfile-unpacker override_dh_systemd_start: - dh_systemd_start --no-restart-on-upgrade + dh_systemd_start --package=qubes-core-agent-passwordless-root --restart-after-upgrade + dh_systemd_start --remaining-packages --no-restart-after-upgrade override_dh_install: if [ "$(DISTRIBUTION)" = "Ubuntu" ]; then \ diff --git a/passwordless-root/.gitignore b/passwordless-root/.gitignore new file mode 100644 index 00000000..30dafbda --- /dev/null +++ b/passwordless-root/.gitignore @@ -0,0 +1,2 @@ +pam_qubes_admin_authz.so +qubes-admin-authzd diff --git a/passwordless-root/75-qubes-admin-authz.preset b/passwordless-root/75-qubes-admin-authz.preset new file mode 100644 index 00000000..043f6f25 --- /dev/null +++ b/passwordless-root/75-qubes-admin-authz.preset @@ -0,0 +1,4 @@ +# This will be re-preset on package upgrade. If you really want you can +# override this with a higher-priority preset file but that is probably a bad +# idea since it will break the pam module. +enable qubes-admin-authzd.service diff --git a/passwordless-root/Makefile b/passwordless-root/Makefile index bcbc7ddd..1b431fee 100644 --- a/passwordless-root/Makefile +++ b/passwordless-root/Makefile @@ -1,10 +1,28 @@ +BINDIR ?= /usr/bin SYSCONFDIR ?= /etc SUDOERSDIR = $(SYSCONFDIR)/sudoers.d -POLKIT1DIR = $(SYSCONFDIR)/polkit-1 PAMDIR = $(SYSCONFDIR)/pam.d PAMCONFIGSDIR = /usr/share/pam-configs/ +SYSLIBDIR ?= /lib -.PHONY: install install-debian install-rh +ifneq ($(DEB_HOST_MULTIARCH),) + PAM_MOD_DIR = /usr/lib/$(DEB_HOST_MULTIARCH)/security +else + PAM_MOD_DIR = /usr/lib64/security +endif + +.PHONY: install install-debian install-rh all clean + +all: qubes-admin-authzd pam_qubes_admin_authz.so + +clean: + rm -f qubes-admin-authzd pam_qubes_admin_authz.so + +qubes-admin-authzd: qubes-admin-authzd.c qubes-admin-authz-common.h + gcc -O2 -Wall -Wextra -Werror -fPIC -pie $(CFLAGS) $(LDFLAGS) $< -o $@ -lqubesdb + +pam_qubes_admin_authz.so: pam_qubes_admin_authz.c qubes-admin-authz-common.h + gcc -O2 -Wall -Wextra -Werror -fPIC -pie -shared $(CFLAGS) $(LDFLAGS) $< -o $@ -lpam install: install -d -m 0750 $(DESTDIR)$(SUDOERSDIR) @@ -14,11 +32,36 @@ install: sed -E '/^[^#]/s/\<(ROLE|TYPE)=[A-Za-z0-9_]+[[:space:]]+//g' qubes.sudoers | \ install -D -m 0440 /dev/stdin $(DESTDIR)$(SUDOERSDIR)/qubes; \ fi - install -d -m 0750 $(DESTDIR)$(POLKIT1DIR)/rules.d - install -D -m 0644 polkit-1-qubes-allow-all.rules $(DESTDIR)$(POLKIT1DIR)/rules.d/00-qubes-allow-all.rules + install -D -t $(DESTDIR)$(PAM_MOD_DIR) pam_qubes_admin_authz.so + install -D -t $(DESTDIR)$(BINDIR) qubes-admin-authzd + install -D -m 0644 -t $(DESTDIR)$(SYSLIBDIR)/systemd/system qubes-admin-authzd.service + install -D -m 0644 -t $(DESTDIR)$(SYSCONFDIR)/qubes/ admin-authzd.conf + install -D -m 0644 -t $(DESTDIR)/usr/share/doc/qubes-core-agent-passwordless-root README.md install-rh: - install -D -m 0644 pam.d_su.qubes $(DESTDIR)$(PAMDIR)/su.qubes + install -D -m 0644 -t $(DESTDIR)$(SYSLIBDIR)/systemd/system-preset 75-qubes-admin-authz.preset + install -d $(DESTDIR)/usr/share/authselect/vendor + for i in \ + local \ + nis \ + sssd \ + winbind \ + ; do \ + install -D -t $(DESTDIR)/usr/share/authselect/vendor/$$i \ + authselect/$$i/system-auth authselect/$$i/password-auth || exit 1; \ + for j in \ + README \ + REQUIREMENTS \ + dconf-db \ + dconf-locks \ + fingerprint-auth \ + nsswitch.conf \ + postlogin \ + smartcard-auth \ + ; do \ + ln -s ../../default/$$i/$$j $(DESTDIR)/usr/share/authselect/vendor/$$i/$$j || exit 1; \ + done; \ + done install-debian: - install -D -m 0644 pam-configs_su.qubes $(DESTDIR)$(PAMCONFIGSDIR)/su.qubes + install -D -m 0644 pam-configs-qubes-admin-authz $(DESTDIR)$(PAMCONFIGSDIR)/qubes-admin-authz diff --git a/passwordless-root/README.md b/passwordless-root/README.md new file mode 100644 index 00000000..3dc85127 --- /dev/null +++ b/passwordless-root/README.md @@ -0,0 +1,88 @@ +# Passwordless in-qube admin authorization + +By default all users in the `qubes` group (usually only the standard `user` +user) are allowed to execute things as root via sudo/su/pkexec. This is allowed +without a password prompt. + +Separating the user isn't that meaningfull since Qubes' primary +compartimentalization layer are qubes and `user` in a qube has access most +things there anyway (user data, GUI I/O, etc). But some advanced users want +more options therefore this mechanism has been extended. + +## Modes + +There are 3 modes: + + - `allow`: Always allow admin access. + - `deny`: Always deny admin access. + - `qrexec`: Make a qrexec call and use it's result to allow/deny. The main + usage is to have a trivial qrexec service in dom0 and use the qrexec policy + with the ask action to allow only after prompting. + +"Admin access" here means being able to run things as another user (including +root), via sudo, su and polkit. This is always limited to users in the `qubes` +group. + + +## Config + +When the `qubes-core-agent-passwordless-root` is installed the +`pam_qubes_admin_authz.so` PAM module is always enabled. This module asks the +`qubes-admin-authzd` daemon which does the actual logic (see below for +technical details). + +The mode can be set via /usr/local/etc/qubes/admin-authzd.conf for per-qube +setting or more common via /etc/qubes/admin-authzd.conf in a template (setting +in /usr/local has precendce). + +There config file have a very simple syntax. The first line needs to contain +one of the listed mode above without anythings else. Currently the rest of the +file is ignored. Please prefix comments with `#` to allow extension of this +configuration file should the need arise. + +Additionally the mode can be set by setting the `admin-authz-mode` feature on a +qube or it's template. This setting override the above config files (internally +the feature sets a qubesdb entry, which is read on boot and written to +/run/qubes-admin-authzd.conf). + + +## qrexec policy + +After setting the mode to qrexec, you need to configure the qrexec policy in +dom0. For example: + +``` +qubes.AuthorizeAdminAccess * * @default ask target=dom0 default_target=dom0 +``` + +asks for requests from any VM. + +Note that in dom0 only a trivial service is run that returns a fixed string +such that the qube knows the result of the policy evaluation. The idea here is +that this way the existing qrexec policy can be reused, including it's ask +prompt. + + +## Limitaions + +Keep in mind that if you have allowed admin access and the qube was compromised +after that point persistence is trivial. + + +## Technical details + +We implement a PAM module named `pam_qubes_admin_authz.so` to permit the +access. Since PAM modules can be run in a setuid context (when called by +sudo/su) we want to keep the code simple there. Therefore we use just some +existing PAM helper functions to check for the requesting users group +membership and check the PAM "service" (`sudo`, `su-l`, etc.) and if they match +we make a connection to an abstract Unix socket. With `SO_PEERCRED` we check +that this socket has been opened by root. The rest, including config file +handling and invoking qrexec-client-vm is then handled by the +`qubes-admin-authzd` at the other end of the socket (started by +qubes-admin-authzd.service). + +## Recovery + +Should you have locked you self out you should still be able to use +`qvm-console-dispvm` and login as root there without a password. diff --git a/passwordless-root/admin-authzd.conf b/passwordless-root/admin-authzd.conf new file mode 100644 index 00000000..fcee6ed1 --- /dev/null +++ b/passwordless-root/admin-authzd.conf @@ -0,0 +1,4 @@ +allow + +# The first line of this file needs to be 'deny', 'allow' or 'qrexec'. +# For more details see /usr/share/doc/qubes-core-agent-passwordless-root/README diff --git a/passwordless-root/authselect/local/password-auth b/passwordless-root/authselect/local/password-auth new file mode 100644 index 00000000..badd0adb --- /dev/null +++ b/passwordless-root/authselect/local/password-auth @@ -0,0 +1,33 @@ +auth required pam_env.so +auth required pam_faildelay.so delay=2000000 +auth required pam_faillock.so preauth silent {include if "with-faillock"} +auth sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 +auth sufficient pam_u2f.so cue {include if "with-pam-u2f"} +auth required pam_u2f.so cue {if not "without-pam-u2f-nouserok":nouserok} {include if "with-pam-u2f-2fa"} +auth sufficient pam_unix.so {if not "without-nullok":nullok} +auth sufficient pam_systemd_home.so {include if "with-systemd-homed"} +auth required pam_faillock.so authfail {include if "with-faillock"} +auth optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} +auth required pam_deny.so + +account required pam_access.so {include if "with-pamaccess"} +account required pam_faillock.so {include if "with-faillock"} +account sufficient pam_systemd_home.so {include if "with-systemd-homed"} +account required pam_unix.so + +password sufficient pam_systemd_home.so {include if "with-systemd-homed"} +password requisite pam_pwquality.so +password [default=1 ignore=ignore success=ok] pam_localuser.so {include if "with-pwhistory"} +password requisite pam_pwhistory.so use_authtok {include if "with-pwhistory"} +password sufficient pam_unix.so yescrypt shadow {if not "without-nullok":nullok} use_authtok +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +session optional pam_ecryptfs.so unwrap {include if "with-ecryptfs"} +session optional pam_systemd_home.so {include if "with-systemd-homed"} +-session optional pam_systemd.so +session optional pam_oddjob_mkhomedir.so {include if "with-mkhomedir"} +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} diff --git a/passwordless-root/authselect/local/system-auth b/passwordless-root/authselect/local/system-auth new file mode 100644 index 00000000..c3431152 --- /dev/null +++ b/passwordless-root/authselect/local/system-auth @@ -0,0 +1,34 @@ +auth required pam_env.so +auth required pam_faildelay.so delay=2000000 +auth required pam_faillock.so preauth silent {include if "with-faillock"} +auth sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 +auth sufficient pam_fprintd.so {include if "with-fingerprint"} +auth sufficient pam_u2f.so cue {include if "with-pam-u2f"} +auth required pam_u2f.so cue {if not "without-pam-u2f-nouserok":nouserok} {include if "with-pam-u2f-2fa"} +auth sufficient pam_unix.so {if not "without-nullok":nullok} +auth sufficient pam_systemd_home.so {include if "with-systemd-homed"} +auth required pam_faillock.so authfail {include if "with-faillock"} +auth optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} +auth required pam_deny.so + +account required pam_access.so {include if "with-pamaccess"} +account required pam_faillock.so {include if "with-faillock"} +account sufficient pam_systemd_home.so {include if "with-systemd-homed"} +account required pam_unix.so + +password sufficient pam_systemd_home.so {include if "with-systemd-homed"} +password requisite pam_pwquality.so +password [default=1 ignore=ignore success=ok] pam_localuser.so {include if "with-pwhistory"} +password requisite pam_pwhistory.so use_authtok {include if "with-pwhistory"} +password sufficient pam_unix.so yescrypt shadow {if not "without-nullok":nullok} use_authtok +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +session optional pam_ecryptfs.so unwrap {include if "with-ecryptfs"} +session optional pam_systemd_home.so {include if "with-systemd-homed"} +-session optional pam_systemd.so +session optional pam_oddjob_mkhomedir.so {include if "with-mkhomedir"} +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} diff --git a/passwordless-root/authselect/nis/password-auth b/passwordless-root/authselect/nis/password-auth new file mode 100644 index 00000000..e166d0f4 --- /dev/null +++ b/passwordless-root/authselect/nis/password-auth @@ -0,0 +1,33 @@ +auth required pam_env.so +auth required pam_faildelay.so delay=2000000 +auth required pam_faillock.so preauth silent {include if "with-faillock"} +auth sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 +auth sufficient pam_u2f.so cue {include if "with-pam-u2f"} +auth required pam_u2f.so cue {if not "without-pam-u2f-nouserok":nouserok} {include if "with-pam-u2f-2fa"} +auth sufficient pam_unix.so {if not "without-nullok":nullok} +auth sufficient pam_systemd_home.so {include if "with-systemd-homed"} +auth required pam_faillock.so authfail {include if "with-faillock"} +auth optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} +auth required pam_deny.so + +account required pam_access.so {include if "with-pamaccess"} +account required pam_faillock.so {include if "with-faillock"} +account sufficient pam_systemd_home.so {include if "with-systemd-homed"} +account required pam_unix.so broken_shadow + +password sufficient pam_systemd_home.so {include if "with-systemd-homed"} +password requisite pam_pwquality.so {if not "with-nispwquality":local_users_only} +password [default=1 ignore=ignore success=ok] pam_localuser.so {include if "with-pwhistory"} +password requisite pam_pwhistory.so use_authtok {include if "with-pwhistory"} +password sufficient pam_unix.so yescrypt shadow {if not "without-nullok":nullok} use_authtok nis +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +session optional pam_ecryptfs.so unwrap {include if "with-ecryptfs"} +session optional pam_systemd_home.so {include if "with-systemd-homed"} +-session optional pam_systemd.so +session optional pam_oddjob_mkhomedir.so {include if "with-mkhomedir"} +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} diff --git a/passwordless-root/authselect/nis/system-auth b/passwordless-root/authselect/nis/system-auth new file mode 100644 index 00000000..e5a466b0 --- /dev/null +++ b/passwordless-root/authselect/nis/system-auth @@ -0,0 +1,34 @@ +auth required pam_env.so +auth required pam_faildelay.so delay=2000000 +auth required pam_faillock.so preauth silent {include if "with-faillock"} +auth sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 +auth sufficient pam_fprintd.so {include if "with-fingerprint"} +auth sufficient pam_u2f.so cue {include if "with-pam-u2f"} +auth required pam_u2f.so cue {if not "without-pam-u2f-nouserok":nouserok} {include if "with-pam-u2f-2fa"} +auth sufficient pam_unix.so {if not "without-nullok":nullok} +auth sufficient pam_systemd_home.so {include if "with-systemd-homed"} +auth required pam_faillock.so authfail {include if "with-faillock"} +auth optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} +auth required pam_deny.so + +account required pam_access.so {include if "with-pamaccess"} +account required pam_faillock.so {include if "with-faillock"} +account sufficient pam_systemd_home.so {include if "with-systemd-homed"} +account required pam_unix.so broken_shadow + +password sufficient pam_systemd_home.so {include if "with-systemd-homed"} +password requisite pam_pwquality.so {if not "with-nispwquality":local_users_only} +password [default=1 ignore=ignore success=ok] pam_localuser.so {include if "with-pwhistory"} +password requisite pam_pwhistory.so use_authtok {include if "with-pwhistory"} +password sufficient pam_unix.so yescrypt shadow {if not "without-nullok":nullok} use_authtok nis +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +session optional pam_ecryptfs.so unwrap {include if "with-ecryptfs"} +session optional pam_systemd_home.so {include if "with-systemd-homed"} +-session optional pam_systemd.so +session optional pam_oddjob_mkhomedir.so {include if "with-mkhomedir"} +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} diff --git a/passwordless-root/authselect/sssd/password-auth b/passwordless-root/authselect/sssd/password-auth new file mode 100644 index 00000000..26fe0d49 --- /dev/null +++ b/passwordless-root/authselect/sssd/password-auth @@ -0,0 +1,45 @@ +auth required pam_env.so +auth required pam_faildelay.so delay=2000000 +auth required pam_deny.so # Smartcard authentication is required {include if "with-smartcard-required"} +auth required pam_faillock.so preauth silent {include if "with-faillock"} +auth sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 +auth sufficient pam_u2f.so cue {include if "with-pam-u2f"} +auth required pam_u2f.so cue {if not "without-pam-u2f-nouserok":nouserok} {include if "with-pam-u2f-2fa"} +auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular +auth [default=1 ignore=ignore success=ok] pam_localuser.so +auth sufficient pam_unix.so {if not "without-nullok":nullok} +auth sufficient pam_systemd_home.so {include if "with-systemd-homed"} +auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular +auth sufficient pam_sss.so forward_pass +auth required pam_faillock.so authfail {include if "with-faillock"} +auth optional pam_gnome_keyring.so auto_start {include if "with-pam-gnome-keyring"} +auth required pam_deny.so + +account required pam_access.so {include if "with-pamaccess"} +account required pam_faillock.so {include if "with-faillock"} +account sufficient pam_systemd_home.so {include if "with-systemd-homed"} +account required pam_unix.so +account sufficient pam_localuser.so {exclude if "with-files-access-provider"} +account sufficient pam_usertype.so issystem +account [default=bad success=ok user_unknown=ignore] pam_sss.so +account required pam_permit.so + +password sufficient pam_systemd_home.so {include if "with-systemd-homed"} +password requisite pam_pwquality.so local_users_only +password [default=1 ignore=ignore success=ok] pam_localuser.so {include if "with-pwhistory"} +password requisite pam_pwhistory.so use_authtok {include if "with-pwhistory"} +password sufficient pam_unix.so yescrypt shadow {if not "without-nullok":nullok} use_authtok +password [success=1 default=ignore] pam_localuser.so +password sufficient pam_sss.so use_authtok +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +session optional pam_ecryptfs.so unwrap {include if "with-ecryptfs"} +session optional pam_systemd_home.so {include if "with-systemd-homed"} +-session optional pam_systemd.so +session optional pam_oddjob_mkhomedir.so {include if "with-mkhomedir"} +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_sss.so +session optional pam_gnome_keyring.so auto_start {include if "with-pam-gnome-keyring"} diff --git a/passwordless-root/authselect/sssd/system-auth b/passwordless-root/authselect/sssd/system-auth new file mode 100644 index 00000000..9d832619 --- /dev/null +++ b/passwordless-root/authselect/sssd/system-auth @@ -0,0 +1,52 @@ +{imply "with-smartcard" if "with-smartcard-required"} +auth required pam_env.so +auth required pam_faildelay.so delay=2000000 +auth required pam_faillock.so preauth silent {include if "with-faillock"} +auth sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 +auth [success=1 default=ignore] pam_succeed_if.so service notin login:gdm:xdm:kdm:kde:xscreensaver:gnome-screensaver:kscreensaver quiet use_uid {include if "with-smartcard-required"} +auth [success=done ignore=ignore default=die] pam_sss.so require_cert_auth ignore_authinfo_unavail {include if "with-smartcard-required"} +auth sufficient pam_fprintd.so {include if "with-fingerprint"} +auth sufficient pam_u2f.so cue {include if "with-pam-u2f"} +auth required pam_u2f.so cue {if not "without-pam-u2f-nouserok":nouserok} {include if "with-pam-u2f-2fa"} +auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular +auth [default=1 ignore=ignore success=ok] pam_localuser.so {exclude if "with-smartcard"} +auth [default=2 ignore=ignore success=ok] pam_localuser.so {include if "with-smartcard"} +auth [success=done authinfo_unavail=ignore user_unknown=ignore ignore=ignore default=die] pam_sss.so try_cert_auth {include if "with-smartcard"} +auth sufficient pam_unix.so {if not "without-nullok":nullok} +auth sufficient pam_systemd_home.so {include if "with-systemd-homed"} +auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular {include if "with-gssapi"} +auth sufficient pam_sss_gss.so {include if "with-gssapi"} +auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular +auth sufficient pam_sss.so forward_pass +auth required pam_faillock.so authfail {include if "with-faillock"} +auth optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} +auth required pam_deny.so + +account required pam_access.so {include if "with-pamaccess"} +account required pam_faillock.so {include if "with-faillock"} +account sufficient pam_systemd_home.so {include if "with-systemd-homed"} +account required pam_unix.so +account sufficient pam_localuser.so {exclude if "with-files-access-provider"} +account sufficient pam_usertype.so issystem +account [default=bad success=ok user_unknown=ignore] pam_sss.so +account required pam_permit.so + +password sufficient pam_systemd_home.so {include if "with-systemd-homed"} +password requisite pam_pwquality.so local_users_only +password [default=1 ignore=ignore success=ok] pam_localuser.so {include if "with-pwhistory"} +password requisite pam_pwhistory.so use_authtok {include if "with-pwhistory"} +password sufficient pam_unix.so yescrypt shadow {if not "without-nullok":nullok} use_authtok +password [success=1 default=ignore] pam_localuser.so +password sufficient pam_sss.so use_authtok +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +session optional pam_ecryptfs.so unwrap {include if "with-ecryptfs"} +session optional pam_systemd_home.so {include if "with-systemd-homed"} +-session optional pam_systemd.so +session optional pam_oddjob_mkhomedir.so {include if "with-mkhomedir"} +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_sss.so +session optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} diff --git a/passwordless-root/authselect/winbind/password-auth b/passwordless-root/authselect/winbind/password-auth new file mode 100644 index 00000000..75e7d876 --- /dev/null +++ b/passwordless-root/authselect/winbind/password-auth @@ -0,0 +1,41 @@ +auth required pam_env.so +auth required pam_faildelay.so delay=2000000 +auth required pam_faillock.so preauth silent {include if "with-faillock"} +auth sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 +auth sufficient pam_u2f.so cue {include if "with-pam-u2f"} +auth required pam_u2f.so cue {if not "without-pam-u2f-nouserok":nouserok} {include if "with-pam-u2f-2fa"} +auth sufficient pam_unix.so {if not "without-nullok":nullok} +auth sufficient pam_systemd_home.so {include if "with-systemd-homed"} +auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular +auth sufficient pam_winbind.so {if "with-krb5":krb5_auth} use_first_pass +auth required pam_faillock.so authfail {include if "with-faillock"} +auth optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} +auth required pam_deny.so + +account required pam_access.so {include if "with-pamaccess"} +account required pam_faillock.so {include if "with-faillock"} +account sufficient pam_systemd_home.so {include if "with-systemd-homed"} +account required pam_unix.so broken_shadow +account sufficient pam_localuser.so +account sufficient pam_usertype.so issystem +account [default=bad success=ok user_unknown=ignore] pam_winbind.so {if "with-krb5":krb5_auth} +account required pam_permit.so + +password sufficient pam_systemd_home.so {include if "with-systemd-homed"} +password requisite pam_pwquality.so local_users_only +password [default=1 ignore=ignore success=ok] pam_localuser.so {include if "with-pwhistory"} +password requisite pam_pwhistory.so use_authtok {include if "with-pwhistory"} +password sufficient pam_unix.so yescrypt shadow {if not "without-nullok":nullok} use_authtok +password sufficient pam_winbind.so {if "with-krb5":krb5_auth} use_authtok +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +session optional pam_ecryptfs.so unwrap {include if "with-ecryptfs"} +session optional pam_systemd_home.so {include if "with-systemd-homed"} +-session optional pam_systemd.so +session optional pam_oddjob_mkhomedir.so {include if "with-mkhomedir"} +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_winbind.so {if "with-krb5":krb5_auth} +session optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} diff --git a/passwordless-root/authselect/winbind/system-auth b/passwordless-root/authselect/winbind/system-auth new file mode 100644 index 00000000..26dc6861 --- /dev/null +++ b/passwordless-root/authselect/winbind/system-auth @@ -0,0 +1,42 @@ +auth required pam_env.so +auth required pam_faildelay.so delay=2000000 +auth required pam_faillock.so preauth silent {include if "with-faillock"} +auth sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 +auth sufficient pam_fprintd.so {include if "with-fingerprint"} +auth sufficient pam_u2f.so cue {include if "with-pam-u2f"} +auth required pam_u2f.so cue {if not "without-pam-u2f-nouserok":nouserok} {include if "with-pam-u2f-2fa"} +auth sufficient pam_unix.so {if not "without-nullok":nullok} +auth sufficient pam_systemd_home.so {include if "with-systemd-homed"} +auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular +auth sufficient pam_winbind.so {if "with-krb5":krb5_auth} use_first_pass +auth required pam_faillock.so authfail {include if "with-faillock"} +auth optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} +auth required pam_deny.so + +account required pam_access.so {include if "with-pamaccess"} +account required pam_faillock.so {include if "with-faillock"} +account sufficient pam_systemd_home.so {include if "with-systemd-homed"} +account required pam_unix.so broken_shadow +account sufficient pam_localuser.so +account sufficient pam_usertype.so issystem +account [default=bad success=ok user_unknown=ignore] pam_winbind.so {if "with-krb5":krb5_auth} +account required pam_permit.so + +password sufficient pam_systemd_home.so {include if "with-systemd-homed"} +password requisite pam_pwquality.so local_users_only +password [default=1 ignore=ignore success=ok] pam_localuser.so {include if "with-pwhistory"} +password requisite pam_pwhistory.so use_authtok {include if "with-pwhistory"} +password sufficient pam_unix.so yescrypt shadow {if not "without-nullok":nullok} use_authtok +password sufficient pam_winbind.so {if "with-krb5":krb5_auth} use_authtok +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +session optional pam_ecryptfs.so unwrap {include if "with-ecryptfs"} +session optional pam_systemd_home.so {include if "with-systemd-homed"} +-session optional pam_systemd.so +session optional pam_oddjob_mkhomedir.so {include if "with-mkhomedir"} +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_winbind.so {if "with-krb5":krb5_auth} +session optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"} diff --git a/passwordless-root/pam-configs-qubes-admin-authz b/passwordless-root/pam-configs-qubes-admin-authz new file mode 100644 index 00000000..09edf418 --- /dev/null +++ b/passwordless-root/pam-configs-qubes-admin-authz @@ -0,0 +1,7 @@ +Name: authorize admin access without password (by package qubes-core-agent-passwordless-root) +Default: yes +Priority: 258 +Auth-Type: Primary +Auth: + sufficient pam_qubes_admin_authz.so group=qubes services=su,su-l,sudo,sudo-i,polkit-1 + diff --git a/passwordless-root/pam-configs_su.qubes b/passwordless-root/pam-configs_su.qubes deleted file mode 100644 index 2c6a9c25..00000000 --- a/passwordless-root/pam-configs_su.qubes +++ /dev/null @@ -1,6 +0,0 @@ -Name: allow su without password (by package qubes-core-agent-passwordless-root) -Default: yes -Priority: 258 -Auth-Type: Primary -Auth: - sufficient pam_succeed_if.so use_uid user ingroup qubes service in su:su-l diff --git a/passwordless-root/pam.d_su.qubes b/passwordless-root/pam.d_su.qubes deleted file mode 100644 index e9853f8e..00000000 --- a/passwordless-root/pam.d_su.qubes +++ /dev/null @@ -1,21 +0,0 @@ -#%PAM-1.0 -auth sufficient pam_rootok.so -# Uncomment the following line to implicitly trust users in the "wheel" group. -#auth sufficient pam_wheel.so trust use_uid -# Uncomment the following line to require a user to be in the "wheel" group. -#auth required pam_wheel.so use_uid - -# {{ Qubes specific modifications begin here -# Prevent su from asking for password -# (by package qubes-core-agent-passwordless-root). -auth sufficient pam_succeed_if.so use_uid user ingroup qubes -# }} Qubes specific modifications end here - -auth substack system-auth -auth include postlogin -account sufficient pam_succeed_if.so uid = 0 use_uid quiet -account include system-auth -password include system-auth -session include system-auth -session include postlogin -session optional pam_xauth.so diff --git a/passwordless-root/pam_qubes_admin_authz.c b/passwordless-root/pam_qubes_admin_authz.c new file mode 100644 index 00000000..4be2715e --- /dev/null +++ b/passwordless-root/pam_qubes_admin_authz.c @@ -0,0 +1,235 @@ +#define _GNU_SOURCE 1 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "qubes-admin-authz-common.h" + +int pam_sm_authenticate(pam_handle_t *pamh, + __attribute__((unused)) int flags, + int argc, + const char **argv) { + bool debug = false; + bool quiet = false; + const char* allowed_group = NULL; + const char* allowed_services = NULL; + int rc = PAM_ABORT; + int s = -1; + FILE* sf = NULL; + + for (int i = 0; i < argc; i += 1) { + if (strcmp(argv[i], "debug") == 0) { + debug = true; + } else if (strcmp(argv[i], "quiet") == 0) { + quiet = true; + } else if (strncmp(argv[i], "group=", 6) == 0) { + allowed_group = &argv[i][6]; + if (strlen(allowed_group) == 0) { + pam_syslog(pamh, LOG_ERR, "missing value for 'group' option"); + rc = PAM_SERVICE_ERR; + goto ret; + } + } else if (strncmp(argv[i], "services=", 9) == 0) { + allowed_services = &argv[i][9]; + } else { + pam_syslog(pamh, LOG_ERR, "unknown option: %s", argv[i]); + // Ignore it, to make it easier to add new options in the future. + } + } + + if (allowed_group == NULL) { + pam_syslog(pamh, LOG_ERR, "missing 'group' option"); + rc = PAM_SERVICE_ERR; + goto ret; + } + + if (allowed_services == NULL) { + pam_syslog(pamh, LOG_ERR, "missing 'services' option"); + rc = PAM_SERVICE_ERR; + goto ret; + } + + // Only allow users in the specified group to use this module. + uid_t uid = getuid(); + if (!pam_modutil_user_in_group_uid_nam(pamh, uid, allowed_group)) { + if (debug) { + pam_syslog(pamh, + LOG_DEBUG, + "uid %i not in %s group", + uid, + allowed_group); + } + rc = PAM_IGNORE; + goto ret; + } + if (debug) { + pam_syslog(pamh, LOG_DEBUG, "uid %i in %s group", uid, allowed_group); + } + + // Only allow specified services. For example for an ssh login attempt it + // would be a bad idea to allow authentication with this module. + const char* service = NULL; + rc = pam_get_item(pamh, PAM_SERVICE, (const void **)&service); + if (rc != PAM_SUCCESS || service == NULL) { + pam_syslog(pamh, LOG_CRIT, "failed to get PAM_SERVICE: %i", rc); + rc = PAM_SYSTEM_ERR; + goto ret; + } + + size_t service_len = strlen(service); + size_t allowed_services_len = strlen(allowed_services); + bool service_allowed = false; + for (size_t i = 0; i < allowed_services_len;) { + size_t len = strcspn(&allowed_services[i], ","); + if (len == service_len && + memcmp(service, &allowed_services[i], len) == 0) + { + service_allowed = true; + break; + } + i += len + 1; + } + + if (!service_allowed) { + if (debug) { + pam_syslog(pamh, LOG_DEBUG, "ignoring service %s", service); + } + rc = PAM_IGNORE; + goto ret; + } + if (debug) { + pam_syslog(pamh, LOG_DEBUG, "handling service %s", service); + } + + s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (s == -1) { + pam_syslog(pamh, LOG_CRIT, "failed to create socket: %i", errno); + rc = PAM_SYSTEM_ERR; + goto ret; + } + + struct sockaddr_un addr = { + .sun_family = AF_UNIX, + .sun_path = SOCKET_PATH, + }; + + if (debug) { + pam_syslog(pamh, LOG_DEBUG, "connecting to authzd"); + } + rc = connect(s, + (struct sockaddr*)&addr, + sizeof(addr.sun_family) + sizeof(SOCKET_PATH) - 1); + if (rc != 0) { + pam_syslog(pamh, LOG_ERR, + "failed to connect to authorization daemon: %i", errno); + rc = PAM_AUTHINFO_UNAVAIL; + goto ret; + } + if (debug) { + pam_syslog(pamh, LOG_DEBUG, "connected"); + } + + struct ucred cred = {}; + socklen_t cred_size = sizeof(cred); + rc = getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cred, &cred_size); + if (rc == -1) { + pam_syslog(pamh, LOG_CRIT, + "failed to get peer credentials from socket: %i", errno); + rc = PAM_SYSTEM_ERR; + goto ret; + } + if (cred_size != sizeof(cred)) { + pam_syslog(pamh, + LOG_CRIT, + "failed to get peer credentials from socket: unexpected size"); + rc = PAM_SYSTEM_ERR; + goto ret; + } + if (debug) { + pam_syslog(pamh, LOG_DEBUG, + "authz socket opened by uid=%i gid=%i pid=%i", + cred.uid, cred.gid, cred.pid); + } + if (cred.uid != 0) { + pam_syslog(pamh, LOG_CRIT, "socket not opened by root"); + rc = PAM_SYSTEM_ERR; + goto ret; + } + + sf = fdopen(s, "r+"); + if (sf == NULL) { + pam_syslog(pamh, + LOG_ERR, + "failed to create stream from socket: %i", + errno); + rc = PAM_SYSTEM_ERR; + goto ret; + } + + if (fprintf(sf, "pam-service+%s\n", service) < 0 || fflush(sf) != 0) { + pam_syslog(pamh, LOG_ERR, "failed to write to socket: %i", errno); + rc = PAM_SYSTEM_ERR; + goto ret; + } + + char res[100] = {}; + if (fgets(res, sizeof(res), sf) == NULL) { + pam_syslog(pamh, LOG_ERR, "failed to read from socket"); + rc = PAM_SYSTEM_ERR; + goto ret; + } + size_t res_len = strlen(res); + if (res_len == 0 || res[res_len - 1] != '\n') { + pam_syslog(pamh, LOG_ERR, "received incomplete response"); + rc = PAM_SYSTEM_ERR; + goto ret; + } + res[res_len - 1] = '\0'; + res_len -= 1; + + // Since the other side is trusted this isn't strictly necessary. But it's + // probably still nicer to ensure that we don't put unexpected bytes into + // the log. + for (size_t i = 0; i < res_len; i += 1) { + if (res[i] < 0x20 || res[i] > 0x7e) { + res[i] = '.'; + } + } + + if (strcmp(res, "authorized") != 0) { + pam_syslog(pamh, LOG_NOTICE, "access not authorized: %s", res); + rc = PAM_AUTH_ERR; + goto ret; + } + + if (!quiet) { + pam_syslog(pamh, LOG_INFO, "access authorized"); + } + + rc = PAM_SUCCESS; + +ret: + if (s >= 0) { + if ((sf != NULL ? fclose(sf) : close(s)) != 0) { + pam_syslog(pamh, LOG_CRIT, "failed to close socket: %i", errno); + rc = PAM_SYSTEM_ERR; + } + } + return rc; +} + +int pam_sm_setcred(__attribute__((unused)) pam_handle_t *pamh, + __attribute__((unused)) int flags, + __attribute__((unused)) int argc, + __attribute__((unused)) const char **argv) { + return PAM_IGNORE; +} diff --git a/passwordless-root/polkit-1-qubes-allow-all.rules b/passwordless-root/polkit-1-qubes-allow-all.rules deleted file mode 100644 index a83c8273..00000000 --- a/passwordless-root/polkit-1-qubes-allow-all.rules +++ /dev/null @@ -1,2 +0,0 @@ -//allow any action, detailed reasoning in sudoers.d/qubes -polkit.addRule(function(action,subject) { if (subject.isInGroup("qubes")) return polkit.Result.YES; }); diff --git a/passwordless-root/qubes-admin-authz-common.h b/passwordless-root/qubes-admin-authz-common.h new file mode 100644 index 00000000..0f9f17ab --- /dev/null +++ b/passwordless-root/qubes-admin-authz-common.h @@ -0,0 +1,3 @@ +#pragma once + +#define SOCKET_PATH "\0qubes.AuthorizeAdminAccess" diff --git a/passwordless-root/qubes-admin-authzd.c b/passwordless-root/qubes-admin-authzd.c new file mode 100644 index 00000000..800d936a --- /dev/null +++ b/passwordless-root/qubes-admin-authzd.c @@ -0,0 +1,290 @@ +#define _GNU_SOURCE 1 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "qubes-admin-authz-common.h" + +#define MODE_DENY 0 +#define MODE_QREXEC 1 +#define MODE_ALLOW 2 + +int str2mode(const char* s, const char* log_name) { + int mode = -1; + if (strcmp(s, "deny") == 0) { + mode = MODE_DENY; + } + if (strcmp(s, "qrexec") == 0) { + mode = MODE_QREXEC; + } + if (strcmp(s, "allow") == 0) { + mode = MODE_ALLOW; + } + if (log_name != NULL) { + if (mode < 0) { + fprintf(stderr, + "mode in %s not 'deny', 'qrexec' or 'allow'\n", + log_name); + } else { + fprintf(stderr, "mode read from %s\n", log_name); + } + } + return mode; +} + +int main() { + int rc = -1; + int mode = -1; + uint64_t client_num = 0; + + char* env_mode_str = getenv("QUBES_ADMIN_AUTHZD_MODE"); + if (env_mode_str != NULL && env_mode_str[0] != '\0') { + mode = str2mode(env_mode_str, "QUBES_ADMIN_AUTHZD_MODE"); + if (mode < 0) { + return 1; + } + } + + char* confs[] = { + "/run/qubes-admin-authzd.conf", + "/usr/local/etc/qubes/admin-authzd.conf", + "/etc/qubes/admin-authzd.conf", + }; + for (size_t i = 0; + mode == -1 && i < sizeof(confs)/sizeof(confs[0]); + i += 1) + { + int fd = open(confs[i], O_RDONLY); + if (fd < 0) { + if (errno == ENOENT) { + fprintf(stderr, "%s does not exits\n", confs[i]); + continue; + } + fprintf(stderr, "failed to read %s: %m\n", confs[i]); + return 1; + } + char buf[100] = {}; + ssize_t read_ret = read(fd, &buf, sizeof(buf) - 1); + if (read_ret < 0) { + fprintf(stderr, "failed to read from %s: %m\n", confs[i]); + return 1; + } + char* eol = strchr(buf, '\n'); + if (eol != NULL) { + *eol = '\0'; + } + mode = str2mode(buf, confs[i]); + if (mode < 0) { + return 1; + } + } + + struct sigaction chld_act = { + .sa_handler = SIG_IGN, + .sa_flags = SA_NOCLDWAIT, + }; + if (sigaction(SIGCHLD, &chld_act, NULL) != 0) { + fprintf(stderr, "sigaction failed: %m\n"); + return 1; + } + + int s = -1; + s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (s == -1) { + fprintf(stderr, "failed to open socket: %m\n"); + return 1; + } + + struct sockaddr_un addr = { + .sun_family = AF_UNIX, + .sun_path = SOCKET_PATH, + }; + + rc = bind(s, + (struct sockaddr*)&addr, + sizeof(addr.sun_family) + sizeof(SOCKET_PATH) - 1); + if (rc != 0) { + fprintf(stderr, "failed to bind socket: %m\n"); + return 1; + } + + if (listen(s, 100) != 0) { + fprintf(stderr, "failed to listen on socket: %m\n"); + return 1; + } + + fprintf(stderr, "mode: %s\n", + mode == MODE_DENY ? "deny" : + mode == MODE_QREXEC ? "qrexec" : "allow"); + + int client = -1; + while (true) { + if (client >= 0) { + if (close(client) != 0) { + fprintf(stderr, + "client %w64u: close of client socket failed: %m\n", + client_num); + } + } + + fprintf(stderr, "waiting for new client\n"); + client = accept(s, NULL, NULL); + if (client < 0) { + fprintf(stderr, "accept failed: %m\n"); + return 1; + } + client_num += 1; + + fprintf(stderr, "client %w64u: connected\n", client_num); + + struct ucred cred = {}; + socklen_t cred_size = sizeof(cred); + rc = getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cred, &cred_size); + if (rc == -1) { + fprintf(stderr, + "client %w64u: " + "failed to get peer credentials from socket: %m\n", + client_num); + continue; + } + if (cred_size != sizeof(cred)) { + fprintf(stderr, + "clinet %w64u: failed to get peer credentials from socket: " + "unexpected size\n", + client_num); + continue; + } + if (cred.uid != 0) { + fprintf(stderr, + "client %w64u: client socket not opened by root\n", + client_num); + continue; + } + + pid_t pid = fork(); + if (pid == -1) { + fprintf(stderr, "client %w64u: fork failed: %m\n", client_num); + continue; + } else if (pid != 0) { + continue; + } + + // child + + FILE* client_f = fdopen(client, "r+"); + if (client_f == NULL) { + fprintf(stderr, + "client %w64u: failed to create stream from socket: %m\n", + client_num); + return 1; + } + + char req[100] = "qubes.AuthorizeAdminAccess+"; + size_t req_prefix_len = strlen(req); + + if (fgets(&req[req_prefix_len], + sizeof(req) - req_prefix_len, + client_f) == NULL) + { + fprintf(stderr, + "client %w64u: failed to read from socket\n", + client_num); + return 1; + } + size_t req_len = strlen(req); // strlen(req) > 0 because of prefix + if (req[req_len - 1] != '\n') { + fprintf(stderr, + "client %w64u: received incomplete request\n", + client_num); + return 1; + } + req[req_len - 1] = '\0'; + req_len -= 1; + + // Since the source is trusted (we received it from the PAM module + // running as root, which gets it from it's config) not strictly + // needed. Probably still nicer to error our early. + for (size_t i = req_prefix_len; i < req_len; i += 1) { + switch (req[i]) { + case 'a' ... 'z': + case 'A' ... 'Z': + case '_': + case '-': + case '.': + case '+': + continue; + } + fprintf(stderr, + "client %w64u: invalid character in request\n", + client_num); + return 1; + } + + fprintf(stderr, + "client %w64u: request received: %s\n", + client_num, + &req[req_prefix_len]); + + if (mode == MODE_DENY || mode == MODE_ALLOW) { + const char* res_str = mode == MODE_ALLOW ? "authorized" : "denied"; + if (fprintf(client_f, "%s\n", res_str) < 0 || + fflush(client_f) != 0) + { + fprintf(stderr, "client %w64u: write failed: %m\n", client_num); + return 1; + } + fprintf(stderr, + "client %w64u: answered '%s'\n", + client_num, + res_str); + return 0; + } + + // mode == MODE_QREXEC + + int new_stdin = open("/dev/null", O_RDONLY); + if (new_stdin < 0) { + fprintf(stderr, + "client %w64u: failed to open /dev/null: %m\n", + client_num); + return 1; + } + if (dup2(new_stdin, 0) < 0) { + fprintf(stderr, + "client %w64u: dup2(_, 0) failed: %m\n", + client_num); + return 1; + } + if (dup2(client, 1) < 0) { + fprintf(stderr, + "client %w64u: dup2(_, 1) failed: %m\n", + client_num); + return 1; + } + if (close(new_stdin) != 0 || fclose(client_f) != 0) { + fprintf(stderr, "client %w64u: close failed: %m\n", client_num); + return 1; + } + + fprintf(stderr, "client %w64u: forwarding to qrexec\n", client_num); + + execlp("qrexec-client-vm", + "qrexec-client-vm", + "@default", + req, + NULL); + fprintf(stderr, "client %w64u: exec failed: %m\n", client_num); + return 1; + } +} diff --git a/passwordless-root/qubes-admin-authzd.service b/passwordless-root/qubes-admin-authzd.service new file mode 100644 index 00000000..a1dc9f8e --- /dev/null +++ b/passwordless-root/qubes-admin-authzd.service @@ -0,0 +1,13 @@ +[Unit] +Description=Qubes admin access authorization daemon +After=qubes-db.service +Requires=qubes-db.service + +[Service] +Type=simple +ExecStart=/usr/bin/qubes-admin-authzd +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/passwordless-root/qubes.sudoers b/passwordless-root/qubes.sudoers index 50c670d0..919a6ea6 100644 --- a/passwordless-root/qubes.sudoers +++ b/passwordless-root/qubes.sudoers @@ -1,4 +1,4 @@ Defaults !requiretty -%qubes ALL=(ALL) ROLE=unconfined_r TYPE=unconfined_t NOPASSWD: ALL +%qubes ALL=(ALL) ROLE=unconfined_r TYPE=unconfined_t ALL # vim: ft=sudoers diff --git a/rpm_spec/core-agent.spec.in b/rpm_spec/core-agent.spec.in index ce0756ac..44c9a704 100644 --- a/rpm_spec/core-agent.spec.in +++ b/rpm_spec/core-agent.spec.in @@ -377,10 +377,10 @@ Integration of NetworkManager for Qubes VM: * show/hide NetworkManager applet icon %package passwordless-root -BuildArch: noarch Summary: Passwordless root access from normal user Conflicts: qubes-core-vm < 4.0.0 Requires(pre): shadow +Requires: authselect %if 0%{?is_opensuse} # for directory ownership BuildRequires: sudo @@ -508,7 +508,7 @@ make -C config-overrides DESTDIR=$RPM_BUILD_ROOT install make -C filesystem DESTDIR=$RPM_BUILD_ROOT install make -C misc "DESTDIR=$RPM_BUILD_ROOT" UDEVRULESDIR=%_udevrulesdir SYSLIBDIR=/usr/lib install make -C network DESTDIR=$RPM_BUILD_ROOT install -make -C passwordless-root DESTDIR=$RPM_BUILD_ROOT install install-rh +make -C passwordless-root DESTDIR=$RPM_BUILD_ROOT SYSLIBDIR=/usr/lib install install-rh make -C qubes-rpc DESTDIR=$RPM_BUILD_ROOT install make -C qubes-rpc/caja DESTDIR=$RPM_BUILD_ROOT install %if !0%{?is_opensuse} @@ -558,16 +558,6 @@ if ! grep -q /etc/default/grub.qubes /etc/default/grub 2>/dev/null; then echo '. /etc/default/grub.qubes' >> /etc/default/grub fi -%triggerin passwordless-root -- util-linux - -qubesfile=/etc/pam.d/su.qubes -origfile=${qubesfile%.qubes} -backupfile=${origfile}.qubes-orig -if [ -r "$origfile" -a ! -r "$backupfile" ]; then - mv -f "$origfile" "$backupfile" -fi -ln -sf "$qubesfile" "$origfile" - %post # disable some Upstart services @@ -724,6 +714,24 @@ fi %systemd_post qubes-network-uplink.service %systemd_post qubes-updates-proxy.service +%post passwordless-root + +systemctl --no-reload preset qubes-admin-authzd.service + +# We really want to have this running immediately, such that passwordless auth +# keeps working on upgrade. (But still allow the user to override the preset, +# if they really want. + +if [ -e /run/systemd/system ]; then + systemctl daemon-reload + + if systemctl is-enabled --quiet qubes-admin-authzd.service; then + systemctl restart qubes-admin-authzd.service + fi +fi + +authselect apply-changes + %post thunar if [ "$1" = 1 ]; then # There is no system-wide Thunar custom actions. There is only a default @@ -756,6 +764,9 @@ fi %systemd_preun qubes-network.service %systemd_preun qubes-updates-proxy.service +%preun passwordless-root +%systemd_preun qubes-admin-authzd.service + %if %{with selinux} %post selinux @@ -886,6 +897,8 @@ if [ -f "$backupfile" ]; then mv -f "$backupfile" "$origfile" fi +authselect apply-changes + %posttrans /usr/bin/glib-compile-schemas %{_datadir}/glib-2.0/schemas &> /dev/null || : @@ -1188,9 +1201,18 @@ rm -f %{name}-%{version} /usr/lib/qubes/show-hide-nm-applet.sh %files passwordless-root -%config(noreplace) /etc/polkit-1/rules.d/00-qubes-allow-all.rules %config(noreplace) /etc/sudoers.d/qubes -%config(noreplace) /etc/pam.d/su.qubes +%config(noreplace) /etc/qubes/admin-authzd.conf + +/usr/lib64/security/pam_qubes_admin_authz.so +/usr/bin/qubes-admin-authzd + +%_unitdir/qubes-admin-authzd.service +%{_unitdir}-preset/75-qubes-admin-authz.preset + +/usr/share/authselect/vendor/*/* + +/usr/share/doc/qubes-core-agent-passwordless-root/README.md %package systemd Summary: Qubes unit files for SystemD init style diff --git a/vm-systemd/qubes-sysinit.sh b/vm-systemd/qubes-sysinit.sh index 16c71adf..48e8fba6 100755 --- a/vm-systemd/qubes-sysinit.sh +++ b/vm-systemd/qubes-sysinit.sh @@ -64,6 +64,13 @@ for i in $services; do fi done +authz_mode="$(qubesdb-read --default '' /qubes-admin-authz-mode)" +if [ -n "$authz_mode" ]; then + echo "$authz_mode" > /run/qubes-admin-authzd.conf +else + rm -f /run/qubes-admin-authzd.conf +fi + # Prepare environment for other services echo > /run/qubes-service-environment