"""
Use secrets sourced from Vault in minion pillars.
.. important::
This module requires the general :ref:`Vault setup <vault-setup>`.
.. warning::
A minion must not be able to write to its own pillar source path,
otherwise a core security assumption in Salt is violated.
.. versionchanged:: 1.0.0
Previous versions of this pillar module found in Salt core were configured
with a parameter named ``conf``, expecting a single value representing
the path to include in the pillar with the prefix ``path=``.
This parameter has been deprecated. Please configure this pillar module
either by just passing the path or declaring it as ``path: <path>``.
Setup
-----
Include this module in your :conf_master:`ext_pillar` configuration:
.. code-block:: yaml
ext_pillar:
- vault: salt/global
.. hint::
You can also include multiple instances of this module in your configuration.
Now all keys of the Vault KV path ``salt/global`` are inserted into each
minion's pillar, which is quite inflexible and usually not what is wanted.
To work around that, you can :ref:`template the path <vault-templating>`.
.. code-block:: yaml
ext_pillar:
- vault: salt/minions/{minion}
- vault: salt/roles/{pillar[roles]}
.. note::
There is currently no ``top.sls`` equivalent.
.. note::
If a pattern matches multiple paths, the results are merged according
to the master configuration values :conf_master:`pillar_source_merging_strategy <pillar_source_merging_strategy>`
and :conf_master:`pillar_merge_lists <pillar_merge_lists>` by default.
If the optional :vconf:`nesting_key <pillar.nesting_key>` is defined,
the merged result is nested below.
There is currently no way to nest multiple results under different keys.
.. vconf:: pillar
Configuration reference
-----------------------
.. vconf:: pillar.path
``path``
The path to include in the minion pillars. Can be :ref:`templated <vault-templating>`.
.. vconf:: pillar.nesting_key
``nesting_key``
The Vault-sourced pillar values are usually merged into the root
of the pillar. This option allows you to specify a parent key
under which all values are nested. If the key contains previous
values, they are merged.
.. vconf:: pillar.merge_strategy
``merge_strategy``
When multiple paths are matched by a templated path, use this merge strategy
instead of :conf_master:`pillar_source_merging_strategy <pillar_source_merging_strategy>`.
.. vconf:: pillar.merge_lists
``merge_lists``
Override the default set by :conf_master:`pillar_merge_lists <pillar_merge_lists>`.
Complete configuration
----------------------
.. code-block:: yaml
ext_pillar:
- vault:
path: salt/roles/{pillar[roles]}
nesting_key: vault_sourced
merge_strategy: smart
merge_lists: false
"""
import logging
import salt.utils.dictupdate
from salt.exceptions import InvalidConfigError
from salt.exceptions import SaltException
from saltext.vault.utils import vault
from saltext.vault.utils.vault import helpers
from saltext.vault.utils.versions import warn_until
log = logging.getLogger(__name__)
[docs]
def ext_pillar(
minion_id, # pylint: disable=W0613
pillar, # pylint: disable=W0613
path=None,
nesting_key=None,
merge_strategy=None,
merge_lists=None,
extra_minion_data=None,
conf=None,
):
"""
Get pillar data from Vault for the configuration ``conf``.
"""
extra_minion_data = extra_minion_data or {}
if extra_minion_data.get("_vault_runner_is_compiling_pillar_templates"):
# Disable vault ext_pillar while compiling pillar for vault policy templates
return {}
if conf is not None:
comps = conf.split()
paths = [comp for comp in comps if comp.startswith("path=")]
if not paths:
log.error('"%s" is not a valid Vault ext_pillar config', conf)
return {}
path_pattern = paths[0].replace("path=", "")
warn_until(
2,
(
"The `conf` parameter to the Vault pillar is deprecated. "
"Please migrate to the `path` parameter. It takes the path "
"as its parameter, without the `path=` prefix."
),
)
elif path is not None:
comps = path.split()
if not comps:
log.error('"%s" is not a valid Vault ext_pillar config', path)
return {}
if len(comps) > 1:
warn_until(
2,
(
"The `conf` parameter to the Vault pillar is deprecated. "
"Please migrate to the `path` parameter. It takes the path "
"as its parameter, without the `path=` prefix."
),
)
comps = [comp for comp in comps if comp.startswith("path=")]
if not comps:
log.error('"%s" is not a valid Vault ext_pillar config', path)
return {}
path = comps[0]
if path.startswith("path="):
warn_until(
2,
(
"The Vault pillar module should not be configured with "
"the `path=` prefix anymore. Please remove it from your "
"configuration."
),
)
path = path[5:]
path_pattern = path
else:
raise InvalidConfigError("Need a Vault path to include in the pillar")
merge_strategy = merge_strategy or __opts__.get("pillar_source_merging_strategy", "smart")
merge_lists = merge_lists or __opts__.get("pillar_merge_lists", False)
vault_pillar = {}
for rendered_path in _get_paths(path_pattern, minion_id, pillar):
try:
vault_pillar_single = vault.read_kv(rendered_path, __opts__, __context__)
except vault.VaultNotFoundError:
log.info(
"Vault secret not found for: %s", rendered_path, exc_info_on_loglevel=logging.DEBUG
)
except SaltException:
log.warning(
"Error fetching Vault secret at: %s",
rendered_path,
exc_info_on_loglevel=logging.DEBUG,
)
else:
vault_pillar = salt.utils.dictupdate.merge(
vault_pillar,
vault_pillar_single,
strategy=merge_strategy,
merge_lists=merge_lists,
)
if nesting_key:
vault_pillar = {nesting_key: vault_pillar}
return vault_pillar
def _get_paths(path_pattern, minion_id, pillar):
"""
Get the paths that should be merged into the pillar dict
"""
mappings = {"minion": minion_id, "pillar": pillar}
paths = []
try:
for expanded_pattern in helpers.expand_pattern_lists(path_pattern, **mappings):
paths.append(expanded_pattern.format(**mappings))
except KeyError:
log.warning("Could not resolve pillar path pattern %s", path_pattern)
log.debug("%s vault pillar paths: %s", minion_id, paths)
return paths