Page MenuHomeFreeBSD

MAC/do: Support multiple users and groups as single rule's targets
ClosedPublic

Authored by olce on Nov 15 2024, 5:08 PM.
Tags
None
Referenced Files
Unknown Object (File)
Sun, Jan 5, 7:45 PM
Unknown Object (File)
Sun, Jan 5, 9:54 AM
Unknown Object (File)
Fri, Dec 27, 11:05 AM
Unknown Object (File)
Fri, Dec 27, 10:15 AM
Unknown Object (File)
Fri, Dec 27, 2:57 AM
Unknown Object (File)
Thu, Dec 26, 12:58 PM
Unknown Object (File)
Thu, Dec 26, 4:57 AM
Unknown Object (File)
Tue, Dec 10, 8:39 AM

Details

Summary

This revision is part of a series. Click on the Stack tab below to see the context.
This series has also been squeezed into D47633 to provide an overall view.

Commit message:
Supporting group targets is a requirement for MAC/do to be able to
enforce a limited set of valid new groups passed to setgroups().
Additionally, it must be possible for this set of groups to also depend
on the target UID, since users and groups are quite tied in UNIX (users
are automatically placed in only the groups specified through
'/etc/passwd' (primary group) and '/etc/group' (supplementary ones)).

These requirements call for a re-design of the specification of the
rules specification string and of 'struct rule'.

A rules specification string is now a list of rules separated by ';'
(instead of ','). One rule is still composed of a "from" part and
a "to" (or "target") part, both being separated by ':' (as before).

The first part, "from", is matched against the credentials of the
process calling setuid()/setgroups(). Its specification remains
unchanged: It is a '<type>=<id>' clause, where <type> is either "uid" or
"gid" and <id> an UID or GID.

The second part, "to", is now a comma-separated (',') list of
'<flags><type>=<id>' clauses similar to that of the "from" part, with
the extensions that <id> may also be "*" or "any" or ".", and that
<flags> may contain at most one of the '+', '-' and '!' characters when
<type> is GID. "*" and "any" both designate any ID for the <type>, and
are aliases to each other. In front of them, only the "+" flag is
allowed (in addition to the previous rules). "." designates the
process' current IDs for the <type>, as explained below.

For GIDs, an absence of flag indicates that the specified GID is allowed
as the real, effective and/or saved GIDs (the "primary" groups).
Conversely, the presence of any allowed flag indicates that the
specification concerns supplementary groups. The '+' flag in front of
"gid" indicates that the ID is allowed as a supplementary group. The
'!' flag indicates that the ID is mandatory, i.e., must be listed in the
supplementary groups. The '-' flag indicates that the GID must not be
listed in the supplementary groups. A specification with '-' is only
useful in conjunction with a '+'-tagged specification where only one of
them has <id> ".", or if other MAC policies are loaded that would give
access to other, unwanted groups.

"." indicates some ID that the calling process already has on privilege
check. For type "uid", it designates any of the real, effective or
saved UIDs. For type "gid", its effect depends on the presence of one
of the '+', '-' or '!' flags. If no flag is present, it designates any
of the real, effective or saved GIDs. If one is present, it designates
any of the supplementary groups.

If the "to" part doesn't specify any explicit UID, any of the UIDs of
the calling process is implied (it is as if "uid=." had been specified).
Similarly, if it doesn't specify any explicit GID, "gid=.,!gid=." is
assumed, meaning that all the groups of the calling process are implied
and must be present. More precisely, each of the desired real,
effective and saved GIDs must be one of the current real, effective or
saved GID, whereas all others (the supplementary ones) must be the same
as those that are current.

No two clauses in a single "to" list may display the same <id>, except
for GIDs but only if, each time the same <id> appears, it does so with
a different flag (no flag counting as a separate flag) and all the
specified flags are not contradictory (e.g., it is possible to have the
same GID appear with no flag and the "+" flag, but the same GID with
both "+" and "-" will be rejected).

'struct rule' now holds arrays of UIDs (field 'uids') and GIDs (field
'gids') that are admissible as targets, with accompanying flags (such as
MDF_SUPP_MUST, representing the '!' flag). Some flags are also held by
ID type, including flags associated to individual IDs, as MDF_CURRENT in
these flags stands for the process being privilege-checked's current
IDs, to which ID flags apply. As a departure from this scheme, "*" or
"any" as <id> for GIDs is either represented by MDF_ANY or MDF_ANY_SUPP.
This is to make it coexist with a "."/MDF_CURRENT specification for the
other category of groups (among primary and supplementary groups), which
needs to be qualified by the usual GID flags.

