Source code for saltext.vault.runners.vault

"""
Runner functions supporting the Vault modules.

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

import base64
import copy
import logging
import os
from collections.abc import Mapping
from pathlib import Path

import salt.cache
import salt.crypt
import salt.exceptions
import salt.pillar
import salt.utils.data
import salt.utils.json
import salt.utils.versions
from salt.defaults import NOT_SET
from salt.exceptions import SaltInvocationError
from salt.exceptions import SaltRunnerError
from salt.utils import immutabletypes

from saltext.vault.utils import vault
from saltext.vault.utils.vault import cache as vcache
from saltext.vault.utils.vault import factory
from saltext.vault.utils.vault import helpers
from saltext.vault.utils.vault.client import VaultClient
from saltext.vault.utils.versions import warn_until

log = logging.getLogger(__name__)

VALID_PARAMS = immutabletypes.freeze(
    {
        "approle": [
            "bind_secret_id",
            "secret_id_bound_cidrs",
            "secret_id_num_uses",
            "secret_id_ttl",
            "token_ttl",
            "token_max_ttl",
            "token_explicit_max_ttl",
            "token_num_uses",
            "token_no_default_policy",
            "token_period",
            "token_bound_cidrs",
        ],
        "token": [
            "ttl",
            "period",
            "explicit_max_ttl",
            "num_uses",
            "no_default_policy",
            "renewable",
        ],
    }
)

NO_OVERRIDE_PARAMS = immutabletypes.freeze(
    {
        "approle": [
            "bind_secret_id",
            "token_policies",
            "policies",
        ],
        "token": [
            "role_name",
            "policies",
            "meta",
        ],
    }
)


[docs] def auth_info(): """ .. versionadded:: 1.0.0 Return information about the currently active Vault authentication. This includes token information and, if AppRole authentication is in use, information about the SecretID. CLI Example: .. code-block:: bash salt-run vault.auth_info """ client = _get_master_client() info = {} info["token"] = client.get("auth/token/lookup-self")["data"] if _config("auth:method") == "approle": try: info["secret_id"] = _get_approle_api().read_secret_id( _config("auth:approle_name"), mount=_config("auth:approle_mount"), secret_id=str(client.auth.approle.secret_id), ) except vault.VaultPermissionDeniedError: info["secret_id"] = ( "Permission denied, allow API `create`, `update` access to " f"`auth/{_config('auth:approle_mount')}" f"/role/{_config('auth:approle_name')}/secret-id/lookup` " "to view info" ) return info
[docs] def generate_token( minion_id, signature, impersonated_by_master=False, ttl=None, uses=None, upgrade_request=False, ): """ .. deprecated:: 1.0.0 Generate a Vault token for minion <minion_id>. minion_id The ID of the minion that requests a token. signature Cryptographic signature which validates that the request is indeed sent by the minion (or the master, see impersonated_by_master). impersonated_by_master If the master needs to create a token on behalf of the minion, this is True. This happens when the master generates minion pillars. ttl Ticket time to live in seconds, 1m minutes, or 2h hrs uses Number of times a token can be used upgrade_request In case the new runner endpoints have not been whitelisted for peer running, this endpoint serves as a gateway to :func:`vault.get_config <get_config>`. Defaults to False. """ if upgrade_request: log.warning( "Detected minion fallback to old vault.generate_token peer run function. " "Please update your master peer_run configuration." ) issue_params = {"explicit_max_ttl": ttl, "num_uses": uses} return get_config(minion_id, signature, impersonated_by_master, issue_params=issue_params) log.debug( "Token generation request for %s (impersonated by master: %s)", minion_id, impersonated_by_master, ) _validate_signature(minion_id, signature, impersonated_by_master) try: warn_until( 2, ( "The vault.generate_token endpoint is deprecated and will be removed " "in version {version}. Please ensure your minions are running the " "Vault Salt extension as well." ), ) if _config("issue:type") != "token": log.warning( "Master is not configured to issue tokens. Since the minion uses " "this deprecated endpoint, issuing token anyways." ) issue_params = {} if ttl is not None: issue_params["explicit_max_ttl"] = ttl if uses is not None: issue_params["num_uses"] = uses token, _ = _generate_token(minion_id, issue_params=issue_params or None, wrap=False) ret = { "token": token["client_token"], "lease_duration": token["lease_duration"], "renewable": token["renewable"], "issued": token["creation_time"], "url": _config("server:url"), "verify": _config("server:verify"), "token_backend": _config("cache:backend"), "namespace": _config("server:namespace"), } if token["num_uses"] >= 0: ret["uses"] = token["num_uses"] return ret except Exception as err: # pylint: disable=broad-except return {"error": f"{type(err).__name__}: {str(err)}"}
[docs] def generate_new_token(minion_id, signature, impersonated_by_master=False, issue_params=None): """ .. versionadded:: 1.0.0 Generate a Vault token for minion <minion_id>. minion_id The ID of the minion that requests a token. signature Cryptographic signature which validates that the request is indeed sent by the minion (or the master, see ``impersonated_by_master``). impersonated_by_master If the master needs to create a token on behalf of the minion, this is True. This happens when the master generates minion pillars. issue_params Dictionary of parameters for the generated tokens. See master configuration :vconf:`issue:token:params` for possible values. Requires :vconf:`issue:allow_minion_override_params` master configuration setting to be effective. """ log.debug( "Token generation request for %s (impersonated by master: %s)", minion_id, impersonated_by_master, ) _validate_signature(minion_id, signature, impersonated_by_master) try: if _config("issue:type") != "token": return {"expire_cache": True, "error": "Master does not issue tokens."} ret = { "server": _config("server"), "auth": {}, } wrap = _config("issue:wrap") token, num_uses = _generate_token(minion_id, issue_params=issue_params, wrap=wrap) if wrap: ret.update(token) ret.update({"misc_data": {"num_uses": num_uses}}) else: ret["auth"] = token return ret except Exception as err: # pylint: disable=broad-except return {"error": f"{type(err).__name__}: {str(err)}"}
def _generate_token(minion_id, issue_params, wrap): endpoint = "auth/token/create" if _config("issue:token:role_name") is not None: endpoint += "/" + _config("issue:token:role_name") payload = _parse_issue_params(issue_params, issue_type="token") payload["policies"] = _get_policies_cached( minion_id, refresh_pillar=_config("policies:refresh_pillar"), expire=_config("policies:cache_time"), ) if not payload["policies"]: raise SaltRunnerError("No policies matched minion.") payload["meta"] = _get_metadata(minion_id, _config("metadata:secret")) client = _get_master_client() log.trace("Sending token creation request to Vault.") res = client.post(endpoint, payload=payload, wrap=wrap) if wrap: return res.serialize_for_minion(), payload["num_uses"] if "num_uses" not in res["auth"]: # older vault versions do not include num_uses in output res["auth"]["num_uses"] = payload["num_uses"] token = vault.VaultToken(**res["auth"]) return token.serialize_for_minion(), payload["num_uses"]
[docs] def get_config( minion_id, signature, impersonated_by_master=False, issue_params=None, config_only=False, ): """ .. versionadded:: 1.0.0 Return Vault configuration for minion <minion_id>. minion_id The ID of the minion that requests the configuration. signature Cryptographic signature which validates that the request is indeed sent by the minion (or the master, see impersonated_by_master). impersonated_by_master If the master needs to contact the Vault server on behalf of the minion, this is True. This happens when the master generates minion pillars. issue_params Parameters for credential issuance. Requires :vconf:`issue:allow_minion_override_params` master configuration setting to be effective. config_only In case the master is configured to issue tokens, do not include a new token in the response. This is used for configuration update checks. Defaults to false. """ log.debug( "Config request for %s (impersonated by master: %s)", minion_id, impersonated_by_master, ) _validate_signature(minion_id, signature, impersonated_by_master) try: minion_config = { "auth": { "method": _config("issue:type"), "token_lifecycle": _config("auth:token_lifecycle"), }, "cache": _config("cache"), "client": _config("client"), "server": _config("server"), "wrap_info_nested": [], } wrap = _config("issue:wrap") if not config_only and _config("issue:type") == "token": minion_config["auth"]["token"], num_uses = _generate_token( minion_id, issue_params=issue_params, wrap=wrap, ) if wrap: minion_config["wrap_info_nested"].append("auth:token") minion_config.update({"misc_data": {"token:num_uses": num_uses}}) if _config("issue:type") == "approle": minion_config["auth"]["approle_mount"] = _config("issue:approle:mount") minion_config["auth"]["approle_name"] = minion_id minion_config["auth"]["secret_id"] = _config("issue:approle:params:bind_secret_id") minion_config["auth"]["role_id"] = _get_role_id( minion_id, issue_params=issue_params, wrap=wrap ) if wrap: minion_config["wrap_info_nested"].append("auth:role_id") return minion_config except Exception as err: # pylint: disable=broad-except return {"error": f"{type(err).__name__}: {str(err)}"}
[docs] def get_role_id(minion_id, signature, impersonated_by_master=False, issue_params=None): """ .. versionadded:: 1.0.0 Return the Vault role-id for minion <minion_id>. Requires the master to be configured to generate AppRoles for minions (:vconf:`issue:type`). minion_id The ID of the minion that requests a role-id. signature Cryptographic signature which validates that the request is indeed sent by the minion (or the master, see impersonated_by_master). impersonated_by_master If the master needs to create a token on behalf of the minion, this is True. This happens when the master generates minion pillars. issue_params Dictionary of configuration values for the generated AppRole. See master configuration :vconf:`issue:approle:params` for possible values. Requires :vconf:`issue:allow_minion_override_params` master configuration setting to be effective. """ log.debug( "role-id request for %s (impersonated by master: %s)", minion_id, impersonated_by_master, ) _validate_signature(minion_id, signature, impersonated_by_master) try: if _config("issue:type") != "approle": return {"expire_cache": True, "error": "Master does not issue AppRoles."} ret = { "server": _config("server"), "data": {}, } wrap = _config("issue:wrap") role_id = _get_role_id(minion_id, issue_params=issue_params, wrap=wrap) if wrap: ret.update(role_id) else: ret["data"]["role_id"] = role_id return ret except Exception as err: # pylint: disable=broad-except return {"error": f"{type(err).__name__}: {str(err)}"}
def _get_role_id(minion_id, issue_params, wrap): approle = _lookup_approle_cached(minion_id) issue_params_parsed = _parse_issue_params(issue_params) if approle is False or ( helpers._get_salt_run_type(__opts__) != helpers.SALT_RUNTYPE_MASTER_IMPERSONATING and not _approle_params_match(approle, issue_params_parsed) ): # This means the role has to be created/updated first # create/update AppRole with role name <minion_id> # token_policies are set on the AppRole log.debug("Managing AppRole for %s.", minion_id) _manage_approle(minion_id, issue_params) # Make sure cached data is refreshed. Clearing the cache would suffice # here, but this branch should not be hit too often, so opt for simplicity. _lookup_approle_cached(minion_id, refresh=True) role_id = _lookup_role_id(minion_id, wrap=wrap) if role_id is False: raise SaltRunnerError(f"Failed to create AppRole for minion {minion_id}.") if approle is False: # This means the AppRole has just been created # create/update entity with name salt_minion_<minion_id> # metadata is set on the entity (to allow policy path templating) _manage_entity(minion_id) # ensure the new AppRole is mapped to the entity _manage_entity_alias(minion_id) if wrap: return role_id.serialize_for_minion() return role_id def _approle_params_match(current, issue_params): """ Check if minion-overridable AppRole parameters match """ req = _parse_issue_params(issue_params) for var in set(VALID_PARAMS["approle"]) - set(NO_OVERRIDE_PARAMS["approle"]): if var in req and req[var] != current.get(var, NOT_SET): return False return True
[docs] def generate_secret_id(minion_id, signature, impersonated_by_master=False, issue_params=None): """ .. versionadded:: 1.0.0 Generate a Vault secret ID for minion <minion_id>. Requires the master to be configured to generate AppRoles for minions (:vconf:`issue:type`). minion_id The ID of the minion that requests a secret ID. signature Cryptographic signature which validates that the request is indeed sent by the minion (or the master, see ``impersonated_by_master``). impersonated_by_master If the master needs to create a token on behalf of the minion, this is True. This happens when the master generates minion pillars. issue_params Dictionary of configuration values for the generated AppRole. See master configuration :vconf:`issue:approle:params` for possible values. Requires :vconf:`issue:allow_minion_override_params` master configuration setting to be effective. """ log.debug( "Secret ID generation request for %s (impersonated by master: %s)", minion_id, impersonated_by_master, ) _validate_signature(minion_id, signature, impersonated_by_master) try: if _config("issue:type") != "approle": return { "expire_cache": True, "error": "Master does not issue AppRoles nor secret IDs.", } approle_meta = _lookup_approle_cached(minion_id) if approle_meta is False: raise vault.VaultNotFoundError(f"No AppRole found for minion {minion_id}.") if helpers._get_salt_run_type( __opts__ ) != helpers.SALT_RUNTYPE_MASTER_IMPERSONATING and not _approle_params_match( approle_meta, issue_params ): _manage_approle(minion_id, issue_params) approle_meta = _lookup_approle_cached(minion_id, refresh=True) if not approle_meta["bind_secret_id"]: return { "expire_cache": True, "error": "Minion AppRole does not require a secret ID.", } ret = { "server": _config("server"), "data": {}, } wrap = _config("issue:wrap") secret_id = _get_secret_id(minion_id, wrap=wrap) if wrap: ret.update(secret_id.serialize_for_minion()) else: ret["data"] = secret_id.serialize_for_minion() ret["misc_data"] = { "secret_id_num_uses": approle_meta["secret_id_num_uses"], } return ret except vault.VaultNotFoundError as err: # when the role does not exist, make sure the minion requests # new configuration details to generate one return { "expire_cache": True, "error": f"{type(err).__name__}: {str(err)}", } except Exception as err: # pylint: disable=broad-except return {"error": f"{type(err).__name__}: {str(err)}"}
[docs] def unseal(): """ Unseal the Vault server. Uses keys from the master config :vconf:`keys` . .. note:: This function will send unseal keys until the API returns success. CLI Example: .. code-block:: bash salt-run vault.unseal """ config = factory.parse_config(__opts__.get("vault", {})) client = VaultClient(**config["server"], **config["client"]) for key in __opts__["vault"]["keys"]: ret = client.post("sys/unseal", payload={"key": key}) # Return immediately after Vault is unsealed. No need to go over all the keys if ret["sealed"] is False: return True return False
[docs] def show_policies(minion_id, refresh_pillar=NOT_SET, expire=None): """ Show the Vault policies that are applied to tokens for the given minion. minion_id The ID of the minion to show policies for. refresh_pillar Whether to refresh the pillar data when rendering templated policies. None will only refresh when the cached data is unavailable, boolean values force one behavior always. Defaults to :vconf:`policies:refresh_pillar` or None. expire Policy computation can be heavy in case pillar data is used in templated policies and it has not been cached. Therefore, a short-lived cache specifically for rendered policies is used. This specifies the expiration timeout in seconds. Defaults to :vconf:`policies:cache_time` or 60. .. note:: When issuing AppRoles to minions, the shown policies are read from Vault configuration for the minion's AppRole and thus refresh_pillar/expire will not be honored. CLI Example: .. code-block:: bash salt-run vault.show_policies myminion """ if _config("issue:type") == "approle": meta = _lookup_approle(minion_id) return meta["token_policies"] if refresh_pillar == NOT_SET: refresh_pillar = _config("policies:refresh_pillar") expire = expire if expire is not None else _config("policies:cache_time") return _get_policies_cached(minion_id, refresh_pillar=refresh_pillar, expire=expire)
[docs] def sync_approles(minions=None, up=False, down=False): """ .. versionadded:: 1.0.0 Sync minion AppRole parameters with current settings, including associated token policies. .. note:: Only updates existing AppRoles. They are issued during the first request for one by the minion. Running this will reset minion overrides, which are reapplied automatically during the next request for authentication details. .. note:: Unlike when issuing tokens, AppRole-associated policies are not regularly refreshed automatically. It is advised to schedule regular runs of this function. If no parameter is specified, will try to sync AppRoles for all known minions. CLI Example: .. code-block:: bash salt-run vault.sync_approles salt-run vault.sync_approles ecorp minions (List of) ID(s) of the minion(s) to update the AppRole for. Defaults to None. up Find all minions that are up and update their AppRoles. Defaults to False. down Find all minions that are down and update their AppRoles. Defaults to False. """ if _config("issue:type") != "approle": raise SaltRunnerError("Master does not issue AppRoles to minions.") if minions is not None: if not isinstance(minions, list): minions = [minions] elif up or down: minions = [] if up: minions.extend(__salt__["manage.list_state"]()) if down: minions.extend(__salt__["manage.list_not_state"]()) else: minions = _list_all_known_minions() for minion in set(minions) & set(list_approles()): _manage_approle(minion, issue_params=None) _lookup_approle_cached(minion, refresh=True) # Running multiple pillar renders in a loop would otherwise # falsely report a cyclic dependency (same loader context?) __opts__.pop("_vault_runner_is_compiling_pillar_templates", None) return True
[docs] def list_approles(): """ .. versionadded:: 1.0.0 List all AppRoles that have been created by the Salt master. They are named after the minions. CLI Example: .. code-block:: bash salt-run vault.list_approles Required policy: .. code-block:: vaultpolicy path "auth/<mount>/role" { capabilities = ["list"] } """ if _config("issue:type") != "approle": raise SaltRunnerError("Master does not issue AppRoles to minions.") api = _get_approle_api() return api.list_approles(mount=_config("issue:approle:mount"))
[docs] def sync_entities(minions=None, up=False, down=False): """ .. versionadded:: 1.0.0 Sync minion entities with current settings. Only updates entities for minions with existing AppRoles. .. note:: This updates associated metadata only. Entities are created only when issuing AppRoles to minions (:vconf:`issue:type` == ``approle``). If no parameter is specified, will try to sync entities for all known minions. CLI Example: .. code-block:: bash salt-run vault.sync_entities minions (List of) ID(s) of the minion(s) to update the entity for. Defaults to None. up Find all minions that are up and update their associated entities. Defaults to False. down Find all minions that are down and update their associated entities. Defaults to False. """ if _config("issue:type") != "approle": raise SaltRunnerError( "Master is not configured to issue AppRoles to minions, which is a " "requirement to use managed entities with Salt." ) if minions is not None: if not isinstance(minions, list): minions = [minions] elif up or down: minions = [] if up: minions.extend(__salt__["manage.list_state"]()) if down: minions.extend(__salt__["manage.list_not_state"]()) else: minions = _list_all_known_minions() for minion in set(minions) & set(list_approles()): _manage_entity(minion) # Running multiple pillar renders in a loop would otherwise # falsely report a cyclic dependency (same loader context?) __opts__.pop("_vault_runner_is_compiling_pillar_templates", None) entity = _lookup_entity_by_alias(minion) if not entity or entity["name"] != f"salt_minion_{minion}": log.info("Fixing association of minion AppRole to minion entity for %s.", minion) _manage_entity_alias(minion) return True
[docs] def list_entities(): """ .. versionadded:: 1.0.0 List all entities that have been created by the Salt master. They are named ``salt_minion_{minion_id}``. CLI Example: .. code-block:: bash salt-run vault.list_entities Required policy: .. code-block:: vaultpolicy path "identity/entity/name" { capabilities = ["list"] } """ if _config("issue:type") != "approle": raise SaltRunnerError("Master does not issue AppRoles to minions.") api = _get_identity_api() entities = api.list_entities() return [x for x in entities if x.startswith("salt_minion_")]
[docs] def show_entity(minion_id): """ .. versionadded:: 1.0.0 Show entity metadata for <minion_id>. CLI Example: .. code-block:: bash salt-run vault.show_entity db1 """ if _config("issue:type") != "approle": raise SaltRunnerError("Master does not issue AppRoles to minions.") api = _get_identity_api() return api.read_entity(f"salt_minion_{minion_id}")["metadata"]
[docs] def show_approle(minion_id): """ .. versionadded:: 1.0.0 Show AppRole configuration for <minion_id>. CLI Example: .. code-block:: bash salt-run vault.show_approle db1 """ if _config("issue:type") != "approle": raise SaltRunnerError("Master does not issue AppRoles to minions.") api = _get_approle_api() return api.read_approle(minion_id, mount=_config("issue:approle:mount"))
[docs] def cleanup_auth(): """ .. versionadded:: 1.0.0 Removes AppRoles and entities associated with unknown minion IDs. Can only clean up entities if the AppRole still exists. .. warning:: Make absolutely sure that the configured minion approle issue mount is exclusively dedicated to the Salt master, otherwise you might lose data by using this function! (:vconf:`issue:approle:mount`) This detects unknown existing AppRoles by listing all roles on the configured minion AppRole mount and deducting known minions from the returned list. CLI Example: .. code-block:: bash salt-run vault.cleanup_auth """ ret = {"approles": [], "entities": []} for minion in set(list_approles()) - set(_list_all_known_minions()): if _fetch_entity_by_name(minion): _delete_entity(minion) ret["entities"].append(minion) _delete_approle(minion) ret["approles"].append(minion) return {"deleted": ret}
[docs] def clear_cache(master=True, minions=True): """ .. versionadded:: 1.0.0 Clears master cache of Vault-specific data. This can include: - AppRole metadata - rendered policies - cached authentication credentials for impersonated minions - cached KV metadata for impersonated minions CLI Example: .. code-block:: bash salt-run vault.clear_cache salt-run vault.clear_cache minions=false salt-run vault.clear_cache master=false minions='[minion1, minion2]' master Clear cached data for the master context. Includes cached master authentication data and KV metadata. Defaults to true. minions Clear cached data for minions on the master. Can include cached authentication credentials and KV metadata for pillar compilation as well as AppRole metadata and rendered policies for credential issuance. Defaults to true. Set this to a list of minion IDs to only clear cached data pertaining to thse minions. """ config, _, _ = factory._get_connection_config("vault", __opts__, __context__, force_local=True) cache = vcache._get_cache_backend(config, __opts__) if cache is None: log.info("Vault cache clearance was requested, but no persistent cache is configured") return True if master: log.debug("Clearing master Vault cache") cache.flush("vault") if minions: for minion in cache.list("minions"): if minions is True or (isinstance(minions, list) and minion in minions): log.debug(f"Clearing master Vault cache for minion {minion}") cache.flush(f"minions/{minion}/vault") return True
def _config(key=None, default=vault.VaultException): ckey = "vault_master_config" if ckey not in __context__: __context__[ckey] = vault.parse_config(__opts__.get("vault", {})) if key is None: return __context__[ckey] val = salt.utils.data.traverse_dict(__context__[ckey], key, default) if val is vault.VaultException: raise vault.VaultException(f"Requested configuration value {key} does not exist.") return val def _list_all_known_minions(): return os.listdir(__opts__["pki_dir"] + "/minions") def _validate_signature(minion_id, signature, impersonated_by_master): """ Validate that either minion with id minion_id, or the master, signed the request """ if not impersonated_by_master and __opts__.get("cluster_id") is not None: pki_dir = Path(__opts__["cluster_pki_dir"]) else: pki_dir = Path(__opts__["pki_dir"]) if impersonated_by_master: public_key = pki_dir / "master.pub" else: public_key = pki_dir / "minions" / minion_id log.trace("Validating signature for %s", minion_id) signature = base64.b64decode(signature) if not salt.crypt.verify_signature(str(public_key), minion_id, signature): raise salt.exceptions.AuthenticationError( f"Could not validate token request from {minion_id}" ) log.trace("Signature ok") # **kwargs because salt.cache.Cache does not pop "expire" from kwargs def _get_policies(minion_id, refresh_pillar=None, **kwargs): # pylint: disable=unused-argument """ Get the policies that should be applied to a token for <minion_id> """ grains, pillar = _get_minion_data(minion_id, refresh_pillar) mappings = {"minion": minion_id, "grains": grains, "pillar": pillar} policies = [] for pattern in _config("policies:assign"): try: for expanded_pattern in helpers.expand_pattern_lists(pattern, **mappings): policies.append(expanded_pattern.format(**mappings).lower()) # Vault requirement except KeyError: log.warning("Could not resolve policy pattern %s for minion %s", pattern, minion_id) log.debug("%s policies: %s", minion_id, policies) return policies def _get_policies_cached(minion_id, refresh_pillar=None, expire=60): # expiration of 0 disables cache if not expire: return _get_policies(minion_id, refresh_pillar=refresh_pillar) cbank = f"minions/{minion_id}/vault" ckey = "policies" cache = salt.cache.factory(__opts__) policies = cache.cache( cbank, ckey, _get_policies, expire=expire, minion_id=minion_id, refresh_pillar=refresh_pillar, ) if not isinstance(policies, list): log.warning("Cached vault policies were not formed as a list. Refreshing.") cache.flush(cbank, ckey) policies = cache.cache( cbank, ckey, _get_policies, expire=expire, minion_id=minion_id, refresh_pillar=refresh_pillar, ) return policies def _get_minion_data(minion_id, refresh_pillar=None): _, grains, pillar = salt.utils.minions.get_minion_data(minion_id, __opts__) if grains is None: # In case no cached minion data is available, make sure the utils module # can distinguish a pillar refresh run impersonating a minion from running # on the master. grains = {"id": minion_id} # To properly refresh minion grains, something like this could be used: # __salt__["salt.execute"](minion_id, "saltutil.refresh_grains", refresh_pillar=False) # This is deliberately not done since grains should not be used to target # secrets anyways. # salt.utils.minions.get_minion_data only returns data from cache or None. # To make sure the correct policies are available, the pillar needs to be # refreshed. This can cause an infinite loop if the pillar data itself # depends on the vault execution module, which relies on this function. # By default, only refresh when necessary. Boolean values force one way. if refresh_pillar is True or (refresh_pillar is None and pillar is None): if __opts__.get("_vault_runner_is_compiling_pillar_templates"): raise SaltRunnerError( "Cyclic dependency detected while refreshing pillar for vault policy templating. " "This is caused by some pillar value relying on the vault execution module. " "Either remove the dependency from your pillar, disable refreshing pillar data " "for policy templating or do not use pillar values in policy templates." ) local_opts = copy.deepcopy(__opts__) # Relying on opts for ext_pillars does not work properly (only the first one runs # correctly). extra_minion_data = {"_vault_runner_is_compiling_pillar_templates": True} local_opts.update(extra_minion_data) pillar = LazyPillar(local_opts, grains, minion_id, extra_minion_data=extra_minion_data) elif pillar is None: # Make sure pillar is a dict. Necessary because a check on LazyPillar would # refresh it unconditionally (even when no pillar values are used) pillar = {} return grains, pillar def _get_metadata(minion_id, metadata_patterns, refresh_pillar=None): _, pillar = _get_minion_data(minion_id, refresh_pillar) mappings = { "minion": minion_id, "pillar": pillar, "jid": globals().get("__jid__", "<no jid set>"), "user": globals().get("__user__", "<no user set>"), } metadata = {} for key, pattern in metadata_patterns.items(): metadata[key] = [] try: for expanded_pattern in helpers.expand_pattern_lists(pattern, **mappings): metadata[key].append(expanded_pattern.format(**mappings)) except KeyError: log.warning( "Could not resolve metadata pattern %s for minion %s", pattern, minion_id, ) # Since composite values are disallowed for metadata, # at least ensure the order of the comma-separated string # is predictable metadata[key].sort() log.debug("%s metadata: %s", minion_id, metadata) return {k: ",".join(v) for k, v in metadata.items()} def _parse_issue_params(params, issue_type=None): if not _config("issue:allow_minion_override_params") or not isinstance(params, dict): params = {} # issue_type is used to override the configured type for minions using the old endpoint # TODO: remove this once the endpoint has been removed issue_type = issue_type or _config("issue:type") if issue_type not in VALID_PARAMS: raise SaltRunnerError("Invalid configuration for minion Vault authentication issuance.") configured_params = _config(f"issue:{issue_type}:params") ret = {} for valid_param in VALID_PARAMS[issue_type]: if valid_param in configured_params and configured_params[valid_param] is not None: ret[valid_param] = configured_params[valid_param] if ( valid_param in params and valid_param not in NO_OVERRIDE_PARAMS[issue_type] and params[valid_param] is not None ): ret[valid_param] = params[valid_param] return ret def _manage_approle(minion_id, issue_params): payload = _parse_issue_params(issue_params) # When the entity is managed during the same run, this can result in a duplicate # pillar refresh. Potential for optimization. payload["token_policies"] = _get_policies(minion_id, refresh_pillar=True) api = _get_approle_api() log.debug("Creating/updating AppRole for minion %s.", minion_id) return api.write_approle(minion_id, **payload, mount=_config("issue:approle:mount")) def _delete_approle(minion_id): api = _get_approle_api() log.debug("Deleting approle for minion %s.", minion_id) return api.delete_approle(minion_id, mount=_config("issue:approle:mount")) def _lookup_approle(minion_id, **kwargs): # pylint: disable=unused-argument api = _get_approle_api() try: return api.read_approle(minion_id, mount=_config("issue:approle:mount")) except vault.VaultNotFoundError: return False def _lookup_approle_cached(minion_id, expire=3600, refresh=False): # expiration of 0 disables cache if not expire: return _lookup_approle(minion_id) cbank = f"minions/{minion_id}/vault" ckey = "approle_meta" cache = salt.cache.factory(__opts__) if refresh: cache.flush(cbank, ckey) meta = cache.cache( cbank, ckey, _lookup_approle, expire=expire, minion_id=minion_id, ) if not isinstance(meta, dict): log.warning( "Cached Vault AppRole meta information was not formed as a dictionary. Refreshing." ) cache.flush(cbank, ckey) meta = cache.cache( cbank, ckey, _lookup_approle, expire=expire, minion_id=minion_id, ) # Falsey values are always refreshed by salt.cache.Cache return meta def _lookup_role_id(minion_id, wrap): api = _get_approle_api() try: return api.read_role_id(minion_id, mount=_config("issue:approle:mount"), wrap=wrap) except vault.VaultNotFoundError: return False def _get_secret_id(minion_id, wrap): api = _get_approle_api() return api.generate_secret_id( minion_id, metadata=_get_metadata(minion_id, _config("metadata:secret")), mount=_config("issue:approle:mount"), wrap=wrap, ) def _lookup_entity_by_alias(minion_id): """ This issues a lookup for the entity using the role-id and mount accessor, thus verifies that an entity and associated entity alias exists. """ role_id = _lookup_role_id(minion_id, wrap=False) api = _get_identity_api() try: return api.read_entity_by_alias(alias=role_id, mount=_config("issue:approle:mount")) except vault.VaultNotFoundError: return False def _fetch_entity_by_name(minion_id): api = _get_identity_api() try: return api.read_entity(name=f"salt_minion_{minion_id}") except vault.VaultNotFoundError: return False def _manage_entity(minion_id): # When the approle is managed during the same run, this can result in a duplicate # pillar refresh. Potential for optimization. metadata = _get_metadata(minion_id, _config("metadata:entity"), refresh_pillar=True) api = _get_identity_api() return api.write_entity(f"salt_minion_{minion_id}", metadata=metadata) def _delete_entity(minion_id): api = _get_identity_api() return api.delete_entity(f"salt_minion_{minion_id}") def _manage_entity_alias(minion_id): role_id = _lookup_role_id(minion_id, wrap=False) api = _get_identity_api() log.debug("Creating entity alias for minion %s.", minion_id) try: return api.write_entity_alias( f"salt_minion_{minion_id}", alias_name=role_id, mount=_config("issue:approle:mount"), ) except vault.VaultNotFoundError as err: raise SaltRunnerError( f"Cannot create alias for minion {minion_id}: no entity found." ) from err def _get_approle_api(): return vault.get_approle_api(__opts__, __context__, force_local=True) def _get_identity_api(): return vault.get_identity_api(__opts__, __context__, force_local=True) def _get_master_client(): # force_local is necessary when issuing credentials while impersonating # minions since the opts dict cannot be used to distinguish master from # minion in that case return vault.get_authd_client(__opts__, __context__, force_local=True) def _revoke_token(token=None, accessor=None): if not token and not accessor: raise SaltInvocationError("Need either token or accessor to revoke token.") endpoint = "auth/token/revoke" if token: payload = {"token": token} else: endpoint += "-accessor" payload = {"accessor": accessor} client = _get_master_client() return client.post(endpoint, payload=payload)
[docs] class LazyPillar(Mapping): """ Simulates a pillar dictionary. Only compiles the pillar once an item is requested. """ def __init__(self, opts, grains, minion_id, extra_minion_data=None): self.opts = opts self.grains = grains self.minion_id = minion_id self.extra_minion_data = extra_minion_data or {} self._pillar = None def _load(self): log.info("Refreshing pillar for vault templating.") self._pillar = salt.pillar.get_pillar( self.opts, self.grains, self.minion_id, extra_minion_data=self.extra_minion_data, ).compile_pillar() def __getitem__(self, key): if self._pillar is None: self._load() return self._pillar[key] def __iter__(self): if self._pillar is None: self._load() yield from self._pillar def __len__(self): if self._pillar is None: self._load() return len(self._pillar)