Source code for saltext.vault.modules.vault_db

"""
Manage the Vault database secret engine, request and cache
leased database credentials.

.. versionadded:: 1.1.0

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

import logging
from datetime import datetime
from datetime import timezone

from salt.defaults import NOT_SET
from salt.exceptions import CommandExecutionError
from salt.exceptions import SaltInvocationError

from saltext.vault.utils import vault
from saltext.vault.utils.vault import db as vaultdb

log = logging.getLogger(__name__)


[docs] def list_connections(mount="database"): """ List configured database connections. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#list-connections>`__. CLI Example: .. code-block:: bash salt '*' vault_db.list_connections mount The mount path the DB backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/config" try: return vault.query("LIST", endpoint, __opts__, __context__)["data"]["keys"] except vault.VaultNotFoundError: return [] except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def fetch_connection(name, mount="database"): """ Read a configured database connection. Returns None if it does not exist. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#read-connection>`__. CLI Example: .. code-block:: bash salt '*' vault_db.fetch_connection mydb name The name of the database connection. mount The mount path the database backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/config/{name}" try: return vault.query("GET", endpoint, __opts__, __context__)["data"] except vault.VaultNotFoundError: return None except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def write_connection( name, plugin, version="", verify=True, allowed_roles=None, root_rotation_statements=None, password_policy=None, rotate=True, mount="database", **kwargs, ): """ Create/update a configured database connection. .. note:: This endpoint distinguishes between create and update ACL capabilities. .. note:: It is highly recommended to use a Vault-specific user rather than the admin user in the database when configuring the plugin. This user will be used to create/update/delete users within the database so it will need to have the appropriate permissions to do so. If the plugin supports rotating the root credentials, it is highly recommended to perform that action after configuring the plugin. This will change the password of the user configured in this step. The new password will not be viewable by users. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#configure-connection>`__. CLI Example: .. code-block:: bash salt '*' vault_db.write_connection mydb elasticsearch \ url=http://127.0.0.1:9200 username=vault password=hunter2 name The name of the database connection. plugin The name of the database plugin. Known plugins to this module are: ``cassandra``, ``couchbase``, ``elasticsearch``, ``influxdb``, ``hanadb``, ``mongodb``, ``mongodb_atlas``, ``mssql``, ``mysql``, ``oracle``, ``postgresql``, ``redis``, ``redis_elasticache``, ``redshift``, ``snowflake``. If you pass an unknown plugin, make sure its Vault-internal name can be formatted as ``{plugin}-database-plugin`` and to pass all required parameters as kwargs. version Specifies the semantic version of the plugin to use for this connection. verify Verify the connection during initial configuration. Defaults to True. allowed_roles List of the roles allowed to use this connection. ``["*"]`` means any role can use this connection. Defaults to empty (no role can use it). root_rotation_statements Specifies the database statements to be executed to rotate the root user's credentials. See the plugin's API page for more information on support and formatting for this parameter. password_policy The name of the password policy to use when generating passwords for this database. If not specified, this will use a default policy defined as: 20 characters with at least 1 uppercase, 1 lowercase, 1 number, and 1 dash character. rotate Rotate the root credentials after plugin setup. Defaults to True. mount The mount path the database backend is mounted to. Defaults to ``database``. kwargs Different plugins require different parameters. You need to make sure that you pass them as supplemental keyword arguments. For known plugins, the required arguments will be checked. """ endpoint = f"{mount}/config/{name}" plugin_meta = vaultdb.get_plugin_meta(plugin) plugin_name = plugin_meta["name"] or plugin payload = {k: v for k, v in kwargs.items() if not k.startswith("_")} if fetch_connection(name, mount=mount) is None: missing_kwargs = set(plugin_meta["required"]) - set(payload) if missing_kwargs: raise SaltInvocationError( f"The plugin {plugin} requires the following additional kwargs: {missing_kwargs}." ) payload["plugin_name"] = f"{plugin_name}-database-plugin" payload["verify_connection"] = verify if version is not None: payload["plugin_version"] = version if allowed_roles is not None: payload["allowed_roles"] = allowed_roles if root_rotation_statements is not None: payload["root_rotation_statements"] = root_rotation_statements if password_policy is not None: payload["password_policy"] = password_policy try: vault.query("POST", endpoint, __opts__, __context__, payload=payload) except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err if not rotate: return True return rotate_root(name, mount=mount)
[docs] def delete_connection(name, mount="database"): """ Delete a configured database connection. Returns None if it does not exist. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#delete-connection>`__. CLI Example: .. code-block:: bash salt '*' vault_db.delete_connection mydb name The name of the database connection. mount The mount path the database backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/config/{name}" try: return vault.query("DELETE", endpoint, __opts__, __context__) except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def reset_connection(name, mount="database"): """ Close a connection and restart its plugin with the configuration stored in the barrier. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#reset-connection>`__. CLI Example: .. code-block:: bash salt '*' vault_db.reset_connection mydb name The name of the database connection. mount The mount path the database backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/reset/{name}" try: return vault.query("POST", endpoint, __opts__, __context__) except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def rotate_root(name, mount="database"): """ Rotate the "root" user credentials stored for the database connection. .. warning:: The rotated password will not be accessible, so it is highly recommended to create a dedicated user account as Vault's configured "root". `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#rotate-root-credentials>`__. CLI Example: .. code-block:: bash salt '*' vault_db.rotate_root mydb name The name of the database connection. mount The mount path the database backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/rotate-root/{name}" try: return vault.query("POST", endpoint, __opts__, __context__) except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def list_roles(static=False, mount="database"): """ List configured database roles. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#list-roles>`__. `API method docs static <https://developer.hashicorp.com/vault/api-docs/secret/databases#list-static-roles>`__. CLI Example: .. code-block:: bash salt '*' vault_db.list_roles static Whether to list static roles. Defaults to False. mount The mount path the DB backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/{'static-' if static else ''}roles" try: return vault.query("LIST", endpoint, __opts__, __context__)["data"]["keys"] except vault.VaultNotFoundError: return [] except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def fetch_role(name, static=False, mount="database"): """ Read a configured database role. Returns None if it does not exist. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#read-role>`__. `API method docs static <https://developer.hashicorp.com/vault/api-docs/secret/databases#read-static-role>`__. CLI Example: .. code-block:: bash salt '*' vault_db.fetch_role myrole name The name of the database role. static Whether this role is static. Defaults to False. mount The mount path the database backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/{'static-' if static else ''}roles/{name}" try: return vault.query("GET", endpoint, __opts__, __context__)["data"] except vault.VaultNotFoundError: return None except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def write_static_role( name, connection, username, rotation_period, rotation_statements=None, credential_type=None, credential_config=None, mount="database", ): """ Create/update a database Static Role. Mind that not all databases support Static Roles. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#create-static-role>`__. CLI Example: .. code-block:: bash salt '*' vault_db.write_static_role myrole mydb myuser 24h name The name of the database role. connection The name of the database connection this role applies to. username The username to manage. rotation_period Specifies the amount of time Vault should wait before rotating the password. The minimum is ``5s``. rotation_statements Specifies the database statements to be executed to rotate the password for the configured database user. Not every plugin type will support this functionality. credential_type Specifies the type of credential that will be generated for the role. Options include: ``password``, ``rsa_private_key``. Defaults to ``password``. See the plugin's API page for credential types supported by individual databases. credential_config Specifies the configuration for the given ``credential_type`` as a mapping. For ``password``, only ``password_policy`` can be passed. For ``rsa_private_key``, ``key_bits`` (defaults to 2048) and ``format`` (defaults to ``pkcs8``) are available. mount The mount path the database backend is mounted to. Defaults to ``database``. """ payload = { "username": username, "rotation_period": rotation_period, } if rotation_statements is not None: payload["rotation_statements"] = rotation_statements return _write_role( name, connection, payload, credential_type=credential_type, credential_config=credential_config, static=True, mount=mount, )
[docs] def write_role( name, connection, creation_statements, default_ttl=None, max_ttl=None, revocation_statements=None, rollback_statements=None, renew_statements=None, credential_type=None, credential_config=None, mount="database", ): r""" Create/update a regular database role. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#create-role>`__. CLI Example: .. code-block:: bash salt '*' vault_db.write_role myrole mydb \ \["CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'", "GRANT SELECT ON *.* TO '{{name}}'@'%'"\] name The name of the database role. connection The name of the database connection this role applies to. creation_statements Specifies a list of database statements executed to create and configure a user, usually templated with {{name}} and {{password}}. Required. default_ttl Specifies the TTL for the leases associated with this role. Accepts time suffixed strings (1h) or an integer number of seconds. Defaults to system/engine default TTL time. max_ttl Specifies the maximum TTL for the leases associated with this role. Accepts time suffixed strings (1h) or an integer number of seconds. Defaults to sys/mounts's default TTL time; this value is allowed to be less than the mount max TTL (or, if not set, the system max TTL), but it is not allowed to be longer. revocation_statements Specifies a list of database statements to be executed to revoke a user. rollback_statements Specifies a list of database statements to be executed to rollback a create operation in the event of an error. Availability and formatting depend on the specific plugin. renew_statements Specifies a list of database statements to be executed to renew a user. Availability and formatting depend on the specific plugin. credential_type Specifies the type of credential that will be generated for the role. Options include: ``password``, ``rsa_private_key``. Defaults to ``password``. See the plugin's API page for credential types supported by individual databases. credential_config Specifies the configuration for the given ``credential_type`` as a mapping. For ``password``, only ``password_policy`` can be passed. For ``rsa_private_key``, ``key_bits`` (defaults to 2048) and ``format`` (defaults to ``pkcs8``) are available. mount The mount path the database backend is mounted to. Defaults to ``database``. """ payload = { "creation_statements": creation_statements, } if default_ttl is not None: payload["default_ttl"] = default_ttl if max_ttl is not None: payload["max_ttl"] = max_ttl if revocation_statements is not None: payload["revocation_statements"] = revocation_statements if rollback_statements is not None: payload["rollback_statements"] = rollback_statements if renew_statements is not None: payload["renew_statements"] = renew_statements return _write_role( name, connection, payload, credential_type=credential_type, credential_config=credential_config, static=False, mount=mount, )
def _write_role( name, connection, payload, credential_type=None, credential_config=None, static=False, mount="database", ): endpoint = f"{mount}/{'static-' if static else ''}roles/{name}" payload["db_name"] = connection if credential_type is not None: payload["credential_type"] = credential_type if credential_config is not None: valid_cred_configs = { "password": ["password_policy"], "rsa_private_key": ["key_bits", "format"], } credential_type = credential_type or "password" if credential_type in valid_cred_configs: invalid_configs = set(credential_config) - set(valid_cred_configs[credential_type]) if invalid_configs: raise SaltInvocationError( f"The following options are invalid for credential type {credential_type}: {invalid_configs}" ) payload["credential_config"] = credential_config try: return vault.query("POST", endpoint, __opts__, __context__, payload=payload) except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def delete_role(name, static=False, mount="database"): """ Delete a configured database role. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#delete-role>`__. `API method docs static <https://developer.hashicorp.com/vault/api-docs/secret/databases#delete-static-role>`__. CLI Example: .. code-block:: bash salt '*' vault_db.delete_role myrole name The name of the database role. static Whether this role is static. Defaults to False. mount The mount path the database backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/{'static-' if static else ''}roles/{name}" try: return vault.query("DELETE", endpoint, __opts__, __context__) except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err
[docs] def get_creds( name, static=False, cache=True, valid_for=NOT_SET, check_server=False, renew_increment=NOT_SET, revoke_delay=NOT_SET, meta=NOT_SET, mount="database", _warn_about_attr_change=True, ): """ Read credentials based on the named role. `API method docs <https://developer.hashicorp.com/vault/api-docs/secret/databases#generate-credentials>`__. `API method docs static <https://developer.hashicorp.com/vault/api-docs/secret/databases#get-static-credentials>`__. CLI Example: .. code-block:: bash salt '*' vault_db.get_creds myrole name The name of the database role. static Whether this role is static. Defaults to False. cache Whether to use cached credentials local to this minion to avoid unnecessary reissuance. When ``static`` is false, set this to a string to be able to use multiple distinct credentials using the same role on the same minion. Set this to false to disable caching. Defaults to true. .. note:: This uses the same cache backend as the Vault integration, so make sure you configure a persistent backend like ``disk`` if you expect the credentials to survive a single run. .. hint:: For some applications, you might need to know the resulting cache key of the credential lease. The cache key is composed as a concatenation of the following parts (with ``.``): * ``db`` * ``<mount>`` * ``dynamic`` if static=False, else ``static`` * ``<name>`` (the name of the database role) * ``default`` if cache=True or static=True, otherwise the value of ``cache`` Examples for a role named ``foo``: * mount=database, static=False, cache=True: ``db.database.dynamic.foo.default`` * mount=mariadb, static=False, cache=alt: ``db.mariadb.dynamic.foo.alt`` * mount=mariadb, static=True, cache=True: ``db.mariadb.static.foo.default`` valid_for When using cache, ensure the credentials are valid for at least this amount of time, otherwise request new ones. This can be an integer, which will be interpreted as seconds, or a time string using the same format as Vault does: Suffix ``s`` for seconds, ``m`` for minuts, ``h`` for hours, ``d`` for days. This will be cached together with the lease and might be used by other modules later. check_server Check on the Vault server whether the lease is still active and was not revoked early. Defaults to false. renew_increment When using cache and ``valid_for`` results in a renewal attempt, request this amount of time extension on the lease. This will be cached together with the lease and might be used by other modules later. revoke_delay When using cache and ``valid_for`` results in a revocation, set the lease validity to this value to allow a short amount of delay between the issuance of the new lease and the revocation of the old one. Defaults to ``60``. This will be cached together with the lease and might be used by other modules later. meta When using cache, this value will be cached together with the lease. It will be emitted by the ``vault_lease`` beacon module whenever a lease is running out (usually because it cannot be extended further). It is intended to support the reactor in deciding what needs to be done in order to to reconfigure dependent, Vault-unaware software with newly issued credentials. Entirely optional. mount The mount path the database backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/{'static-' if static else ''}creds/{name}" if cache: ckey = f"db.{mount}.{'static' if static else 'dynamic'}.{name}" if not static and isinstance(cache, str): ckey += f".{cache}" else: ckey += ".default" creds_cache = vault.get_lease_store(__opts__, __context__) cached_creds = creds_cache.get( ckey, valid_for=valid_for if valid_for is not NOT_SET else None, revoke=revoke_delay if revoke_delay is not NOT_SET else None, check_server=check_server, ) if cached_creds: changed = False for attr, val in ( ("min_ttl", valid_for), ("renew_increment", renew_increment), ("revoke_delay", revoke_delay), ("meta", meta), ): if val is not NOT_SET and getattr(cached_creds, attr) != val: setattr(cached_creds, attr, val) changed = True if changed: # Warn about changes if a lease is managed by the state module # and this function is called e.g. during YAML rendering, overwriting # the desired attributes. The state module sets this to false. if _warn_about_attr_change: log.warning(f"Cached credential `{ckey}` changed lifecycle attributes") creds_cache.store(ckey, cached_creds) return cached_creds.data try: res = vault.query("GET", endpoint, __opts__, __context__) except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err lease = vault.VaultLease( min_ttl=valid_for if valid_for is not NOT_SET else None, renew_increment=renew_increment if renew_increment is not NOT_SET else None, revoke_delay=revoke_delay if revoke_delay is not NOT_SET else None, meta=meta if meta is not NOT_SET else None, **res, ) if cache: creds_cache.store(ckey, lease) return lease.data
[docs] def clear_cached(name=None, mount=None, cache=None, static=None, delta=None, flush_on_failure=True): """ Clear and revoke cached database credentials matching specified parameters. CLI Example: .. code-block:: bash salt '*' vault_db.clear_cached name=myrole mount=database salt '*' vault_db.clear_cached mount=database salt '*' vault_db.clear_cached name Only clear credentials using this role name. mount Only clear credentials from this mount. cache Only clear credentials using this cache name (refer to get_creds for details). static Only clear static (``True``) or dynamic (``False``) credentials. delta Time after which the leases should be revoked by Vault. Defaults to what was set on the lease(s) during creation or 60s. flush_on_failure If a revocation fails, remove the lease from cache anyways. Defaults to true. """ creds_cache = vault.get_lease_store(__opts__, __context__) return creds_cache.revoke_cached( match=vaultdb.create_cache_pattern(name=name, mount=mount, cache=cache, static=static), delta=delta, flush_on_failure=flush_on_failure, )
[docs] def list_cached(name=None, mount=None, cache=None, static=None): """ List cached database credentials matching specified parameters. CLI Example: .. code-block:: bash salt '*' vault_db.list_cached name=myrole mount=database salt '*' vault_db.list_cached mount=database salt '*' vault_db.list_cached name Only list credentials using this role name. mount Only list credentials from this mount. cache Only list credentials using this cache name (refer to get_creds for details). static Only list static (``True``) or dynamic (``False``) credentials. """ creds_cache = vault.get_lease_store(__opts__, __context__) info = creds_cache.list_info( match=vaultdb.create_cache_pattern(name=name, mount=mount, cache=cache, static=static) ) for lease in info.values(): for val in ("creation_time", "expire_time"): if val in lease: lease[val] = ( datetime.fromtimestamp(lease[val], tz=timezone.utc) .astimezone() .strftime("%Y-%m-%d %H:%M:%S %Z") ) return info
[docs] def renew_cached(name=None, mount=None, cache=None, static=None, increment=None): """ Renew cached database credentials matching specified parameters. CLI Example: .. code-block:: bash salt '*' vault_db.renew_cached name=myrole mount=database salt '*' vault_db.renew_cached mount=database salt '*' vault_db.renew_cached name Only renew credentials using this role name. mount Only renew credentials from this mount. cache Only renew credentials using this cache name (refer to get_creds for details). static Only renew static (``True``) or dynamic (``False``) credentials. increment Request the leases to be valid for this amount of time from the current point of time onwards. Can also be used to reduce the validity period. The server might not honor this increment. Can be an integer (seconds) or a time string like ``1h``. Optional. If unset, defaults to what was set on the lease during creation or the lease's default TTL. """ creds_cache = vault.get_lease_store(__opts__, __context__) return creds_cache.renew_cached( match=vaultdb.create_cache_pattern(name=name, mount=mount, cache=cache, static=static), increment=increment, )
[docs] def rotate_static_role(name, mount="database"): """ Rotate Static Role credentials stored for a given role name. `API method docs static <https://developer.hashicorp.com/vault/api-docs/secret/databases#rotate-static-role-credentials>`__. CLI Example: .. code-block:: bash salt '*' vault_db.rotate_static_role mystaticrole name The name of the database role. mount The mount path the database backend is mounted to. Defaults to ``database``. """ endpoint = f"{mount}/rotate-role/{name}" try: return vault.query("POST", endpoint, __opts__, __context__) except vault.VaultException as err: raise CommandExecutionError(f"{err.__class__}: {err}") from err