This commit contains only the changes to parse the new rules and to
build their representation. The privilege granting part is not fixed
here, beyond what making compilation work requires (and, in preparation
for some subsequent commit, minimal adaptations to the matching logic in
check_setuid()).

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

olce requested review of this revision.Nov 15 2024, 5:08 PM

I read the commit message and haven't finished the code. The GID rules seems having much more features and I would like to know more. Do you think you can share some use scenarios and how to config the rule? I believe it would also be useful to have these in mac_do(4).

sys/security/mac_do/mac_do.c
149–156

TODO: For consistency with the block below, switch to (1u << X) for bit flags above.

543

TODO

805–806

TODO

sys/security/mac_do/mac_do.c
322–323

For ease of reading and consistency.

olce edited the summary of this revision. (Show Details)

Move some not-directly-related change to other revisions (existing ones, and a new one, D47772).

Change the position of GID flags in the specification: They now must be placed before gid= instead of after. The old spec could be confusing with '+' and '-' appearing just before (numerical) GIDs. To this end, parsing GID flags was moved into a new small static function (parse_gid_flags()). This change is very localized in the overall revision.

Style fixes (v & FLAG => (v & FLAG) != 0 and related), add missing
redundancy check for UIDs (no UID can appear twice or more in target clauses).

I read the commit message and haven't finished the code. The GID rules seems having much more features and I would like to know more. Do you think you can share some use scenarios and how to config the rule? I believe it would also be useful to have these in mac_do(4).

Sure. I didn't change mac_do(4) as part of this commit, as I prefer to wait for agreement on the new rules, but I will do it just afterwards (examples, but also the relevant explanations from the commit messages and the following comments). In fact, going to do more or less that below to ease your review.

Here are some examples:

sysctl security.mac.do.rules=uid=10001:uid=10002,gid=*,+gid=*

Gives UID 10001 the rights to become UID 10002 and become part of any group. This is basically the equivalent of today's sysctl security.mac.do.rules=uid=10001:10002. Both mdo -u 10002 and mdo -u 10002 -i will work.

sysctl security.mac.do.rules=uid=10001:uid=10002

Allow UID 10001 to switch to UID 10002, but only keeping the exact groups he's already in. So, mdo -u 10002 -i will work, but not mdo -u 10002 (unless UID 10002's groups are exactly UID 10001's groups, or that of the current process if they were modified).

sysctl security.mac.do.rules=uid=10001:uid=10002,uid=10003

Same, but also allows to switch to UID 10003 instead.

sysctl security.mac.do.rules=uid=10001:uid=10002,gid=10002

Same, but the new primary groups must be set to 10002, and no supplementary groups should be set. In particular, mdo -u 10002 will work (if UID 10002's primary group specification is GID 10002, and only if /etc/group doesn't enroll 10002 into any supplementary group), whereas mdo -u 10002 -i will not (unless UID 10001 is also member of the single (primary) group 10002).

sysctl security.mac.do.rules=uid=10001:uid=10002,gid=10002,+gid=.

Same as the previous, but allows to retain any current supplementary groups. mdo -u 10002 -i will work, and mdo -u 10002 also (unless UID 10002 has a different primary group than 10002 or is part of supplementary groups that are not already current).

sysctl security.mac.do.rules=uid=10001:uid=10002,gid=10002,!gid=.

Same as the previous, but *must* retain exactly all current supplementary groups. Compared to the previous example, this will cause mdo -u 10002 to fail unless UID 10002's supplementary groups are exactly those currently in force.

sysctl security.mac.do.rules=uid=10001:uid=10002,gid=10002,+gid=.,-gid=10001

Same as sysctl security.mac.do.rules=uid=10001:uid=10002,gid=10002,+gid=. above, but 10001 cannot be retained as a supplementary group. mdo -u 10002 -i will work but only if 10001 is not present as a supplementary group, and mdo -u 10002 will work (unless UID 10002 has a different primary group than 10002 or is part of supplementary groups that are not already current).

sysctl security.mac.do.rules=uid=10001:uid=10002,gid=10002,+gid=.,!gid=10003

Same as sysctl security.mac.do.rules=uid=10001:uid=10002,gid=10002,+gid=. above, with the additional constraint that 10003 must appear in the supplementary groups. Both mdo -u 10002 -i and mdo -u 10002 will continue to work as explained for that previous case, but only if, respectively, 10003 is a supplementary group of the current process and UID 10002's is in supplementary group 10003 (according to /etc/group).

