Bug 1215355 - AUDIT-0: himmelblau: review of PAM module pam_himmelblau.so
Summary: AUDIT-0: himmelblau: review of PAM module pam_himmelblau.so
Status: RESOLVED FIXED
Alias: None
Product: openSUSE Tumbleweed
Classification: openSUSE
Component: Security (show other bugs)
Version: Current
Hardware: Other Other
: P5 - None : Normal (vote)
Target Milestone: ---
Assignee: Matthias Gerstner
QA Contact: E-mail List
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2023-09-14 18:12 UTC by David Mulder
Modified: 2023-11-15 10:03 UTC (History)
2 users (show)

See Also:
Found By: ---
Services Priority:
Business Priority:
Blocker: ---
Marketing QA Status: ---
IT Deployment: ---


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description David Mulder 2023-09-14 18:12:26 UTC
This is a new project which provides pam and nss modules for authenticating to Microsoft Azure AD (Entra ID).
The project also has 2 daemons with systemd services. I'm not sure whether these need a security review also.

The project source:
https://github.com/openSUSE/himmelblau

The systemd services are in the platform/opensuse directory.
The pam module is actually identical to the kanidm pam binary (and is built from a kanidm git submodule), but is built to use the himmelblau config (and socket file), and to communicate with the himmelblaud daemon. You can find the source in src/kanidm/unix_integration/pam_kanidm.
Comment 1 William Brown 2023-09-15 00:17:50 UTC
Assigning to Matthias as they reviewed the Kanidm pam module in the past.

@Matthias, the himmelblau code is a joint-effort between kanidm and himmelblau, where the two projects share common components like the pam and nss module code, and the resolver daemon has pluggable backends that himmelblau has connected to. As a result, this is pretty much "the same module" as kanidm, just with a different backend in the local resolver. Hopefully that makes it easier for you :)
Comment 5 David Mulder 2023-09-18 15:38:32 UTC
It can wait. This isn't urgent.
Comment 6 Matthias Gerstner 2023-10-04 12:54:08 UTC
(In reply to william.brown@suse.com from comment #1)
> @Matthias, the himmelblau code is a joint-effort between kanidm and himmelblau, where the two projects share common components like the pam and nss module code, and the resolver daemon has pluggable backends that himmelblau has connected to. As a result, this is pretty much "the same module" as kanidm, just with a different backend in the local resolver. Hopefully that makes it easier for you :)

Yeah following the many redirections I end up in
src/kanidm/unix_integration/pam_kanidm/src/pam, so the PAM module itself is
the very same as we have in kanidm, right?

So it would be more interesting actually to look into the different Himmelblau
backend. Is this found in src/daemon?

