Source code for saltext.vault.states.vault_ssh

"""
Manage the Vault (or OpenBao) SSH secret engine.

.. versionadded:: 1.2.0

.. important::
    This module requires the general :ref:`Vault setup <vault-setup>`.
"""

import logging

import salt.utils.dictdiffer
from salt.exceptions import CommandExecutionError
from salt.exceptions import SaltInvocationError

from saltext.vault.utils.vault.helpers import deserialize_csl
from saltext.vault.utils.vault.helpers import timestring_map

log = logging.getLogger(__name__)

LIST_ROLE_PARAMS = (
    "allowed_users",
    "allowed_critical_options",
    "allowed_domains",
    "allowed_extensions",
    "cidr_list",
    "exclude_cidr_list",
)
MAP_ROLE_PARAMS = ("default_critical_options", "default_extensions", "allowed_user_key_lengths")
TIME_ROLE_PARAMS = ("ttl", "max_ttl", "not_before_duration")


[docs] def ca_present( name, private_key=None, public_key=None, key_type="ssh-rsa", key_bits=0, mount="ssh", ): """ Ensure a CA is present on the mount. Note that only one is possible per mount. This state does not inspect the properties once a CA has been initialized. name Irrelevant. private_key Private key part of the SSH CA key pair. Can be a file local to the minion or a PEM-encoded string. If this or ``public_key`` is unspecified, generates a pair on the Vault server. public_key Public key part of the SSH CA key pair. Can be a file local to the minion or a PEM-encoded string. If this or ``public_key`` is unspecified, generates a pair on the Vault server. key_type Desired key type for the generated SSH CA key when generating on the Vault server. Valid: ``ssh-rsa`` (default), ``sha2-nistp256``, ``ecdsa-sha2-nistp384``, ``ecdsa-sha2-nistp521``, or ``ssh-ed25519``. Can also specify an algorithm: ``rsa``, ``ec``, or ``ed25519``. key_bits Desired key bits for the generated SSH CA key when generating on the Vault server. Only used for variable length keys (e.g. ``ssh-rsa``) or when ``ec`` was specified as ``key_type``, in which case this selects the NIST P-curve: ``256``, ``384``, ``521``. 0 (default) selects 4096 bits for RSA or NIST P-256 for EC. mount Name of the mount point the SSH secret backend is mounted at. Defaults to ``ssh``. """ ret = { "name": name, "result": True, "comment": "The CA has already been initialized", "changes": {}, } try: try: current = __salt__["vault_ssh.read_ca"](mount=mount) except CommandExecutionError as err: if ( "keys haven't been configured yet" not in err.message # Vault and "no default issuer currently configured" not in err.message # OpenBao ): raise current = None if current: return ret if __opts__["test"]: ret["result"] = None ret["comment"] = "The CA would have been initialized" ret["changes"]["created"] = f"SSH CA on mount {mount}" return ret ca = __salt__["vault_ssh.create_ca"]( private_key=private_key, public_key=public_key, key_type=key_type, key_bits=key_bits, mount=mount, ) ret["comment"] = "The CA has been initialized" ret["changes"]["created"] = ca except (CommandExecutionError, SaltInvocationError) as err: ret["result"] = False ret["comment"] = str(err) ret["changes"] = {} return ret
[docs] def ca_absent( name, mount="ssh", ): """ Ensure a CA is absent from the mount. Note that you are unable to easily recover a destroy private key. name Irrelevant. mount Name of the mount point the SSH secret backend is mounted at. Defaults to ``ssh``. """ ret = { "name": name, "result": True, "comment": "There is no CA on the mount", "changes": {}, } try: try: current = __salt__["vault_ssh.read_ca"](mount=mount) except CommandExecutionError as err: if ( "keys haven't been configured yet" not in err.message # Vault and "no default issuer currently configured" not in err.message # OpenBao ): raise current = None if not current: return ret if __opts__["test"]: ret["result"] = None ret["comment"] = "The CA would have been destroyed" ret["changes"]["destroyed"] = f"SSH CA on mount {mount}" return ret ca = __salt__["vault_ssh.destroy_ca"](mount=mount) ret["comment"] = "The CA has been destroyed" ret["changes"]["destroyed"] = ca except (CommandExecutionError, SaltInvocationError) as err: ret["result"] = False ret["comment"] = str(err) ret["changes"] = {} return ret
[docs] def role_present_otp( name, default_user, cidr_list=None, allowed_users=None, exclude_cidr_list=None, port=None, mount="ssh", ): """ Ensure an SSH role (OTP type) is present as specified. name Name of the SSH role. default_user Default username for which a credential is generated. Required. cidr_list List of CIDR blocks to which the role is applicable. Required, unless the role is registered as a zero-address role. allowed_users List of usernames the client can request under this role. By default, **any usernames are allowed** (``*``). The ``default_user`` is always allowed. exclude_cidr_list List of CIDR blocks not accepted by the role. port Specifies the port number for SSH connections, which is returned to OTP clients as an informative value. Defaults to ``22``. mount Name of the mount point the SSH secret backend is mounted at. Defaults to ``ssh``. """ ret = { "name": name, "result": True, "comment": "The role is present as specified", "changes": {}, } try: port = int(port) if port else None except TypeError: ret["result"] = False ret["comment"] = "'port' must be castable to an integer" return ret payload = { "default_user": default_user, "cidr_list": cidr_list, "allowed_users": allowed_users, "exclude_cidr_list": exclude_cidr_list, "port": port, } return _role_present(name, "otp", ret, mount=mount, **payload)
def _diff_role_params(curr, wanted): diff = {} for param, val in wanted.items(): if param in LIST_ROLE_PARAMS: curr_param = set(deserialize_csl(curr.get(param, []))) wanted_param = set(deserialize_csl(val or [])) added = wanted_param - curr_param removed = curr_param - wanted_param if added or removed: diff[param] = {"added": list(sorted(added)), "removed": list(sorted(removed))} continue if param in MAP_ROLE_PARAMS: val = val or {} if param == "allowed_user_key_lengths": for algo, allowed in val.items(): if isinstance(allowed, int): val[algo] = [allowed] else: val[algo] = deserialize_csl(allowed) map_diff = salt.utils.dictdiffer.recursive_diff( curr.get(param, {}), val, ignore_missing_keys=False ) if map_diff.added() or map_diff.changed() or map_diff.removed(): diff[param] = { "added": map_diff.added(), "changed": map_diff.changed(), "removed": map_diff.removed(), } continue curr_val = curr.get(param) if param in TIME_ROLE_PARAMS: curr_val = timestring_map(curr_val) val = timestring_map(val) if curr_val != val: diff[param] = {"old": curr_val, "new": val} return diff def _role_present(name, key_type, ret, mount="ssh", **kwargs): try: try: current = __salt__["vault_ssh.read_role"](name, mount=mount) except CommandExecutionError as err: if "VaultNotFoundError" not in str(err): raise current = None ret["changes"]["created"] = name if current: if current["key_type"] != key_type: ret["changes"]["key_type"] = {"old": current["key_type"], "new": key_type} else: ret["changes"] = _diff_role_params(current, kwargs) if not ret["changes"]: return ret if __opts__["test"]: ret["result"] = None ret["comment"] = f"Role `{name}` would have been {'updated' if current else 'created'}" return ret __salt__[f"vault_ssh.write_role_{key_type}"]( name, **kwargs, mount=mount, ) try: new = __salt__["vault_ssh.read_role"](name, mount=mount) except CommandExecutionError as err: if "VaultNotFoundError" not in str(err): raise raise CommandExecutionError( "There were no errors during role management, but it is reported as absent." ) from err new_diff = _diff_role_params(new, kwargs) if new_diff: ret["result"] = False ret["comment"] = ( "There were no errors during role management, but " f"the reported parameters do not match: {new_diff}" ) else: ret["comment"] = f"Role `{name}` has been {'updated' if current else 'created'}" except (CommandExecutionError, SaltInvocationError) as err: ret["result"] = False ret["comment"] = str(err) ret["changes"] = {} return ret
[docs] def role_present_ca( name, default_user="", default_user_template=False, allowed_users=None, allowed_users_template=False, allowed_domains=None, allowed_domains_template=False, ttl=0, max_ttl=0, allowed_critical_options=None, allowed_extensions=None, default_critical_options=None, default_extensions=None, default_extensions_template=False, allow_user_certificates=False, allow_host_certificates=False, allow_bare_domains=False, allow_subdomains=False, allow_user_key_ids=False, key_id_format="", allowed_user_key_lengths=None, algorithm_signer="default", not_before_duration=30, mount="ssh", ): """ Ensure an SSH role (CA type) is present as specified. name Name of the SSH role. default_user Default username for which a credential is generated. When ``default_user_template`` is true, this can contain an identity template with any prefix or suffix, like ``ssh-{{identity.entity.id}}-user``. If you wish this to be a valid principal, it must also be in ``allowed_users``. default_user_template Allow ``default_users`` to be specified using identity template values. A non-templated user is also permitted. Defaults to false. allowed_users List of usernames the client can request under this role. By default, none are allowed. Set this to ``*`` to allow any usernames. If ``allowed_users_template`` is true, this list can contain an identity template with any prefix or suffix. The ``default_user`` is always allowed. allowed_users_template Allow ``allowed_users`` to be specified using identity template values. Non-templated users are also permitted. Defaults to false. allowed_domains List of domains for which a client can request a host certificate. ``*`` allows any domain. See also ``allow_bare_domains`` and ``allow_subdomains``. allowed_domains_template Allow ``allowed_domains`` to be specified using identity template values. Non-templated domains are also permitted. Defaults to false. ttl Specifies the Time To Live value provided as a string duration with time suffix. Hour is the largest suffix. If unset, uses the system default value or the value of ``max_ttl``, whichever is shorter max_ttl Specifies the maximum Time To Live provided as a string duration with time suffix. Hour is the largest suffix. If unset, defaults to the system maximum lease TTL. allowed_critical_options List of critical options that certificates can carry when signed. If unset (default), allows any option. allowed_extensions List of extensions that certificates can carry when signed. If unset (default), always takes the extensions from ``default_extensions`` only. If set to ``*``, allows any extension to be set. For the list of extensions, take a look at the sshd manual's AUTHORIZED_KEYS FILE FORMAT section. You should add a ``permit-`` before the name of extension to allow it. default_critical_options Map of critical options to their values certificates should carry if none are provided when signing. default_extensions Map of extensions to their values certificates should carry if none are provided when signing or allowed_extensions is unset. default_extensions_template Allow ``default_extensions`` to be specified using identity template values. Non-templated values are also permitted. Defaults to false. allow_user_certificates Allow certificates to be signed for ``user`` use. Defaults to false. allow_host_certificates Allow certificates to be signed for ``host`` use. Defaults to false. allow_bare_domains Allow host certificates to be signed for the base domains listed in ``allowed_domains``. This is a separate option as in some cases this can be considered a security threat. Defaults to false. allow_subdomains Allow host certificates to be signed for subdomains of the base domains listed in ``allowed_domains``. Defaults to false. allow_user_key_ids Allow users to override the key ID for a certificate. When false (default), the key ID always equals the token display name. The key ID is logged by the SSH server and can be useful for auditing. key_id_format Specifies a custom format for the key ID of a signed certificate. See `key_id_format <https://developer.hashicorp.com/vault/api-docs/secret/ssh#key_id_format>`_ for available template values. allowed_user_key_lengths Map of ssh key types to allowed sizes when signing with the CA type. Values can be a list of multiple sizes. Keys can both be OpenSSH-style key identifiers and short names (``rsa``, ``ecdsa``, ``dsa``, or ``ed25519``). If an algorithm has a fixed key size, values are ignored. algorithm_signer **RSA** algorithm to sign keys with. Valid: ``ssh-rsa``, ``rsa-sha2-256``, ``rsa-sha2-512``, or ``default`` (which is the default). Ignored when not signing with an RSA key. not_before_duration Specifies the duration by which to backdate the ``ValidAfter`` property. Defaults to ``30s``. mount Name of the mount point the SSH secret backend is mounted at. Defaults to ``ssh``. """ ret = { "name": name, "result": True, "comment": "The role is present as specified", "changes": {}, } if not (allow_user_certificates or allow_host_certificates): ret["result"] = False ret["comment"] = "Either allow_user_certificates or allow_host_certificates must be true" return ret payload = { "default_user": default_user, "default_user_template": default_user_template, "allowed_users": allowed_users, "allowed_users_template": allowed_users_template, "allowed_domains": allowed_domains, "allowed_domains_template": allowed_domains_template, "ttl": ttl, "max_ttl": max_ttl, "allowed_critical_options": allowed_critical_options, "allowed_extensions": allowed_extensions, "default_critical_options": default_critical_options, "default_extensions": default_extensions, "default_extensions_template": default_extensions_template, "allow_user_certificates": allow_user_certificates, "allow_host_certificates": allow_host_certificates, "allow_bare_domains": allow_bare_domains, "allow_subdomains": allow_subdomains, "allow_user_key_ids": allow_user_key_ids, "key_id_format": key_id_format, "allowed_user_key_lengths": allowed_user_key_lengths, "algorithm_signer": algorithm_signer, "not_before_duration": not_before_duration, } return _role_present(name, "ca", ret, mount=mount, **payload)
[docs] def role_absent(name, mount="ssh"): """ Ensure an SSH role is absent. name Name of the role. mount Name of the mount point the SSH secret backend is mounted at. Defaults to ``ssh``. """ ret = { "name": name, "result": True, "comment": "The role is already absent", "changes": {}, } try: try: __salt__["vault_ssh.read_role"](name, mount=mount) except CommandExecutionError as err: if "VaultNotFoundError" not in str(err): raise return ret ret["changes"]["deleted"] = name if __opts__["test"]: ret["result"] = None ret["comment"] = f"Role `{name}` would have been deleted." return ret __salt__["vault_ssh.delete_role"](name, mount=mount) try: __salt__["vault_ssh.read_role"](name, mount=mount) except CommandExecutionError as err: if "VaultNotFoundError" not in str(err): raise ret["comment"] = f"Role `{name}` has been deleted." return ret raise CommandExecutionError( "There were no errors during role deletion, but it is still reported as present." ) except CommandExecutionError as err: ret["result"] = False ret["comment"] = str(err) ret["changes"] = {} return ret