Managing Linux Users & SSH keys using Ansible

Today I was assigned a task to create user accounts on an EC2 instance (Ubuntu) and also add SSH public keys to the respective user account’s authorized key list. The EC2 instance would act as a gateway to access the internal network. (This is a basic setup in which the user creates an SSH tunnel to access resources on the internal network. It’s not a foolproof security solution but controls external access to some extent)

After getting the gist of the problem statement one could go right away, get the SSH public keys of users and create new user accounts on a new/existing instance. This solution seems working (yes, it does work) but in the long run, managing multiple user accounts and their SSH keys would be complicated and error prone.

Let’s deeply analyze the problem statement:

  • Whenever a new user needs access to the internal network, a Linux user account must be created and his/her SSH public key should be added to the authorized keys list
  • If the user doesn’t require access to the internal network anymore, the user account should be revoked immediately
  • No data is stored on the EC2 instance as it is only used for SSH tunneling.

The problem statement involves less manual effort (limited to getting the usernames & SSH keys and maintaining the list of users who are allowed to access the resources). Solution to this problem statement could be automated.

I felt that instead of deleting specific user accounts after explicit revocation (which involves maintaining a list of revoked usernames), it’s a good idea to delete all users and re-add only the required users. This would avoid the blunder of not revoking the access of an old user who is now unauthorized to access the instance.

Ansible script to solve the above problem would look like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
---
- hosts: production
  gather_facts: no       # This is helpful if a new EC2 instance is to be provisioned
  become: yes

  vars:
  - default_users: ['nobody']
  - required_users: ['badshah', 'bob', 'alice']

  tasks:
  - name: Check python
    raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)
    changed_when: false

  - name: Get list of all users
    shell: "getent passwd | awk -F: '$3 > 1000 {print $1}'"
    changed_when: false
    register: users

  - name: Remove all users
    user:
      name: "{{ item }}"
      state: absent
      remove: yes
    with_items: "{{ users.stdout_lines }}"
    when: item not in default_users

  - name: Add required users
    user:
      name: "{{ item }}"
      state: present
    with_items: "{{ required_users }}"

  - name: Add SSH public keys
    authorized_key:
      user: "{{ item }}"
      state: present
      key: "{{ lookup('file', 'keys/{{ item }}') }}"
    with_items: "{{ required_users }}"

If one is provisioning a new EC2 Ubuntu 18.04 Linux instance, python is not installed by default. That’s why the first task checks for python and installs a minimal version of python if it isn’t installed (test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)). Ansible gathers facts about the remote system by default. If python isn’t installed, it runs into error even before starting the first task. So we disable it using gather_facts: no.

Next step is to remove all the Linux users. But this is not a straightforward step as Linux machines have multiple users by default to handle multiple background tasks. One can use the command getent passwd | awk -F: '$3 > 1000 {print $1}', to get all the Linux users with UID greater than 1000 i.e. all the local and remote users of the machine. (The user nobody is a pseudo user and has least permissions on the system. Lets add it to a whitelist called default_users and lets not touch it). Then delete all the Linux user accounts returned from the above command only if it isn’t explicitly mentioned in the default_users array.

Now we have deleted all the user accounts. The remaining task is to create the required users and add their SSH public keys to authorized list. Adding new users and gathering their SSH public keys is the only manual step. Ansible has modules like user and authorized_key which allows managing user accounts and authorized SSH keys respectively. Let’s create a list called required_users which would contain the names of the user accounts to be present.

After a user account is created using the user module, SSH public key could be added from a file. In our case, as there are multiple user accounts, its better to maintain each SSH key in a separate file. {{ lookup(‘file’, ‘keys/{{ item }}’) }} tells Ansible to get the SSH key from a file saved with the same name as the user, in the “keys” directory.

If we need to add a new user, we just have to append the name in required_users and add the SSH key under the keys directory with the same name. If you want to remove the user, just delete the name from the required_users list and the Ansible script will remove the user for you.


Also thanks to Stack Overflow : https://stackoverflow.com/questions/37441796/ansible-for-user-management-removing-dead-accounts