Is there already a packaging of Himmelblau anywhere?
Comment 7 David Mulder 2023-10-04 13:31:10 UTC
(In reply to Matthias Gerstner from comment #6)
> So it would be more interesting actually to look into the different
> Himmelblau
> backend. Is this found in src/daemon?
> 
> Is there already a packaging of Himmelblau anywhere?

Yes, the backend daemon is in src/daemon, and it is literally just building the kanidm pam module (but pointing to a different socket file).

This is packaged here: https://build.opensuse.org/package/show/network:idm/himmelblau
Comment 8 Matthias Gerstner 2023-10-05 11:05:29 UTC
As was stated before the actual PAM module is practically identical to
kanidm's except for the config and socket path constants.

The first layer of the backend in src/daemon is also very similar to what we
have in kanidm. The interesting stuff starts in himmelblau.rs. In the end the
meat of it boils down to some Python interfacing towards the "msal" Python
module.

Some of the HTTPS handling is done in Rust but most of it is done in the msal
Python module. Making sure that the SSL verification is always properly
performed is important. Can you vouch for that - it would save me digging even
deeper into this "msal" stuff than I already did.

The cleartext password is passed around a lot in the Rust program as well as
in the Python parts so clearing it reliably from memory is out of the
question - which is otherwise a hardening measure that some PAM module attempt
to achieve.

It looks like the msal package passes the cleartext password on to the third
party servers e.g. in msal/oauth2cli/oauth2.py:182 `_obtainToken()`. This
means it should be avoided passing on passwords for accounts that aren't
supposed to be authenticated this way, most notably root, of course. Do you
have logic in place that prevents that?
Comment 9 David Mulder 2023-10-05 15:13:09 UTC
(In reply to Matthias Gerstner from comment #8)
> As was stated before the actual PAM module is practically identical to
> kanidm's except for the config and socket path constants.
> 
> The first layer of the backend in src/daemon is also very similar to what we
> have in kanidm. The interesting stuff starts in himmelblau.rs. In the end the
> meat of it boils down to some Python interfacing towards the "msal" Python
> module.
> 
> Some of the HTTPS handling is done in Rust but most of it is done in the msal
> Python module. Making sure that the SSL verification is always properly
> performed is important. Can you vouch for that - it would save me digging
> even
> deeper into this "msal" stuff than I already did.

The msal code is maintained by Microsoft. I would assume they've performed proper SSL verification, but I don't know for sure. I know the project is supposed to be very stable, and is well maintained. The only alternative from Microsoft was to link against the msal Go binaries, which are considered experimental.
I'm planning to replace the msal calls with Rust code, but I haven't gotten to this yet.

> 
> The cleartext password is passed around a lot in the Rust program as well as
> in the Python parts so clearing it reliably from memory is out of the
> question - which is otherwise a hardening measure that some PAM module
> attempt
> to achieve.
> 
> It looks like the msal package passes the cleartext password on to the third
> party servers e.g. in msal/oauth2cli/oauth2.py:182 `_obtainToken()`. This
> means it should be avoided passing on passwords for accounts that aren't
> supposed to be authenticated this way, most notably root, of course. Do you
> have logic in place that prevents that?

`process_etc_passwd_group` in the daemon collects local users/groups and loads them into the cache nxset, which is meant to reject auth for local user/group names. William, I'm looking over the nxset check though, and it looks like it doesn't happen until after attempting to auth (in the resolver code)? I'm pretty sure I've seen the opposite behavior though, so can you comment on this William?
Comment 10 Matthias Gerstner 2023-11-02 13:46:05 UTC
@William: Any news here?
Comment 11 David Mulder 2023-11-02 13:48:36 UTC
(In reply to Matthias Gerstner from comment #10)
> @William: Any news here?

What exactly are you waiting on?
Comment 12 Matthias Gerstner 2023-11-02 14:16:18 UTC
(In reply to David Mulder from comment #11)
> (In reply to Matthias Gerstner from comment #10)
> > @William: Any news here?
> 
> What exactly are you waiting on?

For a final statement about the second half of comment 9.
Comment 13 William Brown 2023-11-02 23:20:28 UTC
Sorry didn't realise I was the blocker here! 

> > 
> > The cleartext password is passed around a lot in the Rust program as well as
> > in the Python parts so clearing it reliably from memory is out of the
> > question - which is otherwise a hardening measure that some PAM module
> > attempt
> > to achieve.
> > 
> > It looks like the msal package passes the cleartext password on to the third
> > party servers e.g. in msal/oauth2cli/oauth2.py:182 `_obtainToken()`. This
> > means it should be avoided passing on passwords for accounts that aren't
> > supposed to be authenticated this way, most notably root, of course. Do you
> > have logic in place that prevents that?
> 
> `process_etc_passwd_group` in the daemon collects local users/groups and
> loads them into the cache nxset, which is meant to reject auth for local
> user/group names. William, I'm looking over the nxset check though, and it
> looks like it doesn't happen until after attempting to auth (in the resolver
> code)? I'm pretty sure I've seen the opposite behavior though, so can you
> comment on this William?

To trace this out, let's assume we're doing a pam flow.

This starts in pam_account_authenticate which then immediately tries to get the users token (information token here, not auth token). This is done with pam_account_authenticate_init which immediately calls get_cached_usertoken. This calls dbtxn.get_account(). If the user is in the DB, then we return the info. If they aren't found, we check the nxcache. 

So lets be clear here, if the user is part of the nxset (aka a local account or id) they can not be part of the db cache, and they won't be in the nxcache. So this will always return None.

At this point we also don't know if the name given to us by the requestor (such as "admin") will actually resolve differently into the user token. Kanidm allows this where you can login with "william" but it will resolve and render to "william@idm.suse.com". So we have to proceed with the auth here because we don't know the final state of the users id yet. 

The auth session then starts and we step with the pam module collecting credentials and components with pam_account_authenticate_step until we get to AuthResult::Success back from the provider in that function. At that point we now have the users fully resolved identity in the token returned to us by the provider. This is where we check the nxset to see if there is an identity collision, of the id of the user as it would be *resolved* on the system. If there is a collision, we return a pam denied. 


So to summarise:

* local system users can't be in the nxcache or the dbcache
* we can't check the nxset until the auth completes because we don't know the final name/gid/uid of the account until auth completes
* if the user has a name/gid/uid conflict, we are checking the *remote* provider credentials, not the local /etc/shadow credentials, so this is not somehow exposing attacks again local users.
* the nxset contains all local account names and uid/gids to ensure that there are no ways for a remote user to "claim" being a local system account. 
* The nxset is watched with inotify to ensure any live changes to the system are immediately reflected in the resolver

Hope that helps :)
Comment 14 Matthias Gerstner 2023-11-03 09:13:07 UTC
Thanks for the very detailed response. I just wanted to make sure this is
covered.

I will prepare a whitelisting for the new PAM module then.
Comment 16 Matthias Gerstner 2023-11-15 10:03:38 UTC
The whitelisting has reached Factory, thus closing this review as fixed.