From: Dermot Bradley Date: Thu, 26 Aug 2021 00:58 +0100 Subject: [PATCH] cloud-init: Add doas support Add doas support to users_groups module. --- diff -aur a/cloudinit/config/cc_users_groups.py b/cloudinit/config/cc_users_groups.py --- a/cloudinit/config/cc_users_groups.py +++ b/cloudinit/config/cc_users_groups.py @@ -54,14 +54,20 @@ if the cloud-config can be intercepted. SSH authentication is preferred. .. note:: + If specifying a doas rule for a user, ensure that the syntax for the rule + is valid, as the only checking performed by cloud-init is to ensure that + the user referenced in the rule is the correct user. + +.. note:: If specifying a sudo rule for a user, ensure that the syntax for the rule is valid, as it is not checked by cloud-init. .. note:: Most of these configuration options will not be honored if the user already exists. The following options are the exceptions; they are applied - to already-existing users: ``plain_text_passwd``, ``hashed_passwd``, - ``lock_passwd``, ``sudo``, ``ssh_authorized_keys``, ``ssh_redirect_user``. + to already-existing users: ``plain_text_passwd``, ``doas``, + ``hashed_passwd``, ``lock_passwd``, ``sudo``, ``ssh_authorized_keys``, + ``ssh_redirect_user``. The ``user`` key can be used to override the ``default_user`` configuration defined in ``/etc/cloud/cloud.cfg``. The ``user`` value should be a dictionary diff -aur a/cloudinit/config/schemas/schema-cloud-config-v1.json b/cloudinit/config/schemas/schema-cloud-config-v1.json --- a/cloudinit/config/schemas/schema-cloud-config-v1.json +++ b/cloudinit/config/schemas/schema-cloud-config-v1.json @@ -140,6 +140,24 @@ "description": "The user's login name. Required otherwise user creation will be skipped for this user.", "type": "string" }, + "doas": { + "description": "List of Doas rules to use. Absence of a doas value or ``null`` will result in no doas rules added for this user.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + { + "type": [ + "string", + "null" + ] + } + ] + }, "expiredate": { "default": null, "description": "Optional. Date on which the user's account will be disabled. Default: ``null``", diff -aur a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -93,6 +93,7 @@ pip_package_name = "python3-pip" usr_lib_exec = "/usr/lib" hosts_fn = "/etc/hosts" + ci_doas_fn = "/etc/doas.conf" ci_sudoers_fn = "/etc/sudoers.d/90-cloud-init-users" hostname_conf_fn = "/etc/hostname" tz_zone_dir = "/usr/share/zoneinfo" @@ -655,6 +656,7 @@ * ``plain_text_passwd`` * ``hashed_passwd`` * ``lock_passwd`` + * ``doas`` * ``sudo`` * ``ssh_authorized_keys`` * ``ssh_redirect_user`` @@ -680,6 +682,11 @@ if kwargs.get("lock_passwd", True): self.lock_passwd(name) + # Configure doas access + if 'doas' in kwargs: + if kwargs['doas']: + self.write_doas_rules(name, kwargs['doas']) + # Configure sudo access if "sudo" in kwargs: if kwargs["sudo"]: @@ -785,6 +792,80 @@ cmd = ["chpasswd"] + (["-e"] if hashed else []) subp.subp(cmd, payload) + def is_doas_rule_valid(self, user, rule): + rule_pattern = r"^(?:permit|deny)" + \ + r"(?:\s+(?:nolog|nopass|persist|keepenv|setenv \{[^}]+\})+)*" + \ + r"\s+([a-zA-Z0-9_]+)+" + \ + r"(?:\s+as\s+[a-zA-Z0-9_]+)*" + \ + r"(?:\s+cmd\s+[^\s]+(?:\s+args\s+[^\s]+(?:\s*[^\s]+)*)*)*" + \ + r"\s*$" + + LOG.debug("Checking if user '%s' is referenced in doas rule %r", + user, rule) + + valid_match = re.search(rule_pattern, rule) + if valid_match: + LOG.debug("User '%s' referenced in doas rule", + valid_match.group(1)) + if valid_match.group(1) == user: + LOG.debug("Correct user is referenced in doas rule") + return True + else: + LOG.debug("Incorrect user '%s' is referenced in doas rule", + valid_match.group(1)) + return False + else: + LOG.debug("Doas rule does not appear to reference any user") + return False + + def write_doas_rules(self, user, rules, doas_file=None): + if not doas_file: + doas_file = self.ci_doas_fn + + if isinstance(rules, (list, tuple)): + for rule in rules: + if not self.is_doas_rule_valid(user, rule): + msg = "Invalid Doas rule %r for user '%s', not writing any Doas rules for user!" % (rule, user) + LOG.error(msg) + return + elif isinstance(rules, str): + if not self.is_doas_rule_valid(user, rules): + msg = "Invalid Doas rule %r for user '%s', not writing any Doas rules for user!" % (rules, user) + LOG.error(msg) + return + + lines = [ + "", + "# cloud-init User rules for %s" % user, + ] + if isinstance(rules, (list, tuple)): + for rule in rules: + lines.append("%s" % rule) + elif isinstance(rules, str): + lines.append("%s" % rules) + else: + msg = "Can not create Doas rule addition with type %r" + raise TypeError(msg % (type_utils.obj_name(rules))) + content = "\n".join(lines) + content += "\n" # trailing newline + + if not os.path.exists(doas_file): + contents = [ + util.make_header(), + content, + ] + try: + util.write_file(doas_file, "\n".join(contents), mode=0o440) + except IOError as e: + util.logexc(LOG, "Failed to write Doas file %s", doas_file) + raise e + else: + try: + util.append_file(doas_file, content) + except IOError as e: + util.logexc(LOG, "Failed to append Doas file %s", doas_file) + raise e + def ensure_sudo_dir(self, path, sudo_base="/etc/sudoers"): # Ensure the dir is included and that # it actually exists as a directory diff -aur a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -278,6 +278,9 @@ lock_passwd: True gecos: {{ variant }} Cloud User {% endif %} +{% if variant == "alpine" %} + doas: ["permit nopass alpine"] +{% endif %} {% if variant == "suse" %} groups: [cdrom, users] {% elif variant == "gentoo" %} @@ -285,7 +288,7 @@ primary_group: users no_user_group: true {% elif variant == "alpine" %} - groups: [adm, sudo] + groups: [adm, sudo, wheel] {% elif variant == "arch" %} groups: [wheel, users] {% elif variant == "openmandriva" %}