diff options
Diffstat (limited to 'cloudinit/ssh_util.py')
| -rw-r--r-- | cloudinit/ssh_util.py | 288 | 
1 files changed, 244 insertions, 44 deletions
diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py index d5113996..ab4c63aa 100644 --- a/cloudinit/ssh_util.py +++ b/cloudinit/ssh_util.py @@ -60,14 +60,16 @@ _DISABLE_USER_SSH_EXIT = 142  DISABLE_USER_OPTS = (      "no-port-forwarding,no-agent-forwarding," -    "no-X11-forwarding,command=\"echo \'Please login as the user \\\"$USER\\\"" -    " rather than the user \\\"$DISABLE_USER\\\".\';echo;sleep 10;" -    "exit " + str(_DISABLE_USER_SSH_EXIT) + "\"") +    'no-X11-forwarding,command="echo \'Please login as the user \\"$USER\\"' +    ' rather than the user \\"$DISABLE_USER\\".\';echo;sleep 10;' +    "exit " + str(_DISABLE_USER_SSH_EXIT) + '"' +)  class AuthKeyLine(object): -    def __init__(self, source, keytype=None, base64=None, -                 comment=None, options=None): +    def __init__( +        self, source, keytype=None, base64=None, comment=None, options=None +    ):          self.base64 = base64          self.comment = comment          self.options = options @@ -75,7 +77,7 @@ class AuthKeyLine(object):          self.source = source      def valid(self): -        return (self.base64 and self.keytype) +        return self.base64 and self.keytype      def __str__(self):          toks = [] @@ -90,7 +92,7 @@ class AuthKeyLine(object):          if not toks:              return self.source          else: -            return ' '.join(toks) +            return " ".join(toks)  class AuthKeyLineParser(object): @@ -121,8 +123,7 @@ class AuthKeyLineParser(object):          """          quoted = False          i = 0 -        while (i < len(ent) and -               ((quoted) or (ent[i] not in (" ", "\t")))): +        while i < len(ent) and ((quoted) or (ent[i] not in (" ", "\t"))):              curc = ent[i]              if i + 1 >= len(ent):                  i = i + 1 @@ -143,7 +144,7 @@ class AuthKeyLineParser(object):      def parse(self, src_line, options=None):          # modeled after opensshes auth2-pubkey.c:user_key_allowed2          line = src_line.rstrip("\r\n") -        if line.startswith("#") or line.strip() == '': +        if line.startswith("#") or line.strip() == "":              return AuthKeyLine(src_line)          def parse_ssh_key(ent): @@ -174,8 +175,13 @@ class AuthKeyLineParser(object):              except TypeError:                  return AuthKeyLine(src_line) -        return AuthKeyLine(src_line, keytype=keytype, base64=base64, -                           comment=comment, options=options) +        return AuthKeyLine( +            src_line, +            keytype=keytype, +            base64=base64, +            comment=comment, +            options=options, +        )  def parse_authorized_keys(fnames): @@ -218,15 +224,15 @@ def update_authorized_keys(old_entries, keys):      lines = [str(b) for b in old_entries]      # Ensure it ends with a newline -    lines.append('') -    return '\n'.join(lines) +    lines.append("") +    return "\n".join(lines)  def users_ssh_info(username):      pw_ent = pwd.getpwnam(username)      if not pw_ent or not pw_ent.pw_dir:          raise RuntimeError("Unable to get SSH info for user %r" % (username)) -    return (os.path.join(pw_ent.pw_dir, '.ssh'), pw_ent) +    return (os.path.join(pw_ent.pw_dir, ".ssh"), pw_ent)  def render_authorizedkeysfile_paths(value, homedir, username): @@ -249,35 +255,213 @@ def render_authorizedkeysfile_paths(value, homedir, username):      return rendered +# Inspired from safe_path() in openssh source code (misc.c). +def check_permissions(username, current_path, full_path, is_file, strictmodes): +    """Check if the file/folder in @current_path has the right permissions. + +    We need to check that: +    1. If StrictMode is enabled, the owner is either root or the user +    2. the user can access the file/folder, otherwise ssh won't use it +    3. If StrictMode is enabled, no write permission is given to group +       and world users (022) +    """ + +    # group/world can only execute the folder (access) +    minimal_permissions = 0o711 +    if is_file: +        # group/world can only read the file +        minimal_permissions = 0o644 + +    # 1. owner must be either root or the user itself +    owner = util.get_owner(current_path) +    if strictmodes and owner != username and owner != "root": +        LOG.debug( +            "Path %s in %s must be own by user %s or" +            " by root, but instead is own by %s. Ignoring key.", +            current_path, +            full_path, +            username, +            owner, +        ) +        return False + +    parent_permission = util.get_permissions(current_path) +    # 2. the user can access the file/folder, otherwise ssh won't use it +    if owner == username: +        # need only the owner permissions +        minimal_permissions &= 0o700 +    else: +        group_owner = util.get_group(current_path) +        user_groups = util.get_user_groups(username) + +        if group_owner in user_groups: +            # need only the group permissions +            minimal_permissions &= 0o070 +        else: +            # need only the world permissions +            minimal_permissions &= 0o007 + +    if parent_permission & minimal_permissions == 0: +        LOG.debug( +            "Path %s in %s must be accessible by user %s," +            " check its permissions", +            current_path, +            full_path, +            username, +        ) +        return False + +    # 3. no write permission (w) is given to group and world users (022) +    # Group and world user can still have +rx. +    if strictmodes and parent_permission & 0o022 != 0: +        LOG.debug( +            "Path %s in %s must not give write" +            "permission to group or world users. Ignoring key.", +            current_path, +            full_path, +        ) +        return False + +    return True + + +def check_create_path(username, filename, strictmodes): +    user_pwent = users_ssh_info(username)[1] +    root_pwent = users_ssh_info("root")[1] +    try: +        # check the directories first +        directories = filename.split("/")[1:-1] + +        # scan in order, from root to file name +        parent_folder = "" +        # this is to comply also with unit tests, and +        # strange home directories +        home_folder = os.path.dirname(user_pwent.pw_dir) +        for directory in directories: +            parent_folder += "/" + directory + +            # security check, disallow symlinks in the AuthorizedKeysFile path. +            if os.path.islink(parent_folder): +                LOG.debug( +                    "Invalid directory. Symlink exists in path: %s", +                    parent_folder, +                ) +                return False + +            if os.path.isfile(parent_folder): +                LOG.debug( +                    "Invalid directory. File exists in path: %s", parent_folder +                ) +                return False + +            if ( +                home_folder.startswith(parent_folder) +                or parent_folder == user_pwent.pw_dir +            ): +                continue + +            if not os.path.exists(parent_folder): +                # directory does not exist, and permission so far are good: +                # create the directory, and make it accessible by everyone +                # but owned by root, as it might be used by many users. +                with util.SeLinuxGuard(parent_folder): +                    mode = 0o755 +                    uid = root_pwent.pw_uid +                    gid = root_pwent.pw_gid +                    if parent_folder.startswith(user_pwent.pw_dir): +                        mode = 0o700 +                        uid = user_pwent.pw_uid +                        gid = user_pwent.pw_gid +                    os.makedirs(parent_folder, mode=mode, exist_ok=True) +                    util.chownbyid(parent_folder, uid, gid) + +            permissions = check_permissions( +                username, parent_folder, filename, False, strictmodes +            ) +            if not permissions: +                return False + +        if os.path.islink(filename) or os.path.isdir(filename): +            LOG.debug("%s is not a file!", filename) +            return False + +        # check the file +        if not os.path.exists(filename): +            # if file does not exist: we need to create it, since the +            # folders at this point exist and have right permissions +            util.write_file(filename, "", mode=0o600, ensure_dir_exists=True) +            util.chownbyid(filename, user_pwent.pw_uid, user_pwent.pw_gid) + +        permissions = check_permissions( +            username, filename, filename, True, strictmodes +        ) +        if not permissions: +            return False +    except (IOError, OSError) as e: +        util.logexc(LOG, str(e)) +        return False + +    return True + +  def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):      (ssh_dir, pw_ent) = users_ssh_info(username) -    default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys') +    default_authorizedkeys_file = os.path.join(ssh_dir, "authorized_keys") +    user_authorizedkeys_file = default_authorizedkeys_file      auth_key_fns = []      with util.SeLinuxGuard(ssh_dir, recursive=True):          try:              ssh_cfg = parse_ssh_config_map(sshd_cfg_file) +            key_paths = ssh_cfg.get( +                "authorizedkeysfile", "%h/.ssh/authorized_keys" +            ) +            strictmodes = ssh_cfg.get("strictmodes", "yes")              auth_key_fns = render_authorizedkeysfile_paths( -                ssh_cfg.get("authorizedkeysfile", "%h/.ssh/authorized_keys"), -                pw_ent.pw_dir, username) +                key_paths, pw_ent.pw_dir, username +            )          except (IOError, OSError):              # Give up and use a default key filename -            auth_key_fns.append(default_authorizedkeys_file) -            util.logexc(LOG, "Failed extracting 'AuthorizedKeysFile' in SSH " -                        "config from %r, using 'AuthorizedKeysFile' file " -                        "%r instead", DEF_SSHD_CFG, auth_key_fns[0]) +            auth_key_fns[0] = default_authorizedkeys_file +            util.logexc( +                LOG, +                "Failed extracting 'AuthorizedKeysFile' in SSH " +                "config from %r, using 'AuthorizedKeysFile' file " +                "%r instead", +                DEF_SSHD_CFG, +                auth_key_fns[0], +            ) + +    # check if one of the keys is the user's one and has the right permissions +    for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns): +        if any( +            [ +                "%u" in key_path, +                "%h" in key_path, +                auth_key_fn.startswith("{}/".format(pw_ent.pw_dir)), +            ] +        ): +            permissions_ok = check_create_path( +                username, auth_key_fn, strictmodes == "yes" +            ) +            if permissions_ok: +                user_authorizedkeys_file = auth_key_fn +                break -    # always store all the keys in the first file configured on sshd_config -    return (auth_key_fns[0], parse_authorized_keys(auth_key_fns)) +    if user_authorizedkeys_file != default_authorizedkeys_file: +        LOG.debug( +            "AuthorizedKeysFile has an user-specific authorized_keys, " +            "using %s", +            user_authorizedkeys_file, +        ) +    return ( +        user_authorizedkeys_file, +        parse_authorized_keys([user_authorizedkeys_file]), +    ) -def setup_user_keys(keys, username, options=None): -    # Make sure the users .ssh dir is setup accordingly -    (ssh_dir, pwent) = users_ssh_info(username) -    if not os.path.isdir(ssh_dir): -        util.ensure_dir(ssh_dir, mode=0o700) -        util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid) +def setup_user_keys(keys, username, options=None):      # Turn the 'update' keys given into actual entries      parser = AuthKeyLineParser()      key_entries = [] @@ -286,11 +470,10 @@ def setup_user_keys(keys, username, options=None):      # Extract the old and make the new      (auth_key_fn, auth_key_entries) = extract_authorized_keys(username) +    ssh_dir = os.path.dirname(auth_key_fn)      with util.SeLinuxGuard(ssh_dir, recursive=True):          content = update_authorized_keys(auth_key_entries, key_entries) -        util.ensure_dir(os.path.dirname(auth_key_fn), mode=0o700) -        util.write_file(auth_key_fn, content, mode=0o600) -        util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid) +        util.write_file(auth_key_fn, content, preserve_mode=True)  class SshdConfigLine(object): @@ -336,7 +519,15 @@ def parse_ssh_config_lines(lines):          try:              key, val = line.split(None, 1)          except ValueError: -            key, val = line.split('=', 1) +            try: +                key, val = line.split("=", 1) +            except ValueError: +                LOG.debug( +                    'sshd_config: option "%s" has no key/value pair,' +                    " skipping it", +                    line, +                ) +                continue          ret.append(SshdConfigLine(line, key, val))      return ret @@ -362,9 +553,10 @@ def update_ssh_config(updates, fname=DEF_SSHD_CFG):      changed = update_ssh_config_lines(lines=lines, updates=updates)      if changed:          util.write_file( -            fname, "\n".join( -                [str(line) for line in lines] -            ) + "\n", preserve_mode=True) +            fname, +            "\n".join([str(line) for line in lines]) + "\n", +            preserve_mode=True, +        )      return len(changed) != 0 @@ -388,12 +580,18 @@ def update_ssh_config_lines(lines, updates):              value = updates[key]              found.add(key)              if line.value == value: -                LOG.debug("line %d: option %s already set to %s", -                          i, key, value) +                LOG.debug( +                    "line %d: option %s already set to %s", i, key, value +                )              else:                  changed.append(key) -                LOG.debug("line %d: option %s updated %s -> %s", i, -                          key, line.value, value) +                LOG.debug( +                    "line %d: option %s updated %s -> %s", +                    i, +                    key, +                    line.value, +                    value, +                )                  line.value = value      if len(found) != len(updates): @@ -401,9 +599,11 @@ def update_ssh_config_lines(lines, updates):              if key in found:                  continue              changed.append(key) -            lines.append(SshdConfigLine('', key, value)) -            LOG.debug("line %d: option %s added with %s", -                      len(lines), key, value) +            lines.append(SshdConfigLine("", key, value)) +            LOG.debug( +                "line %d: option %s added with %s", len(lines), key, value +            )      return changed +  # vi: ts=4 expandtab  | 
