1""" 2:maintainer: SaltStack 3:maturity: new 4:platform: all 5 6Runner functions supporting the Vault modules. Configuration instructions are 7documented in the execution module docs. 8""" 9 10import base64 11import json 12import logging 13import string 14import time 15 16import requests 17import salt.crypt 18import salt.exceptions 19 20log = logging.getLogger(__name__) 21 22 23def generate_token( 24 minion_id, signature, impersonated_by_master=False, ttl=None, uses=None 25): 26 """ 27 Generate a Vault token for minion minion_id 28 29 minion_id 30 The id of the minion that requests a token 31 32 signature 33 Cryptographic signature which validates that the request is indeed sent 34 by the minion (or the master, see impersonated_by_master). 35 36 impersonated_by_master 37 If the master needs to create a token on behalf of the minion, this is 38 True. This happens when the master generates minion pillars. 39 40 ttl 41 Ticket time to live in seconds, 1m minutes, or 2h hrs 42 43 uses 44 Number of times a token can be used 45 """ 46 log.debug( 47 "Token generation request for %s (impersonated by master: %s)", 48 minion_id, 49 impersonated_by_master, 50 ) 51 _validate_signature(minion_id, signature, impersonated_by_master) 52 try: 53 config = __opts__.get("vault", {}) 54 verify = config.get("verify", None) 55 # Vault Enterprise requires a namespace 56 namespace = config.get("namespace") 57 # Allow disabling of minion provided values via the master 58 allow_minion_override = config["auth"].get("allow_minion_override", False) 59 # This preserves the previous behavior of default TTL and 1 use 60 if not allow_minion_override or uses is None: 61 uses = config["auth"].get("uses", 1) 62 if not allow_minion_override or ttl is None: 63 ttl = config["auth"].get("ttl", None) 64 storage_type = config["auth"].get("token_backend", "session") 65 66 if config["auth"]["method"] == "approle": 67 if _selftoken_expired(): 68 log.debug("Vault token expired. Recreating one") 69 # Requesting a short ttl token 70 url = "{}/v1/auth/approle/login".format(config["url"]) 71 payload = {"role_id": config["auth"]["role_id"]} 72 if "secret_id" in config["auth"]: 73 payload["secret_id"] = config["auth"]["secret_id"] 74 # Vault Enterprise call requires headers 75 headers = None 76 if namespace is not None: 77 headers = {"X-Vault-Namespace": namespace} 78 response = requests.post( 79 url, headers=headers, json=payload, verify=verify 80 ) 81 if response.status_code != 200: 82 return {"error": response.reason} 83 config["auth"]["token"] = response.json()["auth"]["client_token"] 84 85 url = _get_token_create_url(config) 86 headers = {"X-Vault-Token": config["auth"]["token"]} 87 if namespace is not None: 88 headers["X-Vault-Namespace"] = namespace 89 audit_data = { 90 "saltstack-jid": globals().get("__jid__", "<no jid set>"), 91 "saltstack-minion": minion_id, 92 "saltstack-user": globals().get("__user__", "<no user set>"), 93 } 94 payload = { 95 "policies": _get_policies(minion_id, config), 96 "num_uses": uses, 97 "meta": audit_data, 98 } 99 100 if ttl is not None: 101 payload["explicit_max_ttl"] = str(ttl) 102 103 if payload["policies"] == []: 104 return {"error": "No policies matched minion"} 105 106 log.trace("Sending token creation request to Vault") 107 response = requests.post(url, headers=headers, json=payload, verify=verify) 108 109 if response.status_code != 200: 110 return {"error": response.reason} 111 112 auth_data = response.json()["auth"] 113 ret = { 114 "token": auth_data["client_token"], 115 "lease_duration": auth_data["lease_duration"], 116 "renewable": auth_data["renewable"], 117 "issued": int(round(time.time())), 118 "url": config["url"], 119 "verify": verify, 120 "token_backend": storage_type, 121 "namespace": namespace, 122 } 123 if uses >= 0: 124 ret["uses"] = uses 125 126 return ret 127 except Exception as e: # pylint: disable=broad-except 128 return {"error": str(e)} 129 130 131def unseal(): 132 """ 133 Unseal Vault server 134 135 This function uses the 'keys' from the 'vault' configuration to unseal vault server 136 137 vault: 138 keys: 139 - n63/TbrQuL3xaIW7ZZpuXj/tIfnK1/MbVxO4vT3wYD2A 140 - S9OwCvMRhErEA4NVVELYBs6w/Me6+urgUr24xGK44Uy3 141 - F1j4b7JKq850NS6Kboiy5laJ0xY8dWJvB3fcwA+SraYl 142 - 1cYtvjKJNDVam9c7HNqJUfINk4PYyAXIpjkpN/sIuzPv 143 - 3pPK5X6vGtwLhNOFv1U2elahECz3HpRUfNXJFYLw6lid 144 145 .. note: This function will send unsealed keys until the api returns back 146 that the vault has been unsealed 147 148 CLI Examples: 149 150 .. code-block:: bash 151 152 salt-run vault.unseal 153 """ 154 for key in __opts__["vault"]["keys"]: 155 ret = __utils__["vault.make_request"]( 156 "PUT", "v1/sys/unseal", data=json.dumps({"key": key}) 157 ).json() 158 if ret["sealed"] is False: 159 return True 160 return False 161 162 163def show_policies(minion_id): 164 """ 165 Show the Vault policies that are applied to tokens for the given minion 166 167 minion_id 168 The minions id 169 170 CLI Example: 171 172 .. code-block:: bash 173 174 salt-run vault.show_policies myminion 175 """ 176 config = __opts__["vault"] 177 return _get_policies(minion_id, config) 178 179 180def _validate_signature(minion_id, signature, impersonated_by_master): 181 """ 182 Validate that either minion with id minion_id, or the master, signed the 183 request 184 """ 185 pki_dir = __opts__["pki_dir"] 186 if impersonated_by_master: 187 public_key = "{}/master.pub".format(pki_dir) 188 else: 189 public_key = "{}/minions/{}".format(pki_dir, minion_id) 190 191 log.trace("Validating signature for %s", minion_id) 192 signature = base64.b64decode(signature) 193 if not salt.crypt.verify_signature(public_key, minion_id, signature): 194 raise salt.exceptions.AuthenticationError( 195 "Could not validate token request from {}".format(minion_id) 196 ) 197 log.trace("Signature ok") 198 199 200def _get_policies(minion_id, config): 201 """ 202 Get the policies that should be applied to a token for minion_id 203 """ 204 _, grains, _ = salt.utils.minions.get_minion_data(minion_id, __opts__) 205 policy_patterns = config.get( 206 "policies", ["saltstack/minion/{minion}", "saltstack/minions"] 207 ) 208 mappings = {"minion": minion_id, "grains": grains or {}} 209 210 policies = [] 211 for pattern in policy_patterns: 212 try: 213 for expanded_pattern in _expand_pattern_lists(pattern, **mappings): 214 policies.append( 215 expanded_pattern.format(**mappings).lower() # Vault requirement 216 ) 217 except KeyError: 218 log.warning("Could not resolve policy pattern %s", pattern) 219 220 log.debug("%s policies: %s", minion_id, policies) 221 return policies 222 223 224def _expand_pattern_lists(pattern, **mappings): 225 """ 226 Expands the pattern for any list-valued mappings, such that for any list of 227 length N in the mappings present in the pattern, N copies of the pattern are 228 returned, each with an element of the list substituted. 229 230 pattern: 231 A pattern to expand, for example ``by-role/{grains[roles]}`` 232 233 mappings: 234 A dictionary of variables that can be expanded into the pattern. 235 236 Example: Given the pattern `` by-role/{grains[roles]}`` and the below grains 237 238 .. code-block:: yaml 239 240 grains: 241 roles: 242 - web 243 - database 244 245 This function will expand into two patterns, 246 ``[by-role/web, by-role/database]``. 247 248 Note that this method does not expand any non-list patterns. 249 """ 250 expanded_patterns = [] 251 f = string.Formatter() 252 253 # This function uses a string.Formatter to get all the formatting tokens from 254 # the pattern, then recursively replaces tokens whose expanded value is a 255 # list. For a list with N items, it will create N new pattern strings and 256 # then continue with the next token. In practice this is expected to not be 257 # very expensive, since patterns will typically involve a handful of lists at 258 # most. 259 260 for (_, field_name, _, _) in f.parse(pattern): 261 if field_name is None: 262 continue 263 (value, _) = f.get_field(field_name, None, mappings) 264 if isinstance(value, list): 265 token = "{{{0}}}".format(field_name) 266 expanded = [pattern.replace(token, str(elem)) for elem in value] 267 for expanded_item in expanded: 268 result = _expand_pattern_lists(expanded_item, **mappings) 269 expanded_patterns += result 270 return expanded_patterns 271 return [pattern] 272 273 274def _selftoken_expired(): 275 """ 276 Validate the current token exists and is still valid 277 """ 278 try: 279 verify = __opts__["vault"].get("verify", None) 280 # Vault Enterprise requires a namespace 281 namespace = __opts__["vault"].get("namespace") 282 url = "{}/v1/auth/token/lookup-self".format(__opts__["vault"]["url"]) 283 if "token" not in __opts__["vault"]["auth"]: 284 return True 285 headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]} 286 # Add Vault namespace to headers if Vault Enterprise enabled 287 if namespace is not None: 288 headers["X-Vault-Namespace"] = namespace 289 response = requests.get(url, headers=headers, verify=verify) 290 if response.status_code != 200: 291 return True 292 return False 293 except Exception as e: # pylint: disable=broad-except 294 raise salt.exceptions.CommandExecutionError( 295 "Error while looking up self token : {}".format(str(e)) 296 ) 297 298 299def _get_token_create_url(config): 300 """ 301 Create Vault url for token creation 302 """ 303 role_name = config.get("role_name", None) 304 auth_path = "/v1/auth/token/create" 305 base_url = config["url"] 306 return "/".join(x.strip("/") for x in (base_url, auth_path, role_name) if x) 307