Last updated on

Use Ansible to create user accounts and setup ssh keys


The Plan

We are going to use Ansible to create user accounts and add users to groups, setup them up with access via ssh using by adding their public keys to authorized_key files. For the minimum version of this task we are just going to do four things:

  • Create a list of user names, by setting a variable in the playbook
  • Create a user account for each user name, using Ansible’s with_item and the variable we created
  • Add each user’s ssh public key to the account, reading a file from the local filesystem
  • Modify /etc/sudoers so the users can use sudo without entering a password

The guide has been tested using a new Digital Ocean Ubuntu 23.10 x64 Droplet on the cheapest plan, and everything runs as root when connected to the server via ssh or console (Such as with Digital Ocean’s Console option on the control panel)

Getting started

For this guide we are going to setup the playbook to run a server directly, using the “local” connection method so when run as root we don’t need to worry about additional authentication or setting up host inventories.

Install Ansible

To get Ansible installed you can just run apt-get install ansible which will install version 2.14.9 today. Or check out the Ansible documentation if you want to get the latest version.

Create and run your first playbook

To check everything is working as it should, it’s best to run a barebones playbook with just a ping task which will check your setup using the simplest version of a playbook possible.

Create a file called users.yml with the following snippet, and run it with ansible-playbook users.yml

---
- hosts: "localhost"
  connection: "local"
  tasks:
    - name: "Ping?"
      ping:

Don’t worry about the [WARNING]: provided hosts list is empty, only localhost is available message, we are only working with localhost so this is to be expected.

Adding a list of users to the playbook vars

At the top of the playbook, we are going to add a simple list of usernames into the vars available later in the playbook.

vars:
  users:
    - paul
    - tanya
    - ruby

Full users.yml

---
- hosts: "localhost"
  connection: "local"
  vars:
    users:
      - paul
      - tanya
      - ruby
  tasks:
    - name: "Ping?"
      ping:

Creating User accounts

Now we have a list of usernames in a variable, we can use that to create user accounts.

In its simplest form the Ansible User Module just needs to be given a name, and we can use the with_items to apply our list to the module in a loop.

When using with_items the value becomes available as item, which you can palce as a string using "{{ item }}" so that the task is run for each item in the list individually.

So our users are more useful, we are also add them to groups with a simple comma seperated list, in this case admin and www-data will be added to each user.

user task

tasks:
  - name: "Create user accounts and add users to groups"
    user:
      name: "{{ item }}"
      groups: "admin,www-data"
    with_items: "{{ users }}"

Full file

---
- hosts: "localhost"
  connection: "local"
  vars:
    users:
      - paul
      - tanya
      - ruby
  tasks:
    - name: "Create user accounts and add users to groups"
      user:
        name: "{{ item }}"
        groups: "admin,www-data"
      with_items: "{{ users }}"

Automate adding ssh keys to user accounts

The newly created user accounts on a server don’t have passwords set, so to be able to log in we need to add each users ssh key to their authorize_keys file. We can do this using Ansible’s Authorized Key Module authorized_key that takes user and a file in key.

key takes a file, which can be loaded using the lookup('file','path to file') function. In this code, we put the public SSH keys in files/username.key.pub. By having the file names match to the username we can use the same users var for the loop without needing to add additional parameters at this stage.

For this example, we are just going to pass some dummy data that are real enough to pass Ansible’s validations.

mkdir files
echo "ssh-ed25519 Z [email protected]" > files/paul.key.pub
echo "ssh-ed25519 Z [email protected]" > files/tanya.key.pub
echo "ssh-ed25519 Z [email protected]" > files/ruby.key.pub

authorized_key task

- name: "Add authorized keys"
  authorized_key:
    user: "{{ item }}"
    key: "{{ lookup('file', 'files/'+ item + '.key.pub') }}"
  with_items: "{{ users }}"

Dir contents

.
├── files
    ├── paul.key.pub
    ├── ruby.key.pub
    └── tanya.key.pub
├── users.yml

Full users.yml

---
- hosts: "localhost"
  connection: "local"
  vars:
    users:
      - "paul"
      - "tanya"
      - "ruby"
  tasks:
    - name: "Create user accounts"
      user:
        name: "{{ item }}"
        groups: "admin,www-data"
      with_items: "{{ users }}"
    - name: "Add authorized keys"
      authorized_key:
        user: "{{ item }}"
        key: "{{ lookup('file', 'files/'+ item + '.key.pub') }}"
      with_items: "{{ users }}"

Keep in mind whenever you are passing around SSH keys, either for a simple bit of automation like this, or any 3rd party you always use the public key which ends .pub, and never the private key. The private key is for the user only, and should never be shared. The public key can be shared with anyone, and is used to verify the user is who they say they are and there is no security risk in sharing it or publishing it.

Use lineinfile to update /etc/sudoers for passwordless sudo

Now your users can login with their ssh keys, but won’t be able to do any server admin with sudo because without passwords set, they can’t enter their password when prompted when they use the command as per the default behaviour. To get around this limitation, we can update /etc/sudoers with Ansible’s lineinfile Module.

This simple implementation of the lineinfile looks for a line starting with – represented in a regexp as ^ – with the string %admin and then ensures it matches the line %admin ALL=(ALL) NOPASSWD: ALL

Once in place, any users in the admin group will no longer be prompted for a password when using sudo.

lineinfile task

- name: "Allow admin users to sudo without a password"
  lineinfile:
    dest: "/etc/sudoers" # path: in version 2.3
    state: "present"
    regexp: "^%admin"
    line: "%admin ALL=(ALL) NOPASSWD: ALL"

Full users.yml

---
- hosts: "localhost"
  connection: "local"
  vars:
    users:
      - "paul"
      - "tanya"
      - "ruby"
  tasks:
    - name: "Create user accounts"
      user:
        name: "{{ item }}"
        groups: "admin,www-data"
      with_items: "{{ users }}"
    - name: "Add authorized keys"
      authorized_key:
        user: "{{ item }}"
        key: "{{ lookup('file', 'files/'+ item + '.key.pub') }}"
      with_items: "{{ users }}"
    - name: "Allow admin users to sudo without a password"
      lineinfile:
        dest: "/etc/sudoers" # path: in version 2.3
        state: "present"
        regexp: "^%admin"
        line: "%admin ALL=(ALL) NOPASSWD: ALL"

Next Steps: Creating a Viable Version

The next part of this guide steps up to the Viable version, by defining expanding the vars to have multiple properties per item using complex vars to add groups per user, using user state for a method to disable users accounts. The improved playbook also introduces handlers and notify to restart services when the configuration changes. Improve the user management playbook in the next guide.