sysctl security.mac.do.rules=gid=100000:uid=0

Makes 100000 a wheel group on steroid, allowing to switch to root without password (with the current state of mdo(1); we could modify the latter to request passwords in some cases, in which case security is weakened as this check would be performed by userland and not mac_do(4), but this is probably perfectly acceptable in less strong, common threat models).


The following transitions, as they only involve GID changes, cannot currently be required through mdo(1), but I think would be valuable to support:

sysctl security.mac.do.rules=gid=10001:gid=10002

Allow users of GID 10001 to enter GID 10002 as a primary group, but only if giving up all their supplementary groups.

sysctl security.mac.do.rules=gid=10001:gid=10002,+gid=.

Same as the previous, but allows to retain any current supplementary groups.

sysctl security.mac.do.rules=gid=10001:gid=10002,!gid=.

Same as the previous, but *must* retain exactly all current supplementary groups.


Let me explain briefly the fine points of the design of the new rules (in addition to the main reason stated in the commit message's first paragraph).

First, it is now possible to specify the exact UIDs, primary groups and supplementary groups allowed in the new credentials, with the allowed groups depending on the allowed UIDs. To do that, simply specify separate rules with same "from" part and a different set of uid=<uid>, gid=<gid> and <flag>gid=<gid> target clauses. Multiple rules can be specified in security.mac.do.rules by separating them with ; (contrary to , previously, which is now used to separate target clauses, as can be seen in the above examples). As before, each applicable rule is tried in turn, and as soon as one rule authorizes it, the requested credentials transition is allowed (more formally, they form an inclusive disjunction).

The granularity of checks on the new credentials stops at the level of primary users and groups (and supplementary groups) because, in the UNIX model, being able to change only one of the effective, real or saved IDs for users or groups then authorizes changing the others similarly without any privilege (e.g., using setuid(), setreuid(), etc.).

Supplementary groups are treated separately than primary ones as unprivileged users cannot change them. They thus can be used in negative access control policies, where group membership leads to certain interactions being restricted or triggering special actions (e.g., logging). In FreeBSD specifically, this is typically possible through mac_bsdextended(4), and more generally could be leveraged by any custom MAC modules. More generally, in UNIX, applications may perform specific actions depending of the credentials of the user running them. Target clauses concerning supplementary groups are of the form <flag>gid=<gid> with <flag> being one (and only one) of the authorized flags +, ! and -, whereas those for primary groups are of the form gid=<gid> (no flags).

In a single rule's "to"/"target" part, target clauses with exactly the same type (i.e., uid=<uid> or gid=<gid> or <flag>gid=<gid>) and different target IDs quite naturally indicate alternatives. For example, the rule uid=10001:uid=10002,uid=10003,gid=10002,gid=10003 expresses that either 10002 or 10003 is possible as both target UIDs and GIDs. This example rule allows to set the UID and GID to different values, e.g., setting UID to 10002 and GID to 10003 is an authorized combination. If this is not desired, then 2 rules must be used instead: uid=10001:uid=10002,gid=10002;uid=10001:uid=10003,gid=10003.

No two exact copies of the same target clause can appear in any single rule. This is a deliberate constraint and not a technical limitation (lifting it amounts to removing/deactivating some code). The reason behind this choice is that I expect humans to fill up the rules in most cases, in which case redundancies or contradictions are most likely the sign of a mistake when designing or typing the rules, and in any case can obfuscate the meaning of a rule. This makes machine-generation of rules a bit harder, but we could quite easily add another (per-jail) sysctl to lift the stringent checks if that proves necessary. We may want to implement machine-generation of rules in userland as a help to administrators (e.g., in mdo(1), adding to it a special mode outputting rules corresponding to what is in the password and group databases).

If the target part of one rule has no uid=<uid> clause, then the default is that the rule only allows new UIDs (effective, real and saved) to be within the current process' set (effective, real and saved; i.e., it is as if uid=. is specified), which can be roughly summarized by saying that the UID can't be changed (but any unprivileged transformation, e.g., if using setuid(), setreuid(), etc., or the new setcred(), stays allowed). If the target part of one rule has no GID clause (for primary nor supplementary groups), then the rule only allows current GIDs as the primary GIDs (effective, real and saved) and enforces that supplementary groups stay exactly the same (i.e., it is as if gid=.,!gid=. has been specified). One may notice a slight discrepancy here with respect to the paragraph above about alternatives, where primary and supplementary groups target clauses were said to be treated as different types. For example, specifying some gid=<gid> (without flags) alone is enough to cancel the default of gid=.,!gid=., in effect forbidding any supplementary group if there is no explicit additional +gid=<gid> or !gid=<gid> clause. We made this choice for two reasons: Firstly, because we view a change of groups as a whole as access control is based on group membership regardless of whether a group is primary or supplementary, and secondly because having separate defaults for primary and supplementary groups, with the defaults still being the current credentials' settings, would need to support some construct actually removing the !gid=. default. The second reason could be fixed quite easily (we could authorize -gid=*, which is not currently, see below). The real debate would be on the first reason (are there drawbacks to ignoring this view, and how do they compare to inconvenience of the slight discrepancy we mentioned?).

Target clauses concerning supplementary groups (i.e., of the form <flag>gid=<gid> with <flag> being one of +, ! or -) that reference the same GID must be compatible. In practice, and given the rule of non-redundancy above, that means that a single GID may appear only in at most two target clauses concerning supplementary rules per rule, one with flag + and the other with flag ! (e.g., +gid=10001,!gid=10001, which is then treated as equivalent to !gid=10001). It is debatable if this tolerance is in contradiction with the reasons for the rule of non-redundancy above. If you find so, it is easy to remove (just a few lines). -gid=<gid> obviously cannot be combined with +gid=<gid> nor !gid=<gid> with the same <gid>.

Where a numerical ID can appear, * or any can be used instead, with the restriction that a supplementary group target clause must have the + flag (why other flags are forbidden should be obvious, but I can make it explicit in the manpage if you prefer).

* or any can also appear alone as the target part of a rule, e.g., like this: uid=10001:*, which basically means that matching users/groups (here, UID 10001) can become anybody else with any set of groups, which is (almost) equivalent as been root. This is basically a shortcut for the equivalent uid=10001:uid=*,gid=*,+gid=*.

The colon : is still the separator between the "from" and "to"/"target" part of a rule, but after some use this has been found rather impractical. In UNIX, we use : to separate elements in the PATH environment variable and a lot of applications have adopted it for this or similar purposes, so administrators' eyes are trained to find it and consider it as a delimiter between top-level elements, whereas here they are in fact not (they don't separate rules, only the two parts of them). It seems that something like -> better articulates the two parts of a rule ("from" and "to"/"target"). So, barring oppositions, we will update this review accordingly later.


