Source code for saltext.vault.states.vault_pki

"""
Manage the Vault (or OpenBao) PKI secret engine and Vault-issued X.509 certificates.

.. versionadded:: 1.1.0

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

import base64
import logging
import os
import tempfile
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from pathlib import Path
from typing import TYPE_CHECKING

from salt.exceptions import CommandExecutionError
from salt.exceptions import SaltInvocationError

from saltext.vault.utils.vault import helpers as hlp
from saltext.vault.utils.vault.helpers import timestring_map

try:
    from salt.utils import x509 as x509util

    from saltext.vault.utils.vault import pki

    HAS_CRYPTOGRAPHY = True
except ImportError:  # pragma: no cover
    HAS_CRYPTOGRAPHY = False


if TYPE_CHECKING:

    from saltext.vault.utils._types import SaltContext
    from saltext.vault.utils._types import SaltFunctions
    from saltext.vault.utils._types import SaltLogger
    from saltext.vault.utils._types import SaltLow
    from saltext.vault.utils._types import SaltOpts
    from saltext.vault.utils._types import SaltStates

    __opts__: SaltOpts
    __context__: SaltContext
    __salt__: SaltFunctions
    __states__: SaltStates
    __low__: SaltLow

log: "SaltLogger" = logging.getLogger(__name__)  # type: ignore

__virtualname__ = "vault_pki"


def __virtual__():
    try:
        __salt__["x509.encode_certificate"]  # pylint: disable=pointless-statement
    except KeyError:  # pragma: no cover
        return (
            False,
            "This state requires the x509_v2 execution module. "
            "x509_v2 needs to be explicitly enabled by setting `x509_v2: true` "
            "in the minion configuration value `features` until Salt 3008 (Argon).",
        )
    if not HAS_CRYPTOGRAPHY:  # pragma: no cover
        return (False, "Could not load cryptography")
    return __virtualname__


VALID_FILE_ARGS = (
    "user",
    "group",
    "mode",
    "attrs",
    "makedirs",
    "dir_mode",
    "backup",
    "create",
    "follow_symlinks",
    "check_cmd",
    "tmp_dir",
    "tmp_ext",
    "selinux",
    "file_encoding",
    "encoding_errors",
    "win_owner",
    "win_perms",
    "win_deny_perms",
    "win_inheritance",
    "win_perms_reset",
)


[docs] def certificate_managed( # pylint: disable=too-many-locals,too-many-statements name, common_name=None, role_name=None, private_key=None, csr=None, mount="pki", ttl="720h", ttl_remaining="168h", issuer_ref=None, encoding="pem", append_ca_chain=False, sign_verbatim=False, private_key_passphrase=None, reissue=False, *, # Vault sign args alt_names=None, # In theory sign-cert only, but the execution module syncs alt_names into subjectAltName exclude_cn_from_sans=False, # sign-cert only not_after=None, serial_number=None, # With sign-verbatim, needs to be specified in CSR user_ids=None, # With sign-verbatim, needs to be specified in CSR, even though it's documented # Vault sign-verbatim only args key_usage=None, ext_key_usage=None, ext_key_usage_oids=None, # Args for file.managed and x509.create_csr (no CN/subjectAltName though) **kwargs, ): """ Ensure an X.509 certificate is present as specified. .. note:: This state can use the ``sign-verbatim`` endpoint, which allows minute control of the certificate's subject name and most extensions (see ``sign_verbatim`` below). If not used, only CN is preserved from the CSR subject, any other subject name attributes are taken from the role instead. Check `this issue <https://github.com/hashicorp/vault/issues/17313>`__ for more information. .. versionchanged:: 1.9.0 Now compares all certificate subject attributes and extensions, including those that are derived from PKI role parameters and issuer URL configuration. This requires read access to the role, issuer and mount default URL configuration. If read access to any of these endpoints is denied, this state is most likely not idempotent anymore. Also, when ``issuer_ref`` is unspecified, now uses the generic ``{mount}/sign*`` endpoints instead of the issuer-specific ``{mount}/issuer/{issuer_ref}/sign/{role_name}`` with the explicit issuer_ref from the role. Required policy: .. code-block:: vaultpolicy # Need to read the role configuration in case of missing issuer_ref # and to more accurately predict changes. path "{mount}/roles/{role_name}" { capabilities = ["read"] } # Read mount default urls to account for cert extensions # if the issuer has no configured URLs. path "<mount>/config/urls" { capabilities = ["read"] } # Read issuer for URL configuration and CA chain. issuer_ref becomes `default` if unspecified path "{mount}/issuer/{issuer_ref}" { capabilities = ["read"] } # When sign_verbatim is false and not specifying issuer_ref path "<mount>/sign/<role_name>" { capabilities = ["update"] } # When sign_verbatim is false and specifying issuer_ref path "<mount>/issuer/<issuer_ref>/sign/<role_name>" { capabilities = ["update"] } # When sign_verbatim is true and neither specifying issuer_ref nor role_name path "<mount>/sign-verbatim" { capabilities = ["update"] } # When sign_verbatim is true and specifying role_name, but not issuer_ref path "<mount>/sign-verbatim/<role_name>" { capabilities = ["update"] } # When sign_verbatim is true and specifying issuer_ref, but not role_name path "<mount>/issuer/<issuer_ref>/sign-verbatim" { capabilities = ["update"] } # When sign_verbatim is true and specifying both issuer_ref and role_name path "<mount>/issuer/<issuer_ref>/sign-verbatim/<role_name>" { capabilities = ["update"] } name Path to the managed certificate file. common_name Subject common name (``CN``) for the certificate. Required, unless the role explicitly sets ``require_cn`` to false or ``sign_verbatim`` is true. Ignored (i.e. also not required) when a ``csr`` is passed that specifies it and the role's ``use_csr_common_name`` is true (the default value). role_name PKI role to use for issuing the certificate. Required, unless ``sign_verbatim`` is true. private_key Path or text of the private key to use for signing the CSR and thus as the private key for the certificate. Either this or ``csr`` is required. csr .. versionadded:: 1.9.0 Path or text of the CSR to use for issuing the certificate. Either this or ``private_key`` is required. mount Mount path the PKI backend is mounted to. Defaults to ``pki``. ttl Specifies the requested Time To Live (after which the certificate will be expired). This cannot be larger than the engine's max (or, if not set, the system max). Can be an integer, which is interpreted as seconds, or a time string such as ``1h``. Hour is the largest suffix. Defaults to ``720h`` or 30 days. ttl_remaining If an existing certificate's remaining Time To Live undercuts this period, renew it. Can be an integer, which is interpreted as seconds, or a time string such as ``1h``. Hour is the largest suffix. Defaults to ``168h`` or 7 days. issuer_ref Override the specified role's issuer for the certificate. Defaults to the one specified in the role. encoding Encoding of the managed certificate file. Valid options are ``pem``, ``pkcs7_pem``, ``der``, ``pkcs7_der``. Defaults to ``pem``. append_ca_chain Whether to append the CA chain to the certificate. Defaults to ``false``. .. note:: This appends all CA chain certificates of the selected issuer except self-signed (root) ones. sign_verbatim If set to true, the resulting certificate follows the CSR more or less exactly, including extensions. Otherwise, only ``CN`` can be set for the subject, any other subject parameters (like ``O``) are taken from the role. .. warning:: This option uses a potentially dangerous endpoint. Be careful when using that option, as roles are not restricting what can be issued anymore. private_key_passphrase Password for the private key if encrypted. reissue Always reissue the certificate. Defaults to ``false``. alt_names Any alternative names to add to the certificate. Can be specified either as dict (``{ "<type>": "<value>" }``), a dict of lists (``{ "<type>": ["<value1>", "<value2>", ...] }``) or list of SAN strings (``["<type1>:<value1>", ...]``). ``<type>`` can be ``dns``, ``email``, ``uri``, ``ip`` or any OID for otherName SANs. ``<value>`` is the corresponding value. Note that otherName SANs need to omit ``UTF8:``. Ignored when a ``csr`` is passed and the role's ``use_csr_sans`` is true (the default value). exclude_cn_from_sans If set to true, the Common Name is not added to the SANs. Useful if the CN is not a hostname or email address. Has no effect when ``sign_verbatim`` is true. not_after Absolute value of the Not After field of the certificate in UTC format ``YYYY-MM-ddTHH:MM:SSZ``. When set, ``ttl`` is ignored. serial_number Single value for the **subject** SERIALNUMBER (OID: 2.5.4.5) name attribute (NOT the certificate's serial number!). user_ids List of User ID (``UID``) subject attributes. Each one is added to the generated CSR's subject Name as a distinct RDN. key_usage When ``sign_verbatim`` is true, list of key usages to encode onto the certificate if the CSR does not specify a ``keyUsage`` extension. For non-verbatim issuance, this parameter must not be specified because Vault takes it from the role. Valid values can be found at https://golang.org/pkg/crypto/x509/#KeyUsage - simply drop the ``KeyUsage`` part of the value. Values are case-insensitive. Pass an empty list to specify no constraints. ext_key_usage When ``sign_verbatim`` is true, list of extended key usages to encode onto the certificate if the CSR does not specify a ``extendedKeyUsage`` extension. For non-verbatim issuance, this parameter must not be specified because Vault takes it from the role. Valid values can be found at https://golang.org/pkg/crypto/x509/#ExtKeyUsage - simply drop the ``ExtKeyUsage`` part of the value. Values are case-insensitive. Pass an empty list to specify no constraints. ext_key_usage_oids When ``sign_verbatim`` is true, list of extended key usage oids to encode onto the certificate if the CSR does not specify a ``extendedKeyUsage`` extension. Useful for adding EKUs not supported by the Go standard library. For non-verbatim issuance, this parameter must not be specified because Vault takes it from the role. kwargs Most parameters for the :py:func:`file.managed <salt.states.file.managed>` state or any of the ones for the Vault PKI :py:func:`sign_certificate <saltext.vault.modules.vault_pki.sign_certificate>` execution module function are passed through. .. hint:: This is a high-level state, which connects several different functions: * Vault API (`sign-certificate <https://developer.hashicorp.com/vault/api-docs/secret/pki#sign-certificate>`__ or `sign-verbatim <https://developer.hashicorp.com/vault/api-docs/secret/pki#sign-verbatim>`__, depending on the value of ``sign_verbatim``). Completely unknown keyword parameters end up there. * :py:func:`x509.create_csr <salt.modules.x509_v2.create_csr>`: Used to generate a CSR that Vault should sign. Any subject name attribute parameters (``O``, ``OU`` etc.) and most extension parameters (``certificatePolicies``, ``keyUsage``, ``extendedKeyUsage`` etc.) end up here. Note that Vault does not follow the CSR literally, even ``sign-verbatim`` e.g. prohibits ``basicConstraints`` with ``CA: true``. The ``CN`` and ``subjectAltName`` parameters are synced with ``common_name`` and ``alt_names`` respectively, so specifying them directly has no effect. Ignored when ``csr`` is defined. * :py:func:`file.managed <salt.states.file.managed>`: Parameters such as ``user``, ``group`` and ``mode`` end up influencing the certificate file on disk. Note: ``encoding`` is a valid parameter for both this function and ``file.managed``. If you need to pass it to the latter, specify it as ``file_encoding`` instead. """ ret = { "name": name, "changes": {}, "result": True, "comment": "The certificate is in the correct state", } changes = {} ca_chain = [] verb = "create" file_args, cert_args = _split_file_kwargs(hlp.filter_state_internal_kwargs(kwargs)) try: hlp.one_of(private_key=private_key, csr=csr) if not sign_verbatim and not role_name: raise SaltInvocationError("`role_name` is required when `sign_verbatim` is false") encoding = hlp.in_vals(("der", "pem", "pkcs7_der", "pkcs7_pem"), encoding=encoding) if encoding == "der" and append_ca_chain: raise SaltInvocationError( "Cannot append the CA chain to DER-encoded certificates. " "Use pkcs7_der if you need a binary encoding including the chain." ) ttl_seconds = timestring_map(ttl, cast=int) if timestring_map(ttl_remaining, cast=int) >= ttl_seconds: raise SaltInvocationError("The ttl_remaning cannot be larger or equal to ttl.") if not sign_verbatim: hlp.none_of( key_usage=key_usage, ext_key_usage=ext_key_usage, ext_key_usage_oids=ext_key_usage_oids, _reason="sign_verbatim is false", ) # check file.managed changes early to avoid using unnecessary resources file_managed_test = _run_state("file.managed", name, test=True, replace=False, **file_args) if file_managed_test["result"] is False: ret["result"] = False ret["comment"] = "Problem while testing file.managed changes, see its output" _add_sub_state_run(ret, file_managed_test) return ret if "is not present and is not set for creation" in file_managed_test["comment"]: _add_sub_state_run(ret, file_managed_test) return ret file_exists = None # handle follow_symlinks if __salt__["file.is_link"](name): if file_args.get("follow_symlinks", True): name = os.path.realpath(name) else: if not __opts__["test"]: # workaround https://github.com/saltstack/salt/issues/31802 __salt__["file.remove"](name) changes["replaced"] = True file_exists = False if file_exists is None: file_exists = __salt__["file.file_exists"](name) if role_name is not None: role_info = __salt__["vault_pki.read_role"](role_name, mount=mount) if role_info is None: raise CommandExecutionError(f"Role {role_name} does not exist") else: # allowed when signing verbatim role_info = {} if csr and alt_names and role_info.get("use_csr_sans", True): # SANs don't fall back to alt_names (we simulate that when generating a CSR on the fly). # This is in contrast to common_name. log.warning( "Ignoring passed `alt_names`: Received a pre-generated CSR and the role does not specify use_csr_sans=false" ) alt_names = None if issuer_ref is None: issuer_ref = role_info.get("issuer_ref", "default") issuer_info = __salt__["vault_pki.read_issuer"](issuer_ref or "default", mount=mount) if issuer_info is None: raise CommandExecutionError( f"Issuer '{issuer_ref or 'default'}' does not exist on mount {mount}" ) url_configs = ( "issuing_certificates", "crl_distribution_points", "delta_crl_distribution_points", "ocsp_servers", ) if not any(issuer_info.get(url_config) for url_config in url_configs): try: # Mount default AIA URLs urls = __salt__["vault_pki.read_urls"](mount=mount) except CommandExecutionError: # pragma: no cover log.warning( "Failed reading default AIA url config. Consider allowing read access to " "`%s/config/urls`. This state will not be idempotent otherwise.", mount, ) urls = {} else: urls = {"enable_templating": bool(issuer_info.get("enable_aia_url_templating"))} for url in url_configs: # Issuer-specific AIA URLs urls[url] = hlp.deserialize_csl(issuer_info.get(url, [])) if append_ca_chain: ca_chain = [x509util.load_cert(x) for x in issuer_info["ca_chain"]] # Filter self-signed CA, which shouldn't be in the chain. ca_chain = [ cert for cert in ca_chain if cert.subject.rfc4514_string() != cert.issuer.rfc4514_string() ] if file_exists: if reissue: # No need to make any checks, just replace the cert changes["replaced"] = True else: changes = pki.check_cert_for_changes( current=name, issuer=issuer_info["certificate"], private_key=private_key, csr=csr, encoding=encoding, sign_verbatim=sign_verbatim, alt_names=alt_names, append_chain=ca_chain, common_name=common_name, exclude_cn_from_sans=exclude_cn_from_sans, expire_tolerance=ttl_remaining, ext_key_usage=ext_key_usage, ext_key_usage_oids=ext_key_usage_oids, key_usage=key_usage, not_after=not_after, private_key_passphrase=private_key_passphrase, role_info=role_info, serial_number=serial_number, ttl=ttl_seconds, urls=urls, user_ids=user_ids, **cert_args, ) else: changes["created"] = True if not changes and file_managed_test["result"] and not file_managed_test["changes"]: _add_sub_state_run(ret, file_managed_test) return ret ret["changes"] = changes if changes and file_exists: verb = "reissue" if __opts__["test"]: ret["result"] = None if changes else True ret["comment"] = ( f"The certificate would have been {verb}d" if changes else ret["comment"] ) _add_sub_state_run(ret, file_managed_test) return ret cert = None if changes: if not set(changes) - { "ca_chain", "encoding", }: verb = "recreate" cert = __salt__["x509.encode_certificate"]( name, append_certs=ca_chain, encoding=encoding, ) else: issued_cert = __salt__["vault_pki.sign_certificate"]( common_name=common_name, role_name=role_name, private_key=private_key, private_key_passphrase=private_key_passphrase, csr=csr, ttl=ttl, issuer_ref=issuer_ref, mount=mount, sign_verbatim=sign_verbatim, remove_roots_from_chain=False, alt_names=alt_names, exclude_cn_from_sans=exclude_cn_from_sans, not_after=not_after, serial_number=serial_number, user_ids=user_ids, key_usage=key_usage, ext_key_usage=ext_key_usage, ext_key_usage_oids=ext_key_usage_oids, **cert_args, ) cert = __salt__["x509.encode_certificate"]( issued_cert["certificate"], append_certs=ca_chain, encoding=encoding, ) ret["comment"] = f"The certificate has been {verb}d" if encoding not in ["pem", "pkcs7_pem"]: # file.managed does not support binary contents, so create # an empty file first (makedirs). This does not work with check_cmd! file_managed_ret = _run_state("file.managed", name, replace=False, **file_args) _add_sub_state_run(ret, file_managed_ret) if not _check_file_ret(file_managed_ret, ret, file_exists): return ret hlp.safe_atomic_write( name, base64.b64decode(cert), __salt__["config.backup_mode"](file_args.get("backup", "")), __opts__["cachedir"], ) if not changes or encoding in ["pem", "pkcs7_pem"]: replace = bool(encoding in ["pem", "pkcs7_pem"] and changes) contents = cert if replace else None file_managed_ret = _run_state( "file.managed", name, contents=contents, replace=replace, **file_args ) _add_sub_state_run(ret, file_managed_ret) if not _check_file_ret(file_managed_ret, ret, file_exists): return ret except (CommandExecutionError, SaltInvocationError) as err: ret["result"] = False ret["comment"] = str(err) ret["changes"] = {} return ret
[docs] def role_managed(name, mount="pki", issuer_ref=None, ttl=None, max_ttl=None, **kwargs): """ Ensures PKI role is present and configured as required. name Name of the role. mount Mount path the PKI backend is mounted to. Defaults to ``pki``. issuer_ref Issuer reference for the role. Can be name, id or literal ``default``. ttl Specifies the Time To Live value to be used for the validity period of the requested certificate, provided as a string duration with time suffix. Hour is the largest suffix. The value specified is strictly used for future validity. If not set, 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 not set, defaults to the system maximum lease TTL. kwargs Any other parameter accepted by the Vault :py:func:`write_role <saltext.vault.modules.vault_pki.write_role>` execution module function or Vault update role API method. """ ret = { "name": name, "result": True, "comment": "The role is present as specified", "changes": {}, } kwargs = {k: v for k, v in kwargs.items() if not k.startswith("_")} def _diff_params(current): nonlocal issuer_ref, ttl, max_ttl, kwargs diff_params = ( ("issuer_ref", issuer_ref), ("ttl", timestring_map(ttl, cast=int)), ("max_ttl", timestring_map(max_ttl, cast=int)), ) changed = {} for param, arg in diff_params: if arg is None: continue if current[param] != arg: changed.update( { param: { "old": current.get(param), "new": arg, } } ) for param, arg in kwargs.items(): if param not in current: continue curr_val = current[param] # Compare normalized values: The API normalizes scalars for # list-type parameters and duration strings into seconds. if isinstance(curr_val, list) and isinstance(arg, str): arg = hlp.deserialize_csl(arg) elif ( isinstance(curr_val, (int, float)) and not isinstance(curr_val, bool) and isinstance(arg, str) ): try: arg = timestring_map(arg, cast=type(curr_val)) except SaltInvocationError: pass if curr_val != arg: changed.update( { param: { "old": curr_val, "new": arg, } } ) return changed changes = {} try: current = __salt__["vault_pki.read_role"](name, mount=mount) if current: changes = _diff_params(current) if not changes: return ret ret["changes"].update(changes) if not current: ret["changes"]["created"] = name if __opts__["test"]: ret["result"] = None ret["comment"] = ( f"PKI role `{name}` would have been {'updated' if current else 'created'}" ) return ret __salt__["vault_pki.write_role"]( name=name, mount=mount, issuer_ref=issuer_ref, ttl=ttl, max_ttl=max_ttl, **kwargs ) ret["comment"] = f"PKI 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_absent(name, mount="pki"): """ Ensure PKI role is absent. name Name of the role. mount Mount path the PKI backend is mounted to. Defaults to ``pki``. """ ret = { "name": name, "result": True, "comment": "", "changes": {}, } try: current = __salt__["vault_pki.read_role"](name, mount=mount) if current is None: ret["comment"] = f"PKI role `{name}` is already absent." return ret ret["changes"]["deleted"] = name if __opts__["test"]: ret["result"] = None ret["comment"] = f"PKI role `{name}` would have been deleted" return ret __salt__["vault_pki.delete_role"](name, mount=mount) ret["comment"] = f"PKI role `{name}` has been deleted." except (CommandExecutionError, SaltInvocationError) as err: ret["result"] = False ret["comment"] = str(err) ret["changes"] = {} return ret
[docs] def intermediate_issuer_managed( name, days_remaining=30, # key params key_ref=None, rotate_key=False, key_type=None, key_algo=None, key_bits=None, max_path_length=0, managed_key_name=None, managed_key_id=None, # issuer params issuer_name=None, leaf_not_after_behavior=None, usage=None, revocation_signature_algorithm=None, aia_urls=None, crl_endpoints=None, delta_crl_endpoints=None, ocsp_servers=None, aia_url_templating=None, mount="pki", # params for x509.create_certificate **kwargs, ): """ .. versionadded:: 1.9.0 Ensure an issuer representing an intermediate CA is present **as the default issuer** on the mount. Rotates the issuer when necessary by generating a new certificate via :py:func:`x509.create_certificate <salt.modules.x509_v2.create_certificate>`. Does not support certificate import. Required policy: .. code-block:: vaultpolicy # read default issuer to check for necessary changes path "<mount>/issuer/default" { capabilities = ["read"] } # when key_ref is not set path "<mount>/keys/generate/<key_type>" { capabilities = ["create", "update"] } # generate a CSR to derive the public key path "<mount>/intermediate/generate/<key_type>" { capabilities = ["create", "update"] } # import the signed cert path "<mount>/intermediate/set-signed" { capabilities = ["create", "update"] } # set default issuer path "<mount>/config/issuers" { capabilities = ["create", "update"] } # update issuer configuration path "<mount>/issuer/<name>" { capabilities = ["patch"] } **Certificate/Key configuration:** name Common name (CN) of the certificate subject. days_remaining Attempt to recreate the certificate if the number of days the certificate is valid for is less than the number specified. Defaults to ``30``. key_ref Instead of managing the key, use the one associated with this key ID/name. When specified, disables key generation/rotation. rotate_key When rotating the default issuer, rotate its key along with it. Defaults to false. Not respected when ``key_ref`` is specified. .. note:: Key parameters are not managed statefully, meaning changes to ``key_type``, ``key_algo`` and ``key_bits`` are only applied when generating a new key. ``key_ref`` changes are applied though. key_type Type of key to generate when necessary and ``key_ref`` is not specified. Either ``internal``, ``exported`` or ``kms``. Defaults to ``internal``. key_algo Key algorithm. Either ``rsa``, ``ed25519`` or ``ec``. Defaults to ``rsa``. key_bits Number of bits to use for the generated keys. Valid values depend on the ``key_algo``. * ``rsa``: 2048 (default), 3072, 4096, 8192. * ``ec``: 224, 256 (default), 384, 521 * ``ed25519``: ignored Defaults to ``0`` (universal default). managed_key_name When ``key_type`` is ``kms``, the managed key's configured name. Either this or ``managed_key_id`` is required then. managed_key_id When ``key_type`` is ``kms``, the managed key's UUID. Either this or ``managed_key_name`` is required then. max_path_length basicConstraints ``pathlen`` parameter, which indicates the maximum number of CAs that can appear below this one in a chain. If set to ``0``, this CA can only issue leaf certificates, not other CAs. A negative value means no limit, unless the issuer certificate has a maximum path length, in which case it means one less than the issuer's pathlen. Defaults to ``0``. kwargs Unknown keyword arguments are passed to :py:func:`x509.create_certificate <salt.modules.x509_v2.create_certificate>`. See there for details. The following arguments are enforced by this function: * ``CN`` * ``basicConstraints`` * ``csr`` * ``format`` * ``private_key`` (empty) * ``public_key`` (empty) * ``raw`` (empty) These receive defaults if not specified: * ``keyUsage``: ``[critical, cRLSign, keyCertSign]`` * ``subjectKeyIdentifier``: ``hash`` * ``authorityKeyIdentifier``: ``keyid:always,issuer`` **Issuer configuration:** issuer_name Custom name for the issuer. Must be unique and not equal to ``default``. leaf_not_after_behavior Behavior of a leaf's ``NotAfter`` field during issuance when it exceeds the issuer's validity. Valid options: * ``err``: Error, unless during CA/ACME issuance. (default) * ``always_enforce_err``: Error, including during CA/ACME issuance. * ``truncate``: Silently truncate the requested NotAfter to that of the issuer. * ``permit``: Allow signed certificate validities to exceed that of the issuer. usage Allowed usages for this issuer. Valid options are: * ``read-only`` - to allow this issuer to be read; implict; always allowed; * ``issuing-certificates`` - to allow this issuer to be used for issuing other certificates; * ``crl-signing`` - to allow this issuer to be used for signing CRLs. This is separate from the CRLSign KeyUsage on the x509 certificate, but this usage cannot be set unless that KeyUsage is allowed on the x509 certificate; * ``ocsp-signing`` - to allow this issuer to be used for signing OCSP responses. revocation_signature_algorithm Which signature algorithm to use when building CRLs. See Go's `x509.SignatureAlgorithm <https://pkg.go.dev/crypto/x509#SignatureAlgorithm>`__ constant for possible values. Default (empty string) is to autoselect. aia_urls Specifies the URL values for the Issuing Certificate field as an array. crl_endpoints Specifies the URL values for the CRL Distribution Points field as an array. delta_crl_endpoints (Requires Vault 1.20+ or OpenBao) Specifies the URL values for the Delta CRL Distribution Points field. This can be an array or a comma- separated string list. ocsp_servers Specifies the URL values for the OCSP Servers field as an array. aia_url_templating Render ``aia_urls``/``crl_endpoints``/``ocsp_servers``/``delta_crl_endpoints`` as templates. Supported variables: ``{{issuer_id}}``, ``{{cluster_path}}``, ``{{cluster_aia_path}}``. mount Mount path the PKI backend is mounted to. Defaults to ``pki``. """ ret = { "name": name, "result": True, "comment": "Intermediate CA issuer is present as specified", "changes": {}, } changes = {} cert_affected = issuer_affected = False issuer_id = None msg = [] basic_constraints = {"critical": True, "ca": True} if max_path_length is not None: basic_constraints["pathlen"] = max_path_length kwargs["basicConstraints"] = basic_constraints kwargs["CN"] = name kwargs["format"] = "pem" kwargs.pop("path", None) kwargs.pop("private_key", None) kwargs.pop("public_key", None) kwargs.pop("csr", None) kwargs.setdefault("keyUsage", ["critical", "cRLSign", "keyCertSign"]) kwargs.setdefault("subjectKeyIdentifier", "hash") kwargs.setdefault("authorityKeyIdentifier", "keyid:always,issuer") try: key_type = hlp.in_vals(("internal", "exported", "kms", None), key_type=key_type) if key_ref is not None: key_type = "existing" rotate_key = False if not (current := __salt__["vault_pki.read_issuer"](mount=mount)): changes["created"] = name cert_affected = True issuer_affected = any( val is not None for val in ( issuer_name, leaf_not_after_behavior, usage, revocation_signature_algorithm, aia_urls, crl_endpoints, delta_crl_endpoints, ocsp_servers, aia_url_templating, ) ) else: issuer_id = current["issuer_id"] if key_ref is None: key_ref = current.get("key_id") if key_ref is None: # pragma: no cover # Unsure if this is allowed to happen, need to check raise CommandExecutionError("Default issuer key_id not set") kwargs["csr"] = __salt__["vault_pki.generate_intermediate_csr"]( "existing", key_ref=key_ref, mount=mount )["csr"] with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "issuer.pem" path.write_text("".join(current["ca_chain"])) x509_ret = _run_state( "x509.certificate_managed", str(path), test=True, days_remaining=days_remaining, **kwargs, ) if x509_ret["result"] is False: ret["result"] = False ret["comment"] = f"Failed running x509.certificate_managed: {x509_ret['comment']}" return ret cert_changes = x509_ret["changes"] if cert_changes and rotate_key: cert_changes["private_key"] = True ext_changes = cert_changes.setdefault( "extensions", {"added": [], "changed": [], "removed": []} ) if "subjectKeyIdentifier" not in ext_changes["changed"]: ext_changes["changed"].append("subjectKeyIdentifier") if cert_changes: changes["cert"], cert_affected = cert_changes, True if issuer_changes := _check_issuer_config_changes( current, issuer_name=issuer_name, leaf_not_after_behavior=leaf_not_after_behavior, usage=usage, revocation_signature_algorithm=revocation_signature_algorithm, aia_urls=aia_urls, crl_endpoints=crl_endpoints, delta_crl_endpoints=delta_crl_endpoints, ocsp_servers=ocsp_servers, aia_url_templating=aia_url_templating, ): changes["issuer"], issuer_affected = issuer_changes, True if not changes: return ret if __opts__["test"]: ret["result"] = None if cert_affected: msg.append( f"Intermediate CA certificate would have been {'rotated' if current else 'created'}" ) if issuer_affected: msg.append( f"Intermediate CA issuer would have been {'updated' if current else 'created'}" ) ret["comment"] = ". ".join(msg) + "." ret["changes"] = changes return ret if cert_affected: if key_ref is None or rotate_key: key_ref = __salt__["vault_pki.generate_key"]( key_type or "internal", key_algo=key_algo, key_bits=key_bits, managed_key_name=managed_key_name, managed_key_id=managed_key_id, mount=mount, )["key_id"] if "csr" not in kwargs or rotate_key: kwargs["csr"] = __salt__["vault_pki.generate_intermediate_csr"]( "existing", key_ref=key_ref, mount=mount )["csr"] cert = __salt__["x509.create_certificate"](**kwargs) res = __salt__["vault_pki.import_issuer_intermediate"](cert, mount=mount) try: issuer_id = res["imported_issuers"][0] except (IndexError, KeyError) as err: # pragma: no cover raise CommandExecutionError( "Generated certificate, but failed importing it" ) from err try: __salt__["vault_pki.set_default_issuer"](issuer_id, mount=mount) except CommandExecutionError as err: ret["result"] = False ret["comment"] = ( f"Generated and imported certificate as issuer `{issuer_id}`, but failed to set it as default issuer: {err}" ) ret["changes"]["imported"] = issuer_id return ret if current is not None: ret["changes"]["cert"] = changes["cert"] msg.append( f"Intermediate CA certificate has been {'rotated' if current else 'created'}" ) if issuer_affected: __salt__["vault_pki.update_issuer"]( ref=issuer_id, name=issuer_name, leaf_not_after_behavior=leaf_not_after_behavior, usage=usage, revocation_signature_algorithm=revocation_signature_algorithm, aia_urls=aia_urls, crl_endpoints=crl_endpoints, delta_crl_endpoints=delta_crl_endpoints, ocsp_servers=ocsp_servers, aia_url_templating=aia_url_templating, mount=mount, ) if current is not None: ret["changes"]["issuer"] = changes["issuer"] msg.append(f"Intermediate CA issuer has been {'updated' if current else 'created'}") if current is None: ret["changes"]["created"] = name ret["comment"] = ". ".join(msg) + "." except (CommandExecutionError, SaltInvocationError) as err: ret["result"] = False if msg: ret["comment"] = ". ".join(msg) + f", but received an exception later: {err}" else: ret["comment"] = str(err) return ret
[docs] def root_issuer_managed( # pylint: disable=too-many-locals,too-many-arguments name, days_remaining=90, # key params key_ref=None, rotate_key=False, key_type=None, key_algo=None, key_bits=None, managed_key_name=None, managed_key_id=None, # cert params alt_names=None, days_valid=3650, max_path_length=-1, key_usage=None, exclude_cn_from_sans=False, permitted_alt_names=None, excluded_alt_names=None, ou=None, organization=None, country=None, locality=None, province=None, street_address=None, postal_code=None, serial_number=None, signature_bits=0, not_before_duration=30, not_after=None, # issuer params issuer_name=None, leaf_not_after_behavior=None, usage=None, revocation_signature_algorithm=None, aia_urls=None, crl_endpoints=None, delta_crl_endpoints=None, ocsp_servers=None, aia_url_templating=None, mount="pki", ): """ .. versionadded:: 1.9.0 Ensure an issuer representing a root CA is present **as the default issuer** on the mount. Rotates the issuer when necessary. Required policy: .. code-block:: vaultpolicy # read default issuer to check for necessary changes path "<mount>/issuer/default" { capabilities = ["read"] } # Read mount default urls to account for cert extensions path "<mount>/config/urls" { capabilities = ["read"] } # when key_ref is not set path "<mount>/keys/generate/<key_type>" { capabilities = ["create", "update"] } # when key_ref is set path "<mount>/keys" { capabilities = ["list"] } # set default issuer path "<mount>/config/issuers" { capabilities = ["create", "update"] } # update issuer configuration path "<mount>/issuer/<name>" { capabilities = ["patch"] } **Certificate/Key configuration:** name Common name (CN) of the certificate subject. days_remaining Attempt to recreate the certificate if the number of days the certificate is valid for is less than the number specified. Defaults to ``30``. key_ref Instead of managing the key, use the one associated with this key ID/name. When specified, disables key generation/rotation. rotate_key When rotating the default issuer, rotate its key along with it. Defaults to false. Not respected when ``key_ref`` is specified. .. note:: Key parameters are not managed statefully, meaning changes to ``key_type``, ``key_algo`` and ``key_bits`` are only applied when generating a new key. ``key_ref`` changes are applied though. key_type Type of key to generate when necessary and ``key_ref`` is not specified. Either ``internal``, ``exported`` or ``kms``. Defaults to ``internal``. key_algo Key algorithm. Either ``rsa``, ``ed25519`` or ``ec``. Defaults to ``rsa``. key_bits Number of bits to use for the generated keys. Valid values depend on the ``key_algo``. * ``rsa``: 2048 (default), 3072, 4096, 8192. * ``ec``: 224, 256 (default), 384, 521 * ``ed25519``: ignored Defaults to ``0`` (universal default). managed_key_name When ``key_type`` is ``kms``, the managed key's configured name. Either this or ``managed_key_id`` is required then. managed_key_id When ``key_type`` is ``kms``, the managed key's UUID. Either this or ``managed_key_name`` is required then. signature_bits Number of bits to use in the signature algorithm. Valid: ``256`` (SHA-2-256), ``384`` (SHA-2-384), ``512`` (SHA-2-512). Defaults to ``0``, which automatically selects an algorithm based on ``key_algo`` and ``key_bits`` of the issuer's private key. days_valid Number of days the certificate should be valid for when (re-)issued. Not respected when ``not_after`` is set explicitly. Defaults to 3650 (10 years). not_before_duration Duration by which to backdate the NotBefore property. Defaults to ``30s``. not_after Absolute value of the Not After field of the certificate in UTC format ``YYYY-MM-ddTHH:MM:SSZ``. When set, ``days_valid`` is ignored. alt_names Any alternative names to add to the certificate. Can be specified either as dict (``{ "<type>": "<value>" }``), a dict of lists (``{ "<type>": ["<value1>", "<value2>", ...] }``) or list of SAN strings (``["<type1>:<value1>", ...]``). ``<type>`` can be ``dns``, ``email``, ``uri``, ``ip`` or any OID for otherName SANs. ``<value>`` is the corresponding value. Note that otherName SANs need to omit ``UTF8:``. max_path_length basicConstraints ``pathlen`` parameter, which indicates the maximum number of CAs that can appear below this one in a chain. If set to ``0``, this CA can only issue leaf certificates, not other CAs. A negative value means no limit, unless the issuer certificate has a maximum path length, in which case it means one less than the issuer's pathlen. Defaults to ``0``. key_usage (Requires Vault 1.20+ or OpenBao) List of key usages to add to the existing set of key usages (CRLSign,CertSign). Per the CA/B Forum, Vault ignores additional values other than DigitalSignature. exclude_cn_from_sans If set to true, the Common Name is not added to the SANs. Useful if the CN is not a hostname or email address. permitted_alt_names List of alternative names for which certificates are allowed to be issued or signed by this CA certificate. The format is similar to the one for ``alt_names``, but ``<type>`` can only be ``dns``, ``email``, ``uri`` and ``ip``. .. important:: Types other than ``dns`` require Vault 1.19+. excluded_alt_names (Vault 1.19+ only) List of alternative names for which certificates are not allowed to be issued or signed by this CA certificate. The format is similar to the one for ``alt_names``, but ``<type>`` can only be ``dns``, ``email``, ``uri`` and ``ip``. Subject DN fields Most of these can be single strings or lists of strings (for multiple values). * ou * organization * country * locality * province * street_address * postal_code * serial_number (only a single value; NOT the certificate's serial number, just the SERIALNUMBER name attribute) **Issuer configuration:** issuer_name Custom name for the issuer. Must be unique and not equal to ``default``. leaf_not_after_behavior Behavior of a leaf's ``NotAfter`` field during issuance when it exceeds the issuer's validity. Valid options: * ``err``: Error, unless during CA/ACME issuance. (default) * ``always_enforce_err``: Error, including during CA/ACME issuance. * ``truncate``: Silently truncate the requested NotAfter to that of the issuer. * ``permit``: Allow signed certificate validities to exceed that of the issuer. usage Allowed usages for this issuer. Valid options are: * ``read-only`` - to allow this issuer to be read; implict; always allowed; * ``issuing-certificates`` - to allow this issuer to be used for issuing other certificates; * ``crl-signing`` - to allow this issuer to be used for signing CRLs. This is separate from the CRLSign KeyUsage on the x509 certificate, but this usage cannot be set unless that KeyUsage is allowed on the x509 certificate; * ``ocsp-signing`` - to allow this issuer to be used for signing OCSP responses. revocation_signature_algorithm Which signature algorithm to use when building CRLs. See Go's `x509.SignatureAlgorithm <https://pkg.go.dev/crypto/x509#SignatureAlgorithm>`__ constant for possible values. Default (empty string) is to autoselect. aia_urls Specifies the URL values for the Issuing Certificate field as an array. crl_endpoints Specifies the URL values for the CRL Distribution Points field as an array. delta_crl_endpoints (Requires Vault 2.0+ or OpenBao) Specifies the URL values for the Delta CRL Distribution Points field. This can be an array or a comma- separated string list. .. note:: This parameter is supported in Vault 1.20+, but not added as a FreshestCRL extension to the root issuer certificate, leading to non-idempotency of this state. ocsp_servers Specifies the URL values for the OCSP Servers field as an array. aia_url_templating Render ``aia_urls``/``crl_endpoints``/``ocsp_servers``/``delta_crl_endpoints`` as templates. Supported variables: ``{{issuer_id}}``, ``{{cluster_path}}``, ``{{cluster_aia_path}}``. mount Mount path the PKI backend is mounted to. Defaults to ``pki``. """ ret = { "name": name, "result": True, "comment": "Root CA issuer is present as specified", "changes": {}, } changes = {} cert_affected = issuer_affected = replace_key = False issuer_id = None msg = [] try: key_type = hlp.in_vals(("internal", "exported", "kms", None), key_type=key_type) if key_ref is not None: key_type = "existing" rotate_key = False if not (current := __salt__["vault_pki.read_issuer"](mount=mount)): changes["created"] = name cert_affected = True issuer_affected = any( val is not None for val in ( issuer_name, leaf_not_after_behavior, usage, revocation_signature_algorithm, aia_urls, crl_endpoints, delta_crl_endpoints, ocsp_servers, aia_url_templating, ) ) else: issuer_id = current["issuer_id"] try: urls = __salt__["vault_pki.read_urls"](mount=mount) except CommandExecutionError: urls = {} replace_key = key_ref is not None and current["key_id"] != __salt__[ "vault_pki.get_key_id" ](key_ref, mount=mount) if cert_changes := pki.check_root_issuer_for_changes( "".join(current["ca_chain"]), alt_names=alt_names, common_name=name, country=country, days_remaining=days_remaining, days_valid=days_valid, exclude_cn_from_sans=exclude_cn_from_sans, excluded_alt_names=excluded_alt_names, key_usage=key_usage, locality=locality, max_path_length=max_path_length, not_after=not_after, not_before_duration=not_before_duration, organization=organization, ou=ou, permitted_alt_names=permitted_alt_names, postal_code=postal_code, province=province, replace_key=replace_key, rotate_key=rotate_key, serial_number=serial_number, signature_bits=signature_bits, street_address=street_address, urls=urls, ): changes["cert"], cert_affected = cert_changes, True if issuer_changes := _check_issuer_config_changes( current, issuer_name=issuer_name, leaf_not_after_behavior=leaf_not_after_behavior, usage=usage, revocation_signature_algorithm=revocation_signature_algorithm, aia_urls=aia_urls, crl_endpoints=crl_endpoints, delta_crl_endpoints=delta_crl_endpoints, ocsp_servers=ocsp_servers, aia_url_templating=aia_url_templating, ): changes["issuer"], issuer_affected = issuer_changes, True if not changes: return ret if __opts__["test"]: ret["result"] = None if cert_affected: msg.append( f"Root CA certificate would have been {'rotated' if current else 'created'}" ) if issuer_affected: msg.append(f"Root CA issuer would have been {'updated' if current else 'created'}") ret["comment"] = ". ".join(msg) ret["changes"] = changes return ret if cert_affected: if key_ref is None or rotate_key: key_ref = __salt__["vault_pki.generate_key"]( key_type or "internal", key_algo=key_algo, key_bits=key_bits, managed_key_name=managed_key_name, managed_key_id=managed_key_id, mount=mount, )["key_id"] if not_after is None: not_after_dt = datetime.now(tz=timezone.utc) + timedelta(days=days_valid) not_after = not_after_dt.strftime("%Y-%m-%dT%H:%M:%SZ") dns_sans, ip_sans, uri_sans, other_sans = pki.split_sans(pki.norm_sans(alt_names or [])) dns_nc_allowed = email_nc_allowed = ip_nc_allowed = uri_nc_allowed = None if permitted_alt_names is not None: dns_nc_allowed, email_nc_allowed, ip_nc_allowed, uri_nc_allowed = ( pki.split_name_constraints( pki.norm_sans(permitted_alt_names or [], allow_other_name=False) ) ) dns_nc_denied = email_nc_denied = ip_nc_denied = uri_nc_denied = None if excluded_alt_names is not None: dns_nc_denied, email_nc_denied, ip_nc_denied, uri_nc_denied = ( pki.split_name_constraints( pki.norm_sans(excluded_alt_names or [], allow_other_name=False) ) ) res = __salt__["vault_pki.generate_root"]( common_name=name, mount=mount, key_type="existing", key_ref=key_ref, alt_names=dns_sans and ",".join(dns_sans) or None, ip_sans=ip_sans and ",".join(ip_sans) or None, uri_sans=uri_sans and ",".join(uri_sans) or None, other_sans=other_sans and ",".join(other_sans) or None, exclude_cn_from_sans=exclude_cn_from_sans, max_path_length=max_path_length, key_usage=key_usage, permitted_dns_domains=dns_nc_allowed, excluded_dns_domains=dns_nc_denied, permitted_ip_ranges=ip_nc_allowed, excluded_ip_ranges=ip_nc_denied, permitted_email_addresses=email_nc_allowed, excluded_email_addresses=email_nc_denied, permitted_uri_domains=uri_nc_allowed, excluded_uri_domains=uri_nc_denied, ou=ou, organization=organization, country=country, locality=locality, province=province, street_address=street_address, postal_code=postal_code, serial_number=serial_number, signature_bits=signature_bits, not_before_duration=not_before_duration, not_after=not_after, ) issuer_id = res["issuer_id"] try: __salt__["vault_pki.set_default_issuer"](issuer_id, mount=mount) except CommandExecutionError as err: ret["result"] = False ret["changes"]["generated"] = issuer_id ret["comment"] = ( f"Generated issuer `{issuer_id}`, but failed to set it as default issuer: {err}" ) return ret if current is not None: # Correctly report new subjectKeyIdentifier, it's "<TBD>" right now if rotate_key or replace_key: new_issuer = __salt__["vault_pki.read_issuer"](mount=mount) new_ski = pki.get_ski("".join(new_issuer["ca_chain"])) if ( "subjectKeyIdentifier" in changes["cert"]["extensions"]["added"] ): # pragma: no cover changes["cert"]["extensions"]["added"]["subjectKeyIdentifier"][ "value" ] = new_ski else: changes["cert"]["extensions"]["changed"]["subjectKeyIdentifier"]["value"][ "new" ] = new_ski ret["changes"]["cert"] = changes["cert"] msg.append(f"Root CA certificate has been {'rotated' if current else 'created'}") if issuer_affected: __salt__["vault_pki.update_issuer"]( ref=issuer_id, name=issuer_name, leaf_not_after_behavior=leaf_not_after_behavior, usage=usage, revocation_signature_algorithm=revocation_signature_algorithm, aia_urls=aia_urls, crl_endpoints=crl_endpoints, delta_crl_endpoints=delta_crl_endpoints, ocsp_servers=ocsp_servers, aia_url_templating=aia_url_templating, mount=mount, ) if current is not None: ret["changes"]["issuer"] = changes["issuer"] msg.append(f"Root CA issuer has been {'updated' if current else 'created'}") ret["comment"] = ". ".join(msg) + "." if current is None: ret["changes"]["created"] = name except (CommandExecutionError, SaltInvocationError) as err: ret["result"] = False if msg: ret["comment"] = ". ".join(msg) + f", but received an exception later: {err}" else: ret["comment"] = str(err) return ret
def _check_issuer_config_changes( current, *, issuer_name: str | None = None, leaf_not_after_behavior: str | None = None, usage: list[str] | str | None = None, revocation_signature_algorithm: str | None = None, aia_urls: list[str] | str | None = None, crl_endpoints: list[str] | str | None = None, delta_crl_endpoints: list[str] | str | None = None, ocsp_servers: list[str] | str | None = None, aia_url_templating: bool | None = None, ): changes = {} for vault_param, saltext_param, val in ( ("issuer_name", "issuer_name", issuer_name), ("leaf_not_after_behavior", "leaf_not_after_behavior", leaf_not_after_behavior), ( "revocation_signature_algorithm", "revocation_signature_algorithm", revocation_signature_algorithm, ), ("enable_aia_url_templating", "aia_url_templating", aia_url_templating), ): if val is not None: # At least on OpenBao and older Vault releases, this is not reported if no URLs are set if vault_param == "enable_aia_url_templating" and vault_param not in current: current["enable_aia_url_templating"] = False if vault_param not in current: log.warning( "Ignoring specified param %s during changes check, the server likely does not support it", saltext_param, ) continue if current[vault_param] != val: changes[saltext_param] = {"old": current[vault_param], "new": val} if usage is not None: wanted = set(hlp.deserialize_csl(usage)) cur = set(hlp.deserialize_csl(current["usage"])) cur.discard("read-only") # always allowed wanted.discard("read-only") if cur != wanted: changes["usage"] = { "added": list(sorted(wanted - cur)), "removed": list(sorted(cur - wanted)), } for vault_param, saltext_param, val in ( ("issuing_certificates", "aia_urls", hlp.deserialize_csl(aia_urls)), ("crl_distribution_points", "crl_endpoints", hlp.deserialize_csl(crl_endpoints)), ( "delta_crl_distribution_points", "delta_crl_endpoints", hlp.deserialize_csl(delta_crl_endpoints), ), ("ocsp_servers", "ocsp_servers", hlp.deserialize_csl(ocsp_servers)), ): if val is not None: # At least on OpenBao and older Vault releases, none of these are reported if all are unset if vault_param not in current: current[vault_param] = [] if current[vault_param] != val: changes[saltext_param] = { "added": list(sorted(set(val) - set(current[vault_param]))), "removed": list(sorted(set(current[vault_param]) - set(val))), } return changes def _split_file_kwargs(kwargs): file_args = {"show_changes": False} extra_args = {} for k, v in kwargs.items(): if k in VALID_FILE_ARGS: file_args[k] = v else: extra_args[k] = v if "file_encoding" in file_args: file_args["encoding"] = file_args.pop("file_encoding") return file_args, extra_args def _add_sub_state_run(ret, sub): sub["low"] = { "name": ret["name"], "state": "file", "__id__": __low__["__id__"], "fun": "managed", } if "sub_state_run" not in ret: ret["sub_state_run"] = [] ret["sub_state_run"].append(sub) def _run_state(func, name, test=None, **kwargs): if test not in (None, True): # pragma: no cover raise SaltInvocationError("test param can only be None or True") test = test or __opts__["test"] res = __salt__["state.single"](func, name, test=test, concurrent=True, **kwargs) if not isinstance(res, dict): raise CommandExecutionError(f"Failed running {func}: {res}") return res[next(iter(res))] def _check_file_ret(fret, ret, current): if fret["result"] is False: ret["result"] = False ret["comment"] = ( f"Could not {'create' if not current else 'update'} file, see file.managed output" ) ret["changes"] = {} return False return True