├── README.md ├── create_users_example.yml ├── generate_user_passwords.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # Securely create multiple users with Ansible 2 | 3 | This Python script and Ansible playbook help you to create several user accounts on different servers, give each user a secure password and force the user to change the password on first login. 4 | 5 | The Python script `generate_user_passwords.py` creates a YAML dictionary of user names and SHA512-hashed passwords that can be used in an Ansible Playbook. 6 | 7 | The Ansible playbook `users.yml` demonstrates how to use the generated user dictionary. 8 | 9 | ## Installation 10 | 11 | To use this script you need to install [Ansible](http://ansible.com/) and the `passlib` Python library. 12 | 13 | The easiest way to install Ansible is by using the package manager of your operating system. Installation instructions for the package managers or installing from source are at http://docs.ansible.com/ansible/intro_installation.html 14 | 15 | The Passlib library can be installed with 16 | 17 | pip install passlib 18 | 19 | ## Generate a list of user names and passwords 20 | Use the provided Python script `generate_user_passwords.py` to generate a dictionary of user names and secure passwords. There are several ways to specify the user names. 21 | 22 | ### User names as parameters 23 | 24 | python generate_user_passwords.py alice bob carol dan 25 | 26 | ### User names from stdin 27 | User names can be piped in from a file 28 | 29 | python generate_user_passwords.py < users.txt 30 | 31 | or generated with a shell command 32 | 33 | awk -F ';' '{print $3}' users.csv | python generate_user_passwords.py 34 | 35 | When the user names are piped in, the script expects each name to be a on a new line. Blank lines are ignored. 36 | 37 | ### Using your own passwords 38 | If you want to specify the passwords yourself, you can use the pattern `username:password` for each user name. The user name will be split at the first `:` character and the second half of this user name will be used as the password. 39 | 40 | In the following example `alice` has the password `foo`, `bob` has the password `abc:xyz` 41 | 42 | python generate_user_passwords.py alice:foo bob:abc:xyz 43 | 44 | ### Generated password length 45 | 46 | You can specify the length of the generated passwords like this: 47 | 48 | python generate_user_passwords.py -l 8 alice bob carol dan 49 | 50 | Default password length is 12. 51 | 52 | ### Storing the output 53 | By default the generated YAML is printed to stdout. The generated output contains the un-hashed generated passwords as comments so you can pass them on to the users. 54 | 55 | Example output: 56 | 57 | ```YAML 58 | --- 59 | user_passwords: 60 | alice: $6$rounds=656000$m/qpgaPV9nDhZA84$0Uz2fQ7PjnX.eMIDSlw0hUetHYat.VuxIzBNsbceZjg60XMe.0hrDekRybNAMe0fPqvczikY0Hdph8KMhcHct. # ws#P)Bg)l853 61 | bob: $6$rounds=656000$RhhaEkZK/60KAYDf$U/nsycrW2A4SAuhBbAW4na4OLunPrUfR31OU3ThY1ge3vc.RUfhyHTg5dShkTYFGB/455lv0vOWDAmbGiOI730 # qbbw8&OeZ1ql 62 | carol: $6$rounds=656000$aXLv86ermeammjFO$MooGjguTxUjhc2m6OefDddz0mszG/SprKiyTsND0lpT3f4.R7V5KucdK9JdLluOF.WnpGAz/GKy2umf5TPkPr. # zIPjxwCFm@ES 63 | ``` 64 | 65 | You can store the output in a file like this: 66 | 67 | python generate_user_passwords.py alice bob carol > user_passwords.yml 68 | 69 | ### Securing the output file 70 | 71 | The point of the Python script and the playbook is to generate a username and password file that is **only temporary, located on your deployment machine!** If you need to put the generated file into version control or store it for longer periods of time, you should encrypt the file with the [`ansible-vault`](http://docs.ansible.com/ansible/playbooks_vault.html) command: 72 | 73 | python generate_user_passwords.py alice bob carol | ansible-vault encrypt > user_passwords.yml 74 | 75 | To display the file again on the command line, use the command 76 | 77 | ansible-vault view 78 | -------------------------------------------------------------------------------- /create_users_example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Example playbook that shows how to create users from the generated ansible variable file 3 | - hosts: all 4 | tasks: 5 | - name: Load passwords from vault 6 | include_vars: user_passwords.yml 7 | 8 | - name: Create users 9 | user: name="{{item.key}}" password="{{item.value}}" shell="/bin/bash" update_password=on_create 10 | with_dict: "{{ user_passwords }}" 11 | register: user_results 12 | 13 | - name: Force password renewal for newly created users 14 | command: chage -d 0 {{item.item.key}} # item.item is the key/value pair from the dict in the previous task 15 | when: "{{item.changed == true}}" 16 | with_items: "{{ user_results.results }}" 17 | -------------------------------------------------------------------------------- /generate_user_passwords.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a script to generate a YAML dictionary of 3 | user names and hashed passwords for use in an Ansible playbook. 4 | 5 | @author Gabriel Birke 6 | """ 7 | 8 | import random, string, sys, getopt 9 | from passlib.hash import sha512_crypt 10 | 11 | YAML_PARENT_VARIABLE = 'user_passwords' 12 | DEFAULT_PASSWORD_LENGTH = 12 13 | # Unambigous chars 14 | ALLOWED_PASSWORD_CHARS = 'abcdefghkmnoprstwxzABCDEFGHJKLMNPQRTWXY3468' 15 | # More secure chars, but harder to type 16 | # ALLOWED_PASSWORD_CHARS = string.ascii_letters + string.digits + '!@$%^&*+=/' 17 | 18 | def generate_pw( length ): 19 | rnd = random.SystemRandom() 20 | return ''.join( rnd.choice(ALLOWED_PASSWORD_CHARS) for i in range(length) ) 21 | 22 | def get_username_and_password( username_str, password_length ): 23 | if ":" in username_str: 24 | username, password = username_str.split(":", 1) 25 | else: 26 | username = username_str 27 | password = generate_pw( password_length ) 28 | return (username, password) 29 | 30 | def get_users_from_stdin(): 31 | users = [] 32 | # TODO check if we have stdin input, otherwise return immediately 33 | for line in sys.stdin: 34 | if line.strip(): 35 | users.append( line.strip() ) 36 | return users 37 | 38 | def usage(): 39 | print("Usage: {} [-l ] USER1[:PASSWORD1] USER2[:PASSWORD2] USER3[:PASSWORD3] ...".format( sys.argv[0] ) ) 40 | print " User names can also be passed via stdin (each username:password on a separate line)" 41 | 42 | 43 | def generate_yaml( usernames, password_length ): 44 | yml = "---\n{}:\n".format( YAML_PARENT_VARIABLE ) 45 | for user in usernames: 46 | username, plaintext_pw = get_username_and_password( user, password_length ) 47 | password_hash = sha512_crypt.encrypt( plaintext_pw ) 48 | yml += " {}: {} # {}\n".format( username, password_hash, plaintext_pw ) 49 | return yml 50 | 51 | def main(argv): 52 | try: 53 | opts, usernames = getopt.getopt( argv, "hl:", ["help", "length="] ) 54 | except getopt.GetoptError: 55 | usage() 56 | sys.exit(2) 57 | 58 | # usernames += get_users_from_stdin() 59 | if len( usernames ) < 1: 60 | print( "You must at least give one username" ) 61 | usage() 62 | sys.exit(2) 63 | 64 | password_length = DEFAULT_PASSWORD_LENGTH 65 | for opt, arg in opts: 66 | if (opt == '-h' or opt == '--help'): 67 | print( "Generate YAML file with encrypted passwords") 68 | usage() 69 | sys.exit(0) 70 | if (opt == '-l' or opt == '--length'): 71 | password_length = int(arg) 72 | print generate_yaml( usernames, password_length ) 73 | 74 | if __name__ == '__main__' : 75 | main(sys.argv[1:]) 76 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | passlib 2 | --------------------------------------------------------------------------------