What does lie ahead?

Beyond what's in the current series, the only useful additions to mac_do(4) I currently see are introducing the ability to tweak mdo(1)'s path, to accommodate, e.g., custom thin jail layouts, as well as allowing multiple paths, in order to use mac_do(4) in combination with other programs. sudo(1) and doas(1) currently can't, as mac_do(4) only operates on setcred() calls, but with the new infrastructure mac_do(4) can easily be extended to support the usual setuid() and co. system calls, which would allow to more finely control transitions to root and to install these userland programs without the setuid bit. The full power of the new mac_do(4), however, cannot be harnessed without modifying these programs to use setcred().

Concerning mdo(1), I see several possible next steps. Currently the credentials transitions that can be requested through it are fairly limited compared to what mac_do(4)'s rules can allow (see examples above). We should probably add to it ways to specify any list of target groups (primary or supplementary), possibly based on user names (so the list comes from what's in /etc/passwd and /etc/group) but allowing some tweaks (such as, the list of groups in which user test is in, but not group restrict). mdo(1) should also allow changes of groups only. We may want that mdo(1) asks for a password before calling setcred() in certain cases, as already evoked above in the examples. This weakens the security paradigm of the mac_do(4) + mdo(1) combination (as we now rely on userland for part of the gating process), but I think is acceptable in lots of cases. As evoked above in the fine points, mdo(1) could grow a mode producing rules (or, at least, their target part) corresponding to what's in the password and group databases for some users. Having mac_do(4) store passwords and check them itself is a possibility, but absent any specific demand at this point I'm not sure if it's worth doing it.

This revision is now accepted and ready to land.Dec 2 2024, 8:54 AM

I locally have very small changes to this revision, essentially whitespace fixes and the introduction of a new flag MDF_EXPLICIT_SUPP_MUST as an impact of the update of D47620.

Will update the diff here after having made the : => -> separator change.