Implement optional admin authorization via qrexec#613
Conversation
marmarek
left a comment
There was a problem hiding this comment.
I've looked at the PAM module implementation for now, but not much at the PAM configs. On the first sight looks like there is quite a bit of duplication in the PAM configs, but that's probably unavoidable to make it work with any authselect profile...
| all: 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 $< -o $@ |
There was a problem hiding this comment.
Include standard CFLAGS var too (both debian and fedora add preferred hardening options here)
| gcc -O2 -Wall -Wextra -Werror -fPIC -pie $< -o $@ | ||
|
|
||
| pam_qubes_admin_authz.so: pam_qubes_admin_authz.c qubes-admin-authz-common.h | ||
| gcc -O2 -Wall -Wextra -Werror -fPIC -pie -shared $< -o $@ -lpam |
| dom0. For example: | ||
|
|
||
| ``` | ||
| qubes.AuthorizeInVMAdminAccess * * @default target=dom0 default_target=dom0 |
There was a problem hiding this comment.
Fixed. I'm wondering if we should use "VM" in the service name or change it to "qube"?
There was a problem hiding this comment.
I wonder about even simpler name like qubes.AuthorizeAdminAccess? But it might be confusing where that admin is (whether it gives dom0 admin access for example)... But OTOH that concern should be covered by the custom prompt already, right? (at least for the "ask" case)
@marmarta any opinion? Past related discussion starting at this comment
There was a problem hiding this comment.
I think AuthorizeAdminAccess is ok, within context of the custom prompt.
There was a problem hiding this comment.
As in, if we got a prompt that just had the service name I'm with @HW42 , but if there's a custom prompt, it's fine to omit it. If we do keep it, lets make it InQube, because from what I understand in can apply to things like remote qubes which might not be vms..
There was a problem hiding this comment.
changed to qubes.AuthorizeAdminAccess
| if (!(strcmp(service, "su") == 0 || | ||
| strcmp(service, "su-l") == 0 || | ||
| strcmp(service, "sudo") == 0 || | ||
| strcmp(service, "sudo-i") == 0 || | ||
| strcmp(service, "polkit-1") == 0)) { |
There was a problem hiding this comment.
Is this necessary, instead of just adding it to only relevant PAM configuration files? If it's necessary, better have the list in the module parameter, so it can be adjusted without rebuilding (for example to use it for other services too, like doas or login).
There was a problem hiding this comment.
I think yes, given how Debian and Fedora structure their PAM configurations. They have common stacks (like /etc/pam.d/common-auth) and those are then included by the service specific configs (like /etc/pam.d/su). Since the later are owned by those packages they are not so easy to update, unlike the former, for with their is authselect in Fedora and pam-auth-update in Debian.
My original plan was to configure it like this:
auth [default=1 success=ignore] pam_succeed_if.so use_uid user ingroup qubes service in su:su-l:sudo:sudo-i:polkit-1
auth sufficient pam_qubes_admin_authz.so
This works under Debian but the PAM stack under Fedora is broken because how of how it interacts with the relative jump (in default=1) works:
jump over the next N modules in the stack. Note that N equal to 0 is not allowed, it would be treated as ignore in such case. The side effect depends on the PAM function call: for pam_authenticate, pam_acct_mgmt, pam_chauthtok, and pam_open_session it is ignore; for pam_setcred and pam_close_session it is one of ignore, ok, or bad depending on the module's return value.
This means that for non matching requests pam_setcred get a failed state for non-matching requests and this is propagated through. This even breaks login on the console!
This can be worked around by negating the logic:
auth [default=ignore success=1] pam_succeed_if.so use_uid user ingroup qubes service in su:su-l:sudo:sudo-i:polkit-1
auth [default=1] pam_permit.so
auth sufficient pam_qubes_admin_authz.so
But I disliked this relatively complex jumping around.
But making it configurable via a module parameter is easy. In the end you will need to re-build the package anyway. But it would be easier for experimenting or other unusual usage of the module.
There was a problem hiding this comment.
this (and the group) is now configured by pam module arguments
| return 1; | ||
| } | ||
|
|
||
| execlp("qrexec-client-vm", "qrexec-client-vm", "@default", "qubes.AuthorizeInVMAdminAccess", NULL); |
There was a problem hiding this comment.
Just an idea: add PAM service name as the call argument? This way you could for example allow polkit but not sudo. Or do you think it it's not really useful?
There was a problem hiding this comment.
Not sure about a use case, since you get full admin access in all cases. But if you think it's useful, I would not object.
There was a problem hiding this comment.
I'm not really sure. It could be useful if there are cases where it allows some limited root access, but even for polkit it would allow pkexec (right?) so a full shell anyway... So, maybe not that useful?
There was a problem hiding this comment.
Yes currently the polkit config allows full access. But yes, in the future their might be an PAM using program that doesn't give full access.
sudo has an pam_service option, so you might build something based on that. Since polkit is scriptable there probably too, but I didn't looked into that.
There was a problem hiding this comment.
This is now implemented (Turned out to be a bit more involved than expected, since the info needs to be passed all the way).
e7b9577 to
b7decf6
Compare
Those are direct copies of the Fedora configs, just with our line added. And yes AFAIU that's how you are supposed to use authselect: You can only define a full profile not a small addition like with pam-auth-config. |
DemiMarie
left a comment
There was a problem hiding this comment.
Mostly looks good and I like the mechanism. The main caveat is that privilege escalation via various Xen devices is likely still possible. This is because libvchan does not have kernel support.
| if (read_ret < 0) { | ||
| pam_syslog(pamh, LOG_ERR, "failed to read from socket: %i", errno); | ||
| rc = PAM_SYSTEM_ERR; | ||
| goto ret; | ||
| } | ||
|
|
||
| // 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 < sizeof(res) - 1; i += 1) { | ||
| if (res[i] == '\0') { | ||
| break; | ||
| } | ||
| if (res[i] < 0x20 || res[i] > 0x7e) { | ||
| res[i] = '.'; | ||
| } | ||
| } |
There was a problem hiding this comment.
Maybe check that the other side didn’t send a NUL byte in the string?
There was a problem hiding this comment.
While not really helpful, I initially implemented this. It actually made the loop a bit simpler. But I have now switched to fgets which doesn't provide the read length (C's stdlib is really lacking ...). The code now handles a NUL like an incomplete read, which isn't nice, but not a problem either.
Offtopic for this PR... |
| # 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 |
There was a problem hiding this comment.
Is there a concrete scenario where policy-rc.d blocks startup of something incorrectly? I'm a bit worried about assuming the user's intentions here, since Kicksecure's build system uses a "deny everything" policy-rc.d to prevent everything from starting, because of the fallout it can have on the rest of the build. Bypassing this mechanism seems dangerous at best. If a user does end up blocked, they can always open a DispVM console and log in as root, or use qvm-run -u root ....
There was a problem hiding this comment.
I have this in (some of) my Debian templates as a stopgap solution to allow disabling services of newly installed packages before they are run (Debian's default policy).
since Kicksecure's build system uses a "deny everything" policy-rc.d to prevent everything from starting
But in that case you don't have a running systemd, right? So this is a nop.
That being said I don't feel too strongly about handling this corner case. So would be fine with dropping it, if preferred.
Related: |
See included README for details. Issue: QubesOS/qubes-issues#2695
When reviewing diff it against |
|
Bookworm fails, since it's gcc doesn't know the Will fix the archlinux error. |
My manual of |
It's a C23 feature and the manpages only recently catched up. Given that all distros we still support seem to have an gcc that understands it, seems nicer than |
Yes, I would prefer that. |
marmarek
left a comment
There was a problem hiding this comment.
Besides some minor comments below, it looks nice already. I've tested it on Fedora and seems to be working fine, including not breaking just after install.
One thing I'm not sure is default log verbosity. At least connected and waiting for new client messages do not add much.
| } | ||
|
|
||
| if (allowed_group == NULL) { | ||
| pam_syslog(pamh, LOG_ERR, "missing 'group' option"); |
There was a problem hiding this comment.
I'm not sure if it's really necessary to require it. Do you think misconfiguration (like typo in the option name) is too big of a risk?
| } | ||
|
|
||
| if (allowed_services == NULL) { | ||
| pam_syslog(pamh, LOG_ERR, "missing 'services' option"); |
See included README for details.
Issue: QubesOS/qubes-issues#2695