1""" 2Control of entries in SSH authorized_key files 3============================================== 4 5The information stored in a user's SSH authorized key file can be easily 6controlled via the ssh_auth state. Defaults can be set by the enc, options, 7and comment keys. These defaults can be overridden by including them in the 8name. 9 10Since the YAML specification limits the length of simple keys to 1024 11characters, and since SSH keys are often longer than that, you may have 12to use a YAML 'explicit key', as demonstrated in the second example below. 13 14.. code-block:: yaml 15 16 AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY==: 17 ssh_auth.present: 18 - user: root 19 - enc: ssh-dss 20 21 ? AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY==... 22 : 23 ssh_auth.present: 24 - user: root 25 - enc: ssh-dss 26 27 thatch: 28 ssh_auth.present: 29 - user: root 30 - source: salt://ssh_keys/thatch.id_rsa.pub 31 - config: '%h/.ssh/authorized_keys' 32 33 sshkeys: 34 ssh_auth.present: 35 - user: root 36 - enc: ssh-rsa 37 - options: 38 - option1="value1" 39 - option2="value2 flag2" 40 - comment: myuser 41 - names: 42 - AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY== 43 - ssh-dss AAAAB3NzaCL0sQ9fJ5bYTEyY== user@domain 44 - option3="value3" ssh-dss AAAAB3NzaC1kcQ9J5bYTEyY== other@testdomain 45 - AAAAB3NzaC1kcQ9fJFF435bYTEyY== newcomment 46 47 sshkeys: 48 ssh_auth.manage: 49 - user: root 50 - enc: ssh-rsa 51 - options: 52 - option1="value1" 53 - option2="value2 flag2" 54 - comment: myuser 55 - ssh_keys: 56 - AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY== 57 - ssh-dss AAAAB3NzaCL0sQ9fJ5bYTEyY== user@domain 58 - option3="value3" ssh-dss AAAAB3NzaC1kcQ9J5bYTEyY== other@testdomain 59 - AAAAB3NzaC1kcQ9fJFF435bYTEyY== newcomment 60""" 61 62 63import re 64import sys 65 66 67def _present_test( 68 user, name, enc, comment, options, source, config, fingerprint_hash_type 69): 70 """ 71 Run checks for "present" 72 """ 73 result = None 74 if source: 75 keys = __salt__["ssh.check_key_file"]( 76 user, 77 source, 78 config, 79 saltenv=__env__, 80 fingerprint_hash_type=fingerprint_hash_type, 81 ) 82 if keys: 83 comment = "" 84 for key, status in keys.items(): 85 if status == "exists": 86 continue 87 comment += "Set to {}: {}\n".format(status, key) 88 if comment: 89 return result, comment 90 err = sys.modules[__salt__["test.ping"].__module__].__context__.pop( 91 "ssh_auth.error", None 92 ) 93 if err: 94 return False, err 95 else: 96 return ( 97 True, 98 "All host keys in file {} are already present".format(source), 99 ) 100 else: 101 # check if this is of form {options} {enc} {key} {comment} 102 sshre = re.compile(r"^(.*?)\s?((?:ssh\-|ecds)[\w-]+\s.+)$") 103 fullkey = sshre.search(name) 104 # if it is {key} [comment] 105 if not fullkey: 106 key_and_comment = name.split() 107 name = key_and_comment[0] 108 if len(key_and_comment) == 2: 109 comment = key_and_comment[1] 110 else: 111 # if there are options, set them 112 if fullkey.group(1): 113 options = fullkey.group(1).split(",") 114 # key is of format: {enc} {key} [comment] 115 comps = fullkey.group(2).split() 116 enc = comps[0] 117 name = comps[1] 118 if len(comps) == 3: 119 comment = comps[2] 120 121 check = __salt__["ssh.check_key"]( 122 user, 123 name, 124 enc, 125 comment, 126 options, 127 config=config, 128 fingerprint_hash_type=fingerprint_hash_type, 129 ) 130 if check == "update": 131 comment = "Key {} for user {} is set to be updated".format(name, user) 132 elif check == "add": 133 comment = "Key {} for user {} is set to be added".format(name, user) 134 elif check == "exists": 135 result = True 136 comment = "The authorized host key {} is already present for user {}".format( 137 name, user 138 ) 139 140 return result, comment 141 142 143def _absent_test( 144 user, name, enc, comment, options, source, config, fingerprint_hash_type 145): 146 """ 147 Run checks for "absent" 148 """ 149 result = None 150 if source: 151 keys = __salt__["ssh.check_key_file"]( 152 user, 153 source, 154 config, 155 saltenv=__env__, 156 fingerprint_hash_type=fingerprint_hash_type, 157 ) 158 if keys: 159 comment = "" 160 for key, status in list(keys.items()): 161 if status == "add": 162 continue 163 comment += "Set to remove: {}\n".format(key) 164 if comment: 165 return result, comment 166 err = sys.modules[__salt__["test.ping"].__module__].__context__.pop( 167 "ssh_auth.error", None 168 ) 169 if err: 170 return False, err 171 else: 172 return (True, "All host keys in file {} are already absent".format(source)) 173 else: 174 # check if this is of form {options} {enc} {key} {comment} 175 sshre = re.compile(r"^(.*?)\s?((?:ssh\-|ecds)[\w-]+\s.+)$") 176 fullkey = sshre.search(name) 177 # if it is {key} [comment] 178 if not fullkey: 179 key_and_comment = name.split() 180 name = key_and_comment[0] 181 if len(key_and_comment) == 2: 182 comment = key_and_comment[1] 183 else: 184 # if there are options, set them 185 if fullkey.group(1): 186 options = fullkey.group(1).split(",") 187 # key is of format: {enc} {key} [comment] 188 comps = fullkey.group(2).split() 189 enc = comps[0] 190 name = comps[1] 191 if len(comps) == 3: 192 comment = comps[2] 193 194 check = __salt__["ssh.check_key"]( 195 user, 196 name, 197 enc, 198 comment, 199 options, 200 config=config, 201 fingerprint_hash_type=fingerprint_hash_type, 202 ) 203 if check == "update" or check == "exists": 204 comment = "Key {} for user {} is set for removal".format(name, user) 205 else: 206 comment = "Key is already absent" 207 result = True 208 209 return result, comment 210 211 212def present( 213 name, 214 user, 215 enc="ssh-rsa", 216 comment="", 217 source="", 218 options=None, 219 config=".ssh/authorized_keys", 220 fingerprint_hash_type=None, 221 **kwargs 222): 223 """ 224 Verifies that the specified SSH key is present for the specified user 225 226 name 227 The SSH key to manage 228 229 user 230 The user who owns the SSH authorized keys file to modify 231 232 enc 233 Defines what type of key is being used; can be ed25519, ecdsa, ssh-rsa 234 or ssh-dss 235 236 comment 237 The comment to be placed with the SSH public key 238 239 source 240 The source file for the key(s). Can contain any number of public keys, 241 in standard "authorized_keys" format. If this is set, comment and enc 242 will be ignored. 243 244 .. note:: 245 The source file must contain keys in the format ``<enc> <key> 246 <comment>``. If you have generated a keypair using PuTTYgen, then you 247 will need to do the following to retrieve an OpenSSH-compatible public 248 key. 249 250 1. In PuTTYgen, click ``Load``, and select the *private* key file (not 251 the public key), and click ``Open``. 252 2. Copy the public key from the box labeled ``Public key for pasting 253 into OpenSSH authorized_keys file``. 254 3. Paste it into a new file. 255 256 options 257 The options passed to the key, pass a list object 258 259 config 260 The location of the authorized keys file relative to the user's home 261 directory, defaults to ".ssh/authorized_keys". Token expansion %u and 262 %h for username and home path supported. 263 264 fingerprint_hash_type 265 The public key fingerprint hash type that the public key fingerprint 266 was originally hashed with. This defaults to ``sha256`` if not specified. 267 """ 268 ret = {"name": name, "changes": {}, "result": True, "comment": ""} 269 270 if source == "": 271 # check if this is of form {options} {enc} {key} {comment} 272 sshre = re.compile(r"^(.*?)\s?((?:ssh\-|ecds)[\w-]+\s.+)$") 273 fullkey = sshre.search(name) 274 # if it is {key} [comment] 275 if not fullkey: 276 key_and_comment = name.split(None, 1) 277 name = key_and_comment[0] 278 if len(key_and_comment) == 2: 279 comment = key_and_comment[1] 280 else: 281 # if there are options, set them 282 if fullkey.group(1): 283 options = fullkey.group(1).split(",") 284 # key is of format: {enc} {key} [comment] 285 comps = fullkey.group(2).split(None, 2) 286 enc = comps[0] 287 name = comps[1] 288 if len(comps) == 3: 289 comment = comps[2] 290 291 if __opts__["test"]: 292 ret["result"], ret["comment"] = _present_test( 293 user, 294 name, 295 enc, 296 comment, 297 options or [], 298 source, 299 config, 300 fingerprint_hash_type, 301 ) 302 return ret 303 304 # Get only the path to the file without env referrences to check if exists 305 if source != "": 306 source_path = __salt__["cp.get_url"](source, None, saltenv=__env__) 307 308 if source != "" and not source_path: 309 data = "no key" 310 elif source != "" and source_path: 311 key = __salt__["cp.get_file_str"](source, saltenv=__env__) 312 filehasoptions = False 313 # check if this is of form {options} {enc} {key} {comment} 314 sshre = re.compile(r"^(ssh\-|ecds).*") 315 key = key.rstrip().split("\n") 316 for keyline in key: 317 filehasoptions = sshre.match(keyline) 318 if not filehasoptions: 319 data = __salt__["ssh.set_auth_key_from_file"]( 320 user, 321 source, 322 config=config, 323 saltenv=__env__, 324 fingerprint_hash_type=fingerprint_hash_type, 325 ) 326 else: 327 # Split keyline to get key and comment 328 keyline = keyline.split(" ") 329 key_type = keyline[0] 330 key_value = keyline[1] 331 key_comment = keyline[2] if len(keyline) > 2 else "" 332 data = __salt__["ssh.set_auth_key"]( 333 user, 334 key_value, 335 enc=key_type, 336 comment=key_comment, 337 options=options or [], 338 config=config, 339 fingerprint_hash_type=fingerprint_hash_type, 340 ) 341 else: 342 data = __salt__["ssh.set_auth_key"]( 343 user, 344 name, 345 enc=enc, 346 comment=comment, 347 options=options or [], 348 config=config, 349 fingerprint_hash_type=fingerprint_hash_type, 350 ) 351 352 if data == "replace": 353 ret["changes"][name] = "Updated" 354 ret["comment"] = "The authorized host key {} for user {} was updated".format( 355 name, user 356 ) 357 return ret 358 elif data == "no change": 359 ret[ 360 "comment" 361 ] = "The authorized host key {} is already present for user {}".format( 362 name, user 363 ) 364 elif data == "new": 365 ret["changes"][name] = "New" 366 ret["comment"] = "The authorized host key {} for user {} was added".format( 367 name, user 368 ) 369 elif data == "no key": 370 ret["result"] = False 371 ret["comment"] = "Failed to add the ssh key. Source file {} is missing".format( 372 source 373 ) 374 elif data == "fail": 375 ret["result"] = False 376 err = sys.modules[__salt__["test.ping"].__module__].__context__.pop( 377 "ssh_auth.error", None 378 ) 379 if err: 380 ret["comment"] = err 381 else: 382 ret["comment"] = ( 383 "Failed to add the ssh key. Is the home " 384 "directory available, and/or does the key file " 385 "exist?" 386 ) 387 elif data == "invalid" or data == "Invalid public key": 388 ret["result"] = False 389 ret[ 390 "comment" 391 ] = "Invalid public ssh key, most likely has spaces or invalid syntax" 392 393 return ret 394 395 396def absent( 397 name, 398 user, 399 enc="ssh-rsa", 400 comment="", 401 source="", 402 options=None, 403 config=".ssh/authorized_keys", 404 fingerprint_hash_type=None, 405): 406 """ 407 Verifies that the specified SSH key is absent 408 409 name 410 The SSH key to manage 411 412 user 413 The user who owns the SSH authorized keys file to modify 414 415 enc 416 Defines what type of key is being used; can be ed25519, ecdsa, ssh-rsa 417 or ssh-dss 418 419 comment 420 The comment to be placed with the SSH public key 421 422 options 423 The options passed to the key, pass a list object 424 425 source 426 The source file for the key(s). Can contain any number of public keys, 427 in standard "authorized_keys" format. If this is set, comment, enc and 428 options will be ignored. 429 430 .. versionadded:: 2015.8.0 431 432 config 433 The location of the authorized keys file relative to the user's home 434 directory, defaults to ".ssh/authorized_keys". Token expansion %u and 435 %h for username and home path supported. 436 437 fingerprint_hash_type 438 The public key fingerprint hash type that the public key fingerprint 439 was originally hashed with. This defaults to ``sha256`` if not specified. 440 441 .. versionadded:: 2016.11.7 442 """ 443 ret = {"name": name, "changes": {}, "result": True, "comment": ""} 444 445 if __opts__["test"]: 446 ret["result"], ret["comment"] = _absent_test( 447 user, 448 name, 449 enc, 450 comment, 451 options or [], 452 source, 453 config, 454 fingerprint_hash_type, 455 ) 456 return ret 457 458 # Extract Key from file if source is present 459 if source != "": 460 key = __salt__["cp.get_file_str"](source, saltenv=__env__) 461 filehasoptions = False 462 # check if this is of form {options} {enc} {key} {comment} 463 sshre = re.compile(r"^(ssh\-|ecds).*") 464 key = key.rstrip().split("\n") 465 for keyline in key: 466 filehasoptions = sshre.match(keyline) 467 if not filehasoptions: 468 ret["comment"] = __salt__["ssh.rm_auth_key_from_file"]( 469 user, 470 source, 471 config, 472 saltenv=__env__, 473 fingerprint_hash_type=fingerprint_hash_type, 474 ) 475 else: 476 # Split keyline to get key 477 keyline = keyline.split(" ") 478 ret["comment"] = __salt__["ssh.rm_auth_key"]( 479 user, 480 keyline[1], 481 config=config, 482 fingerprint_hash_type=fingerprint_hash_type, 483 ) 484 else: 485 # Get just the key 486 sshre = re.compile(r"^(.*?)\s?((?:ssh\-|ecds)[\w-]+\s.+)$") 487 fullkey = sshre.search(name) 488 # if it is {key} [comment] 489 if not fullkey: 490 key_and_comment = name.split(None, 1) 491 name = key_and_comment[0] 492 if len(key_and_comment) == 2: 493 comment = key_and_comment[1] 494 else: 495 # if there are options, set them 496 if fullkey.group(1): 497 options = fullkey.group(1).split(",") 498 # key is of format: {enc} {key} [comment] 499 comps = fullkey.group(2).split() 500 enc = comps[0] 501 name = comps[1] 502 if len(comps) == 3: 503 comment = comps[2] 504 ret["comment"] = __salt__["ssh.rm_auth_key"]( 505 user, name, config=config, fingerprint_hash_type=fingerprint_hash_type 506 ) 507 508 if ret["comment"] == "User authorized keys file not present": 509 ret["result"] = False 510 return ret 511 elif ret["comment"] == "Key removed": 512 ret["changes"][name] = "Removed" 513 514 return ret 515 516 517def manage( 518 name, 519 ssh_keys, 520 user, 521 enc="ssh-rsa", 522 comment="", 523 source="", 524 options=None, 525 config=".ssh/authorized_keys", 526 fingerprint_hash_type=None, 527 **kwargs 528): 529 """ 530 .. versionadded:: 3000 531 532 Ensures that only the specified ssh_keys are present for the specified user 533 534 ssh_keys 535 The SSH key to manage 536 537 user 538 The user who owns the SSH authorized keys file to modify 539 540 enc 541 Defines what type of key is being used; can be ed25519, ecdsa, ssh-rsa 542 or ssh-dss 543 544 comment 545 The comment to be placed with the SSH public key 546 547 source 548 The source file for the key(s). Can contain any number of public keys, 549 in standard "authorized_keys" format. If this is set, comment and enc 550 will be ignored. 551 552 .. note:: 553 The source file must contain keys in the format ``<enc> <key> 554 <comment>``. If you have generated a keypair using PuTTYgen, then you 555 will need to do the following to retrieve an OpenSSH-compatible public 556 key. 557 558 1. In PuTTYgen, click ``Load``, and select the *private* key file (not 559 the public key), and click ``Open``. 560 2. Copy the public key from the box labeled ``Public key for pasting 561 into OpenSSH authorized_keys file``. 562 3. Paste it into a new file. 563 564 options 565 The options passed to the keys, pass a list object 566 567 config 568 The location of the authorized keys file relative to the user's home 569 directory, defaults to ".ssh/authorized_keys". Token expansion %u and 570 %h for username and home path supported. 571 572 fingerprint_hash_type 573 The public key fingerprint hash type that the public key fingerprint 574 was originally hashed with. This defaults to ``sha256`` if not specified. 575 """ 576 ret = {"name": "", "changes": {}, "result": True, "comment": ""} 577 578 all_potential_keys = [] 579 for ssh_key in ssh_keys: 580 # gather list potential ssh keys for removal comparison 581 # options, enc, and comments could be in the mix 582 all_potential_keys.extend(ssh_key.split(" ")) 583 existing_keys = __salt__["ssh.auth_keys"](user=user).keys() 584 remove_keys = set(existing_keys).difference(all_potential_keys) 585 for remove_key in remove_keys: 586 if __opts__["test"]: 587 remove_comment = "{} Key set for removal".format(remove_key) 588 ret["comment"] = remove_comment 589 ret["result"] = None 590 else: 591 remove_comment = absent(remove_key, user)["comment"] 592 ret["changes"][remove_key] = remove_comment 593 594 for ssh_key in ssh_keys: 595 run_return = present( 596 ssh_key, 597 user, 598 enc, 599 comment, 600 source, 601 options, 602 config, 603 fingerprint_hash_type, 604 **kwargs 605 ) 606 if run_return["changes"]: 607 ret["changes"].update(run_return["changes"]) 608 else: 609 ret["comment"] += "\n" + run_return["comment"] 610 ret["comment"] = ret["comment"].strip() 611 612 if run_return["result"] is None: 613 ret["result"] = None 614 elif not run_return["result"]: 615 ret["result"] = False 616 617 return ret 618