diff --git a/changelog/62120.added b/changelog/62120.added new file mode 100644 index 00000000000..4303d124f0b --- /dev/null +++ b/changelog/62120.added @@ -0,0 +1,4 @@ +Config option pass_variable_prefix allows to distinguish variables that contain paths to pass secrets. +Config option pass_strict_fetch allows to error out when a secret cannot be fetched from pass. +Config option pass_dir allows setting the PASSWORD_STORE_DIR env for pass. +Config option pass_gnupghome allows setting the $GNUPGHOME env for pass. diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 3489b83fd28..3cbc0c0f826 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -964,6 +964,14 @@ VALID_OPTS = immutabletypes.freeze( # The port to be used when checking if a master is connected to a # minion "remote_minions_port": int, + # pass renderer: Fetch secrets only for the template variables matching the prefix + "pass_variable_prefix": str, + # pass renderer: Whether to error out when unable to fetch a secret + "pass_strict_fetch": bool, + # pass renderer: Set GNUPGHOME env for Pass + "pass_gnupghome": str, + # pass renderer: Set PASSWORD_STORE_DIR env for Pass + "pass_dir": str, } ) @@ -1604,6 +1612,10 @@ DEFAULT_MASTER_OPTS = immutabletypes.freeze( "fips_mode": False, "detect_remote_minions": False, "remote_minions_port": 22, + "pass_variable_prefix": "", + "pass_strict_fetch": False, + "pass_gnupghome": "", + "pass_dir": "", } ) diff --git a/salt/renderers/pass.py b/salt/renderers/pass.py index 4e06d727d09..66d9ff650eb 100644 --- a/salt/renderers/pass.py +++ b/salt/renderers/pass.py @@ -45,6 +45,34 @@ Install pass binary pass: pkg.installed + +Salt master configuration options + +.. code-block:: yaml + + # If the prefix is *not* set (default behavior), all template variables are + # considered for fetching secrets from Pass. Those that cannot be resolved + # to a secret are passed through. + # + # If the prefix is set, only the template variables with matching prefix are + # considered for fetching the secrets, other variables are passed through. + # + # For ease of use it is recommended to set the following options as well: + # renderer: 'jinja|yaml|pass' + # pass_strict_fetch: true + # + pass_variable_prefix: 'pass:' + + # If set to 'true', error out when unable to fetch a secret for a template variable. + pass_strict_fetch: true + + # Set GNUPGHOME env for Pass. + # Defaults to: ~/.gnupg + pass_gnupghome: + + # Set PASSWORD_STORE_DIR env for Pass. + # Defaults to: ~/.password-store + pass_dir: """ @@ -54,7 +82,7 @@ from os.path import expanduser from subprocess import PIPE, Popen import salt.utils.path -from salt.exceptions import SaltRenderError +from salt.exceptions import SaltConfigurationError, SaltRenderError log = logging.getLogger(__name__) @@ -80,6 +108,23 @@ def _fetch_secret(pass_path): # Make a backup in case we want to return the original value without stripped whitespaces original_pass_path = pass_path + # Remove the optional prefix from pass path + pass_prefix = __opts__["pass_variable_prefix"] + if pass_prefix: + # If we do not see our prefix we do not want to process this variable + # and we return the unmodified pass path + if not pass_path.startswith(pass_prefix): + return pass_path + + # strip the prefix from the start of the string + pass_path = pass_path[len(pass_prefix) :] + + # The pass_strict_fetch option must be used with pass_variable_prefix + pass_strict_fetch = __opts__["pass_strict_fetch"] + if pass_strict_fetch and not pass_prefix: + msg = "The 'pass_strict_fetch' option requires 'pass_variable_prefix' option enabled" + raise SaltConfigurationError(msg) + # Remove whitespaces from the pass_path pass_path = pass_path.strip() @@ -91,14 +136,32 @@ def _fetch_secret(pass_path): env = os.environ.copy() env["HOME"] = expanduser("~") + pass_dir = __opts__["pass_dir"] + if pass_dir: + env["PASSWORD_STORE_DIR"] = pass_dir + + pass_gnupghome = __opts__["pass_gnupghome"] + if pass_gnupghome: + env["GNUPGHOME"] = pass_gnupghome + proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env) pass_data, pass_error = proc.communicate() # The version of pass used during development sent output to # stdout instead of stderr even though its returncode was non zero. if proc.returncode or not pass_data: - log.warning("Could not fetch secret: %s %s", pass_data, pass_error) - return original_pass_path + try: + pass_error = pass_error.decode("utf-8") + except (AttributeError, ValueError): + pass + msg = "Could not fetch secret '{}' from the password store: {}".format( + pass_path, pass_error + ) + if pass_strict_fetch: + raise SaltRenderError(msg) + else: + log.warning(msg) + return original_pass_path return pass_data.rstrip("\r\n")