diff --git a/libexec/rc/rc.conf b/libexec/rc/rc.conf --- a/libexec/rc/rc.conf +++ b/libexec/rc/rc.conf @@ -725,6 +725,9 @@ mixer_enable="YES" # Run the sound mixer. opensm_enable="NO" # Opensm(8) for infiniband devices defaults to off nuageinit_enable="NO" # Run nuageinit at startup +mdoctl_enable="NO" # Load MAC/do at startup +mdoctl_program="/sbin/mdoctl" # mdoctl executable +mdoctl_config="/etc/mdo.conf" # Location of mdo.conf # rctl(8) requires kernel options RACCT and RCTL rctl_enable="YES" # Load rctl(8) rules on boot diff --git a/libexec/rc/rc.d/Makefile b/libexec/rc/rc.d/Makefile --- a/libexec/rc/rc.d/Makefile +++ b/libexec/rc/rc.d/Makefile @@ -46,6 +46,7 @@ mountlate \ mdconfig \ mdconfig2 \ + mdoctl \ msgs \ netif \ netoptions \ diff --git a/libexec/rc/rc.d/mdoctl b/libexec/rc/rc.d/mdoctl new file mode 100755 --- /dev/null +++ b/libexec/rc/rc.d/mdoctl @@ -0,0 +1,25 @@ +#!/bin/sh +# +# PROVIDE: mdoctl +# REQUIRE: FILESYSTEMS +# BEFORE: LOGIN + +. /etc/rc.subr + +name="mdoctl" +desc="MAC/do configuration" +rcvar="mdoctl_enable" +start_cmd="mdoctl_start" + +load_rc_config $name + +# doesn't make sense to run in a svcj: configures kernel +mdoctl_svcj="NO" + +mdoctl_start() { + sysctl -qn security.mac.do >/dev/null || kldload -qn mac_do || return 1 + $mdoctl_program enable + $mdoctl_program load -f "$mdoctl_config" +} + +run_rc_command "$1" diff --git a/sbin/Makefile b/sbin/Makefile --- a/sbin/Makefile +++ b/sbin/Makefile @@ -34,6 +34,7 @@ md5 \ mdconfig \ mdmfs \ + mdoctl \ mknod \ mksnap_ffs \ mount \ diff --git a/sbin/mdoctl/Makefile b/sbin/mdoctl/Makefile new file mode 100644 --- /dev/null +++ b/sbin/mdoctl/Makefile @@ -0,0 +1,7 @@ +PACKAGE= utilities +PROG_CXX= mdoctl +SRCS= mdoctl.cc +CXXSTD= c++23 +MAN= mdoctl.8 + +.include diff --git a/sbin/mdoctl/mdoctl.8 b/sbin/mdoctl/mdoctl.8 new file mode 100644 --- /dev/null +++ b/sbin/mdoctl/mdoctl.8 @@ -0,0 +1,52 @@ +.\" SPDX-License-Identifier: ISC +.\" Copyright (c) 2025 Lexi Winter. +.\" +.Dd May 24, 2025 +.Dt MDOCTL 8 +.Os +.Sh NAME +.Nm mdoctl +.Nd manage MAC/do +.Sh SYNOPSIS +.Nm +.Cm disable +.Nm +.Cm enable +.Nm +.Cm load +.Op Fl f Ar path +.Nm +.Cm status +.Sh DESCRIPTION +The +.Nm +utility is used to manage the MAC/do framework, +.Xr mac_do 4 . +The behaviour of +.Nm +depends on the command: +.Bl -tag -width disable +.It Cm status +Display the current status of MAC/do. +.It Cm enable +Enable MAC/do. +.It Cm disable +Disable MAC/do. +.It Cm load +Load the MAC/do ruleset from +.Pa /etc/mdo.conf , +or from +.Ar file +if specified +(see +.Xr mdo.conf 5 ) . +.El +.Sh SEE ALSO +.Xr mac_do 4 , +.Xr mdo 1 , +.Xr mdo.conf 5 +.Sh HISTORY +The +.Nm +utility appeared in +.Fx 15.0 . diff --git a/sbin/mdoctl/mdoctl.cc b/sbin/mdoctl/mdoctl.cc new file mode 100644 --- /dev/null +++ b/sbin/mdoctl/mdoctl.cc @@ -0,0 +1,825 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2025 Lexi Winter. + */ + +/* + * mdoctl: manage MAC/do + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace std::literals; + +namespace { + +/* + * scope_guard: invoke a callable when this object is destroyed; this is similar + * to scope_exit from the library fundamentals TS, which LLVM doesn't implement. + */ +template +struct guard final { + // Initialise the guard with a callable we will invoke later. + guard(F func) : _func(std::move(func)) {} + + /* + * We are being destroyed, so call the callable. + * If the callable throws, std::terminate() will be called. + */ + ~guard() { + if (_func) + std::invoke(*_func); + } + + // Release the guard. This turns the destructor into a no-op. + void release() noexcept { + _func.release(); + } + + // Not default-constructible or copyable. + guard() = delete; + guard(guard const &) = delete; + guard(guard &&) noexcept = delete; + guard &operator=(guard const &) = delete; + guard &operator=(guard &&) noexcept = delete; + +private: + // The callable to be invoked when we are destroyed. + std::optional _func; +}; + +/* + * The current status of MAC/do in the kernel. not_loaded means we couldn't + * load the MAC/do sysctl tree for some reason, which is usually because the + * kernel modules isn't loaded. + */ +enum struct mdo_status { + enabled, disabled, not_loaded +}; + +/* MAC/do constants */ +const std::string + sysctl_enabled = "security.mac.do.enabled", + sysctl_rules = "security.mac.do.rules", + default_config_path = "/etc/mdo.conf", + + // What is considered "whitespace" when loading mdo.conf. + // This matches isspace() for the C locale. + whitespace = " \t\n\f\r\v"; + + /* + * These are the identifiers used in mdo.conf, and refer to nss names. + * They are NOT the identifiers used to configure the MAC/do kernel + * module; see the 'matcher' and 'target' classes for those. + */ +constexpr std::string_view + subject_type_user = "user", + subject_type_group = "group", + subject_type_sgroup = "+group", + subject_type_force_sgroup = "!group", + subject_type_deny_sgroup = "-group"; + +/* Prototypes */ +void usage(); +auto mdo_enabled() -> mdo_status; +int read_file(std::string filename, std::output_iterator auto &&iter); +auto skipws(std::string_view text) -> std::string_view; +auto split(std::string_view text, std::string_view sep) + -> std::pair; +auto join(std::ranges::input_range auto &&elms, std::string_view sep) + -> std::string; +auto next_word(std::string_view text) + -> std::pair; +auto uidofname(std::string_view name) -> std::optional<::uid_t>; +auto gidofname(std::string_view name) -> std::optional<::gid_t>; +/* Commands */ +int c_status(int argc, char **argv); +int c_enable(int argc, char **argv); +int c_disable(int argc, char **argv); +int c_load(int argc, char **argv); + +/* The command table */ +std::unordered_map commands{ + std::pair{"status"sv, &c_status}, + std::pair{"enable"sv, &c_enable}, + std::pair{"disable"sv, &c_disable}, + std::pair{"load"sv, &c_load}, +}; + +/********************************************************************** + * Utility functions. + */ + +/* + * read_file: load the contents of a file into an output iterator. + * Return 0 on success, or an errno value on failure. + */ +int +read_file(std::string filename, std::output_iterator auto &&iter) +{ + constexpr std::size_t bufsize = 1024; + std::array buffer; + int fd, err; + + if ((fd = ::open(filename.c_str(), O_RDONLY)) == -1) + return (errno); + + auto fd_guard = guard([fd] { ::close(fd); }); + + while ((err = ::read(fd, &buffer[0], buffer.size())) > 0) { + auto data = std::span(buffer).subspan(0, err); + std::ranges::copy(data, iter); + } + + return (err ? 0 : errno); +} + +/* + * skipws: Return the provided string with leading whitespace removed. + */ +std::string_view +skipws(std::string_view text) +{ + auto pos = text.find_first_not_of(whitespace); + if (pos == std::string_view::npos) + return {}; + return (text.substr(pos)); +} + +/* + * Split a string on a delimiter, returning a pair consisting of the string up + * to but not including the delimiter, and the remainder of the string. + * Only the first delimiter is significant, so multiple sequential delimiters + * will be treated as empty fields. + */ +std::pair +split(std::string_view text, std::string_view sep) +{ + auto pos = text.find_first_of(sep); + if (pos == std::string_view::npos) + // The entire string is the final token. + return {text, {}}; + + return {text.substr(0, pos), text.substr(pos + 1)}; +} + +/* + * Join the elements of a range into a string using the provided separator. + * Each element is converted to a string as if by std::format. + */ +std::string +join(std::ranges::input_range auto &&elms, std::string_view sep) +{ + std::string ret; + + for (auto const &elm : elms) { + if (!ret.empty()) + ret += sep; + ret += std::format("{}", elm); + } + + return (ret); +} + +/* + * next_word: skip leading whitespace in the given string, then return + * the next whitespace-delimited word and the rest of the string. + */ +std::pair +next_word(std::string_view text) +{ + return (split(skipws(text), whitespace)); +} + +/* + * uidofname: convert a username into a uid. + */ +std::optional<::uid_t> +uidofname(std::string_view name) +{ + std::string cname(std::from_range, name); + if (auto const *pw = ::getpwnam(cname.c_str()); pw != nullptr) + return {pw->pw_uid}; + return {}; +} + +/* + * gidofname: convert a group name into a gid. + */ +std::optional<::gid_t> +gidofname(std::string_view name) +{ + std::string cname(std::from_range, name); + if (auto const *gr = ::getgrnam(cname.c_str()); gr != nullptr) + return {gr->gr_gid}; + return {}; +} + +/********************************************************************** + * The rule parser. + */ + +// A generic exception indicating some sort of parsing error occurred. +// what() should be informative. +struct parse_error final : std::runtime_error { + template + parse_error(std::format_string fmt, Args &&...args) + : std::runtime_error(std::format(fmt, + std::forward(args)...)) + {} +}; + +/* + * A matcher (uid or gid). + * This is what MAC/do calls the "from" part of the rule, i.e. a user or group + * which the rest of the rule applies to. + */ +struct matcher final { + std::string type; // The type of thing we match, e.g. "uid" + std::string id; // The subject we match, e.g. "0". + + // Predefined values for 'type'. + static inline const std::string type_uid = "uid"; + static inline const std::string type_gid = "gid"; + + // Format this matcher in MAC/do format. + std::string format() const { + return (std::format("{}={}", type, id)); + } + + /* + * Static constructors. + */ + + // A matcher that matches a specific uid. + static matcher uid(::uid_t userid) { + return matcher(type_uid, std::to_string(userid)); + } + + // A matcher that matches a specific gid. + static matcher gid(::gid_t grpid) { + return matcher(type_gid, std::to_string(grpid)); + } + + /* + * Constructors. To create new instances of this type, you must use + * one of the static constructors defined above. + */ + + matcher(matcher const &) = default; + matcher(matcher &&) noexcept = default; + +private: + matcher(std::string type_, std::string id_) + : type(std::move(type_)) + , id(std::move(id_)) + {} +}; + +/* + * Parse a matcher for a given subject. If the subject starts with '%', + * then it's a group name, otherwise it's a username. No restrictions + * are imposed on the format aside from what NSS requires. + * + * On error, throws parse_error. + */ +matcher +parse_matcher(std::string_view text) +{ + // This should probably not happen. + if (text.empty()) + throw parse_error("invalid empty subject"); + + // If the string starts with '%', then it's a group name. + if (text[0] == '%') { + auto group = text.substr(1); + if (auto gid = gidofname(group); gid) + return (matcher::gid(*gid)); + else + throw parse_error("unknown group: '{}'", group); + } + + // Otherwise, it's a username. + if (auto uid = uidofname(text); uid) + return (matcher::uid(*uid)); + throw parse_error("unknown user: '{}'", text); +} + +/* + * A target constraint. + * This is what MAC/do calls the "target" part of the rule; it describes what + * the subject is allowed to do once the rule matches. + */ +struct target final { + std::string type; // The type of thing we match, e.g. "uid" + std::string id; // The subject we match, e.g. "0". + + // Predefined values for 'type'. + static inline const std::string type_uid = "uid"; + static inline const std::string type_gid = "gid"; + static inline const std::string type_sgid = "+gid"; + static inline const std::string type_force_sgid = "!gid"; + static inline const std::string type_deny_sgid = "-gid"; + // Predefined values for 'id'. + static inline const std::string id_any = "*"; + + // Format this target in MAC/do format. + std::string format() const { + return (std::format("{}={}", type, id)); + } + + /* + * Static constructors. + */ + + // Permit a specific uid. + static target uid(::uid_t id) { + return target(type_uid, std::to_string(id)); + } + + // Permit any uid. + static target uid() { + return target(type_uid, id_any); + } + + // Permit a specific gid. + static target gid(::gid_t id) { + return target(type_gid, std::to_string(id)); + } + + // Permit any gid. + static target gid() { + return target(type_gid, id_any); + } + + // Permit a specific supplemental gid. + static target sgid(::gid_t id) { + return target(type_sgid, std::to_string(id)); + } + + // Permit any supplemental gid. + static target sgid() { + return target(type_sgid, id_any); + } + + // Deny a specific supplemental gid. + static target deny_sgid(::gid_t id) { + return target(type_deny_sgid, std::to_string(id)); + } + + // Require a specific supplemental gid. + static target force_sgid(::gid_t id) { + return target(type_force_sgid, std::to_string(id)); + } + + /* + * Constructors. To create new instances of this type, you must use + * one of the static constructors defined above. + */ + + target(target const &) = default; + target(target &&) noexcept = default; + +private: + target(std::string type_, std::string id_) + : type(std::move(type_)) + , id(std::move(id_)) + {} +}; + +/* + * Parse a user target. + * Note that while MAC/do accepts both "*" and "any" to match any user, we only + * accept "*", because "any" is a valid username. + */ +target +parse_user_target(std::string_view text) +{ + // "*" matches any user + if (text == target::id_any) + return (target::uid()); + + // Otherwise, we should have a username + if (auto uid = uidofname(text); uid) + return (target::uid(*uid)); + + throw parse_error("unknown user '{}'", text); +} + +/* + * Parse a group target. + * Note that while MAC/do accepts both "*" and "any" to match any group, we only + * accept "*", because "any" is a valid username. + */ +target +parse_group_target(std::string_view text) +{ + // "*" matches any group + if (text == target::id_any) + return target::gid(); + + // Otherwise, we should have a group name. + if (auto gid = gidofname(text); gid) + return target::gid(*gid); + + throw parse_error("unknown group '{}'", text); +} + +/* + * Parse a +group target. + */ +target +parse_sgroup_target(std::string_view text) +{ + // "*" matches any group + if (text == "*") + return target::sgid(); + + // Otherwise we should have a group name + if (auto gid = gidofname(text); gid) + return target::sgid(*gid); + + throw parse_error("unknown group '{}'", text); +} + +/* + * Parse a "!group" target. + */ +target +parse_forcegroup_target(std::string_view text) +{ + // "*" is not supported with !group + if (text == target::id_any) + throw parse_error("cannot use \"{}\" with \"{}\"", + text, subject_type_force_sgroup); + + // So we must have a group name. + if (auto gid = gidofname(text); gid) + return (target::force_sgid(*gid)); + + throw parse_error("unknown group '{}'", text); +} +/* + * Parse a "-group" target. + */ +target +parse_denygroup_target(std::string_view text) +{ + // "*" is not supported with -group + if (text == target::id_any) + throw parse_error("cannot use \"{}\" with \"{}\"", + text, subject_type_deny_sgroup); + + // So we must have a group name. + if (auto gid = gidofname(text); gid) + return (target::deny_sgid(*gid)); + + throw parse_error("unknown group '{}'", text); +} + +/* + * Parse a target of some sort. + */ +target +parse_target(std::string_view text) +{ + // The target types we can parse. + static const std::unordered_map targets{ + std::pair{subject_type_user, &parse_user_target}, + std::pair{subject_type_group, &parse_group_target}, + std::pair{subject_type_sgroup, &parse_sgroup_target}, + std::pair{subject_type_force_sgroup, &parse_forcegroup_target}, + std::pair{subject_type_deny_sgroup, &parse_denygroup_target}, + }; + auto [type, value] = split(text, "="); + + if (auto it = targets.find(type); it != targets.end()) + return (it->second(value)); + + throw parse_error("invalid target type '{}'", type); +} + +/* + * A rule, containing a matcher and a list of targets + */ +struct rule final { + matcher subject; + std::vector targets; + + rule(matcher subject_) : subject(std::move(subject_)) {} + + // Create a new rule from a subject and a range of targets. + rule(matcher subject_, std::ranges::range auto &&targets_) + : subject(subject_) + , targets(std::from_range, targets_) + {} + + rule(rule const &) = default; + rule(rule &&) noexcept = default; + + /* + * Format this rule in the MAC/do kernel format. + */ + std::string format() const { + /* + * The mac_do rule format is: + * SUBJECT>TARGET[,TARGET...] + */ + auto target_text = join( + std::views::transform(targets, &target::format), + ","); + auto rule_text = std::format("{}>{}", + subject.format(), target_text); + return (rule_text); + } +}; + +/* + * Parse a single rule and return a rule object. + * + * A rule consists of a single matcher, followed by one or more all targets, all + * separated by whitespace. + */ +rule +parse_rule(std::string_view text) +{ + std::string_view subject_text, target_text; + std::vector targets; + + // The rule should start with a subject matcher. + std::tie(subject_text, text) = next_word(text); + if (subject_text.empty()) + throw parse_error("missing subject"); + + // Parse the subject. + matcher subject = parse_matcher(subject_text); + + // The target list cannot be empty. + text = skipws(text); + if (text.empty()) + throw parse_error("no target specified"); + + // Parse whitespace-separated targets. + do { + std::tie(target_text, text) = next_word(text); + targets.push_back(parse_target(target_text)); + text = skipws(text); + } while (!text.empty()); + + // Create the rule. + return (rule(std::move(subject), std::move(targets))); +} + +/* + * mdo_enabled: return a constant indicating whether MAC/do is enabled + * mdo_status::enabled -> enabled + * mdo_status::disabled -> disabled + * mdo_status::not_loaded -> the mac_do module isn't loaded (or we couldn't + * fetch the sysctl for some reason) + */ +mdo_status +mdo_enabled() +{ + int status_value = 0; + std::size_t status_size = sizeof(status_value); + + if (sysctlbyname(sysctl_enabled.c_str(), &status_value, &status_size, + nullptr, 0) != 0) + return (mdo_status::not_loaded); + + return (status_value ? mdo_status::enabled : mdo_status::disabled); +} + +/********************************************************************** + * Command handlers. + */ + +/* + * status: show the current status of MAC/do. + */ +int +c_status(int argc, char **) +{ + if (argc != 1) { + usage(); + return (1); + } + + switch (mdo_enabled()) { + case mdo_status::enabled: + std::print("enabled\n"); + break; + case mdo_status::disabled: + std::print("disabled\n"); + break; + case mdo_status::not_loaded: + std::print("not loaded\n"); + break; + default: + std::print("unknown\n"); + } + + return (0); +} + +/* + * c_enable: turn MAC/do on + */ +int +c_enable(int argc, char **) +{ + int value; + std::size_t size; + + if (argc != 1) { + usage(); + return (1); + } + + value = 1; + size = sizeof(value); + if (sysctlbyname(sysctl_enabled.c_str(), nullptr, 0, + &value, size) == 0) + return (0); + + if (errno == ENOENT) + std::print(std::cerr, "{}: MAC/do is not loaded\n", + getprogname()); + else + std::print(std::cerr, "{}: {}\n", + getprogname(), strerror(errno)); + return (1); +} + +/* + * c_disable: turn MAC/do off + */ +int +c_disable(int argc, char **) +{ + int value; + std::size_t size; + + if (argc != 1) { + usage(); + return (1); + } + + value = 0; + size = sizeof(value); + if (sysctlbyname(sysctl_enabled.c_str(), nullptr, 0, + &value, size) == 0) + return (0); + + if (errno == ENOENT) + std::print(std::cerr, "{}: MAC/do is not loaded\n", + getprogname()); + else + std::print(std::cerr, "{}: {}\n", + getprogname(), strerror(errno)); + return (1); +} + +/* + * c_load: load MAC/do rules from a file into the kernel + */ +int +c_load(int argc, char **argv) +{ + std::vector rules; + std::string config, rules_text, cfgpath; + std::string_view rest; + unsigned lineno; + int ch; + + // Parse our arguments. + + cfgpath = default_config_path; + while ((ch = getopt(argc, argv, "f:")) != -1) { + switch (ch) { + case 'f': + cfgpath = optarg; + break; + case '?': + default: + usage(); + return (-1); + } + } + + argc -= optind; + argv += optind; + + if (argc != 0) { + usage(); + return (1); + } + + // Load the text of the configuration file. + if (auto err = read_file(cfgpath, std::back_inserter(config)); + err != 0) { + std::print(std::cerr, "{}: {}\n", cfgpath, strerror(err)); + return (1); + } + + // Parse each line of the configuration file into a rule. + lineno = 0; + rest = std::string_view(config); + do { + std::string_view line; + + // Fetch the next line. + std::tie(line, rest) = split(rest, "\n"); + ++lineno; + + // Remove leading whitespace + line = skipws(line); + + // Skip comments and empty lines + if (line.empty() || line[0] == '#') + continue; + + // Parse the rule. + try { + rules.push_back(parse_rule(line)); + } catch (parse_error const &exc) { + std::print(std::cerr, "{}:{}: error: {}\n", + cfgpath, lineno, exc.what()); + return (1); + } + } while (!rest.empty()); + + // We parsed all the rules successfully, so format and concat them + rules_text = join(std::views::transform(rules, &rule::format), ";"); + + // Load the new rules into the kernel. + if (sysctlbyname(sysctl_rules.c_str(), nullptr, 0, + rules_text.data(), rules_text.size()) == -1) { + std::print("{}: failed to configure kernel ruleset: {}\n", + getprogname(), strerror(errno)); + return (1); + } + + return (0); +} + +/* + * usage: print usage + */ +void +usage() +{ + std::print(std::cerr, +"usage: {0} disable\n" +" {0} enable\n" +" {0} load [-f ]\n" +" {0} status\n", + getprogname()); +} + +} // anonymous namespace + +int +main(int argc, char **argv) +{ + if (argc < 2) { + usage(); + return (1); + } + + setprogname(argv[0]); + + ++argv; + --argc; + + /* + * Dispatch the top-level command. + */ + + if (auto c = commands.find(argv[0]); c != commands.end()) + return (c->second(argc, argv)); + + std::print(std::cerr, "{}: unknown command '{}'\n", + getprogname(), argv[0]); + usage(); + return (1); +} + diff --git a/share/man/man5/Makefile b/share/man/man5/Makefile --- a/share/man/man5/Makefile +++ b/share/man/man5/Makefile @@ -27,6 +27,7 @@ link.5 \ mailer.conf.5 \ make.conf.5 \ + mdo.conf.5 \ moduli.5 \ motd.5 \ mount.conf.5 \ diff --git a/share/man/man5/mdo.conf.5 b/share/man/man5/mdo.conf.5 new file mode 100644 --- /dev/null +++ b/share/man/man5/mdo.conf.5 @@ -0,0 +1,75 @@ +.\" SPDX-License-Identifier: ISC +.\" Copyright (c) 2025 Lexi Winter. +.\" +.Dd May 24, 2025 +.Dt MDO.CONF 5 +.Os +.Sh NAME +.Nm mdo.conf +.Nd MAC/do configuration file +.Sh DESCRIPTION +The +.Nm +file is used by +.Xr mdoctl 8 +to configure the MAC/do framework (see +.Xr mac_do 4 ) . +.Pp +The file is formatted as a list of rules, one per line. +Empty lines and lines beginning with the +.Sq # +character are ignored. +Rules consist of a whitespace-separated list of +.Dq Ar name Ns Li = Ns Ar value +pairs. +The first field specifies the subject who is invoking +.Xr setcred 2 . +The remaining fields specify the targets permitted for that subject. +.Pp +The following subjects are supported: +.Bl -tag -indent +.It Cm user Ns = Ns Ar username +This rule applies to the user +.Ar username . +.It Cm group Ns = Ns % Ns Ar groupname +This rule applies to the group +.Ar groupname . +.El +.Pp +The following targets are supported: +.Bl -tag -indent +.It Cm user Ns = Ns Ar username +Allow the user id to be changed to +.Ar username . +.It Cm group Ns = Ns Ar groupname +Allow the primary group to be changed to +.Ar groupname . +.It Cm +group Ns = Ns Ar groupname +Allow the supplemental group list to include +.Ar groupname . +.It Cm !group Ns = Ns Ar groupname +Require the supplemental group list to include +.Ar groupname . +.It Cm -group Ns = Ns Ar groupname +Forbid the supplemental group list to include +.Ar groupname . +.Bl +.Sh EXAMPLE +To allow anyone in the group +.Dq wheel +to change their user id to +.Dq root , +with no restriction on the supplemental group list: +.Bd -literal -offset indent +%wheel user=root group=* +group=* +.Ed +.Sh SEE ALSO +.Xr mac_do 4 , +.Xr mdoctl 8 , +.Xr mdo 1 , +.Sh HISTORY +The +.Nm +file appeared in +.Fx 15.0 . + diff --git a/share/man/man5/rc.conf.5 b/share/man/man5/rc.conf.5 --- a/share/man/man5/rc.conf.5 +++ b/share/man/man5/rc.conf.5 @@ -22,7 +22,7 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd May 21, 2025 +.Dd May 24, 2025 .Dt RC.CONF 5 .Os .Sh NAME @@ -4940,6 +4940,24 @@ .Dq Li YES , these are the flags to pass to the .Xr sendmail 8 +.It Va mdoctl_enable +.Pq Vt bool +If set to +.Dq Li YES , +run +.Xr mdoctl 8 +at startup to configure MAC/do rules. +.It Va mdoctl_program +.Pq Vt str +The path to +.Pa mdoctl . +The default path is +.Pa /sbin/mdoctl . +.It Va mdoctl_config +.Pq Vt str +The MAC/do configuration to load. +The default configuration file is +.Pa /etc/mdo.conf . .It Va precious_machine If set to .Dq Li YES ,