HEX
Server: nginx/1.29.3
System: Linux mail.sarafai.ru 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64
User: www-data (33)
PHP: 7.4.33
Disabled: dl,exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,show_source
Upload Files
File: //usr/lib/cloudinit/config/cc_custom_ssh_authorized_keys.py
# Author: Tatiana Kholkina <holkina@selectel.ru>

"""
Custom SSH Authorized Keys
--------------------------
**Summary:** ssh authorized keys management

This module allows to add and delete SSH authorized keys.

Authorized keys for the default user/first user defined in ``users`` can be
specified using ``custom_ssh_authorized_keys``. Keys should be specified as a
list of public keys. The list of keys will be updated after an instance reboot.
To delete the key just remove it from the list and reboot an instance.
If ``custom_ssh_authorized_keys`` is empty all keys added via this config key
will be removed. If ``custom_ssh_authorized_keys`` is not presented in config
nothing will be changed.

Root login can be enabled/disabled using the ``disable_root`` config key. Root
login options can be manually specified with ``disable_root_opts``. If
``disable_root_opts`` is specified and contains the string ``$USER``,
it will be replaced with the username of the default user. By default,
root login is disabled, and root login opts are set to::

    no-port-forwarding,no-agent-forwarding,no-X11-forwarding

.. note::
    keys will be marked with a prefix to correctly determine them

.. note::
    if one of provided keys is invalid no changes will be applied

**Internal name:** ``cc_custom_ssh_authorized_keys``

**Module frequency:** always

**Supported distros:** all

**Config keys**::

    disable_root: <true/false>
    disable_root_opts: <disable root options string>
    custom_ssh_authorized_keys:
        - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA3FSyQwBI6Z+nCSjUU ...
        - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZ ...
"""

import os

from cloudinit.distros import ug_util
from cloudinit.log import logging
from cloudinit.settings import PER_ALWAYS
from cloudinit import ssh_util
from cloudinit import util

try:
    from cloudinit.config.cc_ssh import DISABLE_ROOT_OPTS as DISABLE_USER_OPTS
except ImportError:
    from cloudinit.ssh_util import DISABLE_USER_OPTS

LOG = logging.getLogger(__name__)

frequency = PER_ALWAYS

KEY_PREFIX = 'cloud-init'


def handle(_name, cfg, cloud, log, _args):
    if "custom_ssh_authorized_keys" in cfg:
        # Get user to add authorized_keys to (the same as in cc_ssh)
        (users, _groups) = ug_util.normalize_users_groups(cfg, cloud.distro)
        (user, _user_config) = ug_util.extract_default(users)
        disable_root = util.get_cfg_option_bool(cfg, "disable_root", True)
        disable_root_opts = util.get_cfg_option_str(cfg, "disable_root_opts",
                                                    DISABLE_USER_OPTS)

        content = cfg["custom_ssh_authorized_keys"] or []

        if not user:
            user = 'root'
        try:
            apply_credentials(content, user, disable_root, disable_root_opts)
        except Exception:
            util.logexc(log, "Applying ssh credentials failed!")


def apply_credentials(new_keys, user, disable_root, disable_root_opts):
    if user:
        setup_user_keys(new_keys, user)
    else:
        if disable_root:
            if not user:
                user = "NONE"
            key_prefix = disable_root_opts.replace('$USER', user)
        else:
            key_prefix = ''

        setup_user_keys(new_keys, 'root', options=key_prefix)


def setup_user_keys(new_keys, username, options=None):
    # Make sure the users .ssh dir is setup accordingly
    (ssh_dir, pwent) = ssh_util.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)

    # Turn the 'update' keys given into actual entries.
    parser = ssh_util.AuthKeyLineParser()
    key_entries = []
    for k in new_keys:
        key_entries.append(parser.parse(str(k), options=options))

    if not validate_and_add_prefix(key_entries):
        LOG.warning("Keys added via custom_ssh_authorized_keys "
                    "are invalid. Changes are not applied.")
        return

    # Extract existing keys from .ssh/authorized_keys
    (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(username)
    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)


def update_authorized_keys(existing_keys, new_keys):
    # Remove keys with prefix
    keys_to_add = list(filter(lambda k: not k.comment.startswith(KEY_PREFIX),
                              existing_keys))
    for key in new_keys:
        keys_to_add.append(key)

    # Now format them back to strings...
    lines = [str(b) for b in keys_to_add]

    # Ensure it ends with a newline
    lines.append('')
    return '\n'.join(lines)


def validate_and_add_prefix(keys):
    for key in keys:
        if not key.valid():
            return False
        key.comment = '%(prefix)s-%(comment)s' % {
            'prefix': KEY_PREFIX,
            'comment': key.comment
        }
    return True