Better handling of public SSH keys using Ansible..

Ansible has a dedicated module to manage public keys; the authorized_key module. It’s a very nice module, with enough flexibility to do almost anything I can think of.

However, it does have one very annoying thing. While I was migrating our automation scripts to ansible; I got to the point where I was working on the script that provisions our users. By default, we disabled all password authentication and root SSH access. Only key based access is allowed.

I found that I have to actually put the public SSH key strings inside the playbook vars. That’s just not cool. SSH keys are long, they might have specific options (although the authorized_key module allows you to configure that) and it’s harder to maintain the list of keys like this. So, I tried to work around this. My target was to add the public SSH keys for my users as static files in an ansible role. Basically, I will be populating my my group_vars files by reading files inside my roles.

  • First, I added the public key files in the ‘files‘ directory of the role I was using to configure the users.
  • Now, I have to find a way to “read” the key files and set them in the vars file. Fortunately, ansible provides Lookup plugins that allows me to do just that!
  • So, the related part of the vars file should look like this:


ssh_users:
  - name: user1
    key: "{{ lookup('file', 'user1.pub') }}"
  - name: user2
    key: "{{ lookup('file', 'user2.pub') }}"
  - name: user3
    key: "{{ lookup('file', 'user3.pub') }}"
  - name: user4
    key: "{{ lookup('file', 'user4.pub') }}"

  • Next, all we need to do is call the authorized_key module as usual

- name: Add ssh user keys
  authorized_key: user={{ item.name }} key="{{ item.key }}"
  with_items: ssh_users

Edit: Updated the variable name to avoid the deprecated syntax. Details in the first comment.

Here you go. Key files are neatly tucked in the files directory, easy to maintain and no wrapped lines and cluttered options missing up your var files.

17 Comments

  1. Quick public service announcement note that “${foo} and $foo” are legacy variable syntax, so you can just say “with_items: ssh_users”. On the development branch (1.6) these deprecated constructs are finally removed. That is all :)

    1. Thanks, Michael, for the update! I will make sure to update my playbooks and the post to avoid this. We don’t want those to break when we upgrade :)

      Great job done on Ansible, I love it :)

  2. authorized_key has a with_file option which seems to achieve the same result:


    - name: SSH key
    authorized_key: user=gerhard key="{{ item }}"
    with_file:
    - demi.pub

    1. Yes, it does have the with_files. However, it helps only when you are adding multiple public keys for the same user.

      I am trying to add multiple users, with a key per user. I couldn’t do that with with_files alone.

      1. I was able to take the above guide, and combine it with the with_fileglob and file lookup to fill a couple folders with allowed, and denied pubkeys:

        tasks:
        – name: Manage Authorized Keys – Allow
        authorized_key: user=targetUser key=”{{ lookup(‘file’, item) }}” state=present
        with_fileglob: pubkeys/allow/*
        – name: Manage Authorized Keys – Deny
        authorized_key: user= targetUser key=”{{ lookup(‘file’, item) }}” state=absent
        with_fileglob: pubkeys/deny/*

  3. Thanks for this, a great example of how slick ansible is compared to puppet – IMHO, and this example fits my needs perfectly.

    In this line – user={{ item.name }} key=”{{ item.key }}”
    Why is it that you need double quotes for item.key but not item.name?

    1. That’s actually a good question! I put the double quotes because that’s how it is in the documentation. Here is the example from the documentation:

      # Using with_file
      - name: Set up authorized_keys for the deploy user
      authorized_key: user=deploy
      key="{{ item }}"
      with_file:
      - public_keys/doe-jane
      - public_keys/doe-john

      1. It needs to be quoted because those variables potentially could have an equal sign in their contents (ssh keys usually do) which Ansible could then treat as a variable assignment. By quoting them you tell Anslible to treat these variables as string literals. To do otherwise potentially opens up a vector for data execution. The latest versions of Ansible try to detect for this condition and warn on it.

  4. I’ve been struggling (and almost gave up) on how to achieve those same results: many users, each with a local key file.

    As a side notice, you can also specify multiple keys per user by separating keys with a newline:


    ssh_users:
    - name: user1
    key: "{{ lookup('file', 'user1.pub') }}
    {{ lookup('file', 'user1b.pub') }}"
    - name: user2
    key: "{{ lookup('file', 'user2.pub') }}"

    1. Arrgggh! You need to add (at least) an extra newline between keys:

      ssh_users:
      – name: user1
      key: “{{ lookup(‘file’, ‘user1.pub’) }}

      {{ lookup(‘file’, ‘user1b.pub’) }}”
      – name: user2
      key: “{{ lookup(‘file’, ‘user2.pub’) }}”

  5. I wrote a simple task to test ssh module

    – name: Add ssh user keys
    authorized_key:
    user: dheeraj
    key: “{{ lookup(‘file’, ‘dheeraj.pub’) }}”

    when I run the playbook I get the following error

    fatal: [10.253.73.51]: FAILED! => {“changed”: false, “failed”: true, “msg”: “Failed to lookup user dheeru: ‘getpwnam(): name not found: dheeru'”}

    What would be the issue?

    1. Sorry the error was
      “msg”: “Failed to lookup user dheeru: ‘getpwnam(): name not found: dheeraj’

  6. Just a note for other newbs like me. It is probably related to my version of ansible (2.3.1.0). I had to modify the with_items as follows; otherwise it bombs out saying that I have undefined variables:

    from this:
    with_items: ssh_users

    to this:
    with_items: “{{ ssh_users }}”

  7. Hello,

    Could you please let me know what is missing in below code?

    Requirement/End Result: I need to create multiple users on destination servers along with their .ssh directory and push their authorized_keys & authorized_keys2 files to different destinations for each user.

    # Add users & keys to destination servers

    – hosts: lb:app2
    tasks:
    – name: Add list of users
    # tags: system-user
    user:
    name: “{{ item.name }}”
    uid: “{{ item.uid }}”
    groups: “{{ item.groups }}”
    comment: “{{ item.comment }}”
    password: ” {{ item.password }}”
    createhome: yes
    state: present
    with_items:
    – { name: testuser1, uid: 1002, groups: “wheel, automate”, comment: “{{ ‘Test Admin ID’ }}”, password: “{{ ‘$6$wsix5/A0$Qs46riLAIqJfolLAzqrMc8ZVVN8tBSZWaoDKco9gnqQJJqvf1hA3K9HHM8HtJXzcpA/ZnvagCPmiXsxl4ifzn.’ }}” }
    – { name: testuser2, uid: 1003, groups: “automate”, comment: “{{ ‘Test2 Admin ID’ }}”, password: “{{ ‘$6$gs3s6NUC$EwG7Lys4yxSLW8d1bceC1y4JH/ag0wmJt/AKnMg2DNHTy/HMfMYJV06SUyD89ZNioh2IfVmC14bbqFWWpfC9E/’ }}” }
    – name: Add .ssh directories
    file:
    name: “{{ item.name }}”
    path: “/home/{{ item.name}}/.ssh”
    state: directory
    mode: 0700
    owner: “{{ item.name }}”
    group: “{{ item.group|default(item.name) }}”
    with_items:
    – { name: testuser1, path: “{{ item.name }}” }

    Terminal Output:

    ansible-playbook -v add_users.yml
    Using /etc/ansible/ansible.cfg as config file

    PLAY [lb:app2] ************************************************************************************************************************************************************************************************

    TASK [Gathering Facts] ****************************************************************************************************************************************************************************************
    ok: [app2]
    ok: [lb1]

    TASK [Add list of users] **************************************************************************************************************************************************************************************
    changed: [app2] => (item={u’comment’: u’Test Admin ID’, u’password’: u’$6$wsix5/A0$Qs46riLAIqJfolLAzqrMc8ZVVN8tBSZWaoDKco9gnqQJJqvf1hA3K9HHM8HtJXzcpA/ZnvagCPmiXsxl4ifzn.’, u’name’: u’testuser1′, u’groups’: u’wheel, automate’, u’uid’: 1002}) => {“changed”: true, “comment”: “Test Admin ID”, “createhome”: true, “group”: 1002, “groups”: “wheel, automate”, “home”: “/home/testuser1”, “item”: {“comment”: “Test Admin ID”, “groups”: “wheel, automate”, “name”: “testuser1”, “password”: “$6$wsix5/A0$Qs46riLAIqJfolLAzqrMc8ZVVN8tBSZWaoDKco9gnqQJJqvf1hA3K9HHM8HtJXzcpA/ZnvagCPmiXsxl4ifzn.”, “uid”: 1002}, “name”: “testuser1”, “password”: “NOT_LOGGING_PASSWORD”, “shell”: “/bin/bash”, “state”: “present”, “system”: false, “uid”: 1002}
    changed: [lb1] => (item={u’comment’: u’Test Admin ID’, u’password’: u’$6$wsix5/A0$Qs46riLAIqJfolLAzqrMc8ZVVN8tBSZWaoDKco9gnqQJJqvf1hA3K9HHM8HtJXzcpA/ZnvagCPmiXsxl4ifzn.’, u’name’: u’testuser1′, u’groups’: u’wheel, automate’, u’uid’: 1002}) => {“changed”: true, “comment”: “Test Admin ID”, “createhome”: true, “group”: 1002, “groups”: “wheel, automate”, “home”: “/home/testuser1”, “item”: {“comment”: “Test Admin ID”, “groups”: “wheel, automate”, “name”: “testuser1”, “password”: “$6$wsix5/A0$Qs46riLAIqJfolLAzqrMc8ZVVN8tBSZWaoDKco9gnqQJJqvf1hA3K9HHM8HtJXzcpA/ZnvagCPmiXsxl4ifzn.”, “uid”: 1002}, “name”: “testuser1”, “password”: “NOT_LOGGING_PASSWORD”, “shell”: “/bin/bash”, “state”: “present”, “system”: false, “uid”: 1002}
    changed: [lb1] => (item={u’comment’: u’Test2 Admin ID’, u’password’: u’$6$gs3s6NUC$EwG7Lys4yxSLW8d1bceC1y4JH/ag0wmJt/AKnMg2DNHTy/HMfMYJV06SUyD89ZNioh2IfVmC14bbqFWWpfC9E/’, u’name’: u’testuser2′, u’groups’: u’automate’, u’uid’: 1003}) => {“changed”: true, “comment”: “Test2 Admin ID”, “createhome”: true, “group”: 1003, “groups”: “automate”, “home”: “/home/testuser2”, “item”: {“comment”: “Test2 Admin ID”, “groups”: “automate”, “name”: “testuser2”, “password”: “$6$gs3s6NUC$EwG7Lys4yxSLW8d1bceC1y4JH/ag0wmJt/AKnMg2DNHTy/HMfMYJV06SUyD89ZNioh2IfVmC14bbqFWWpfC9E/”, “uid”: 1003}, “name”: “testuser2”, “password”: “NOT_LOGGING_PASSWORD”, “shell”: “/bin/bash”, “state”: “present”, “system”: false, “uid”: 1003}
    changed: [app2] => (item={u’comment’: u’Test2 Admin ID’, u’password’: u’$6$gs3s6NUC$EwG7Lys4yxSLW8d1bceC1y4JH/ag0wmJt/AKnMg2DNHTy/HMfMYJV06SUyD89ZNioh2IfVmC14bbqFWWpfC9E/’, u’name’: u’testuser2′, u’groups’: u’automate’, u’uid’: 1003}) => {“changed”: true, “comment”: “Test2 Admin ID”, “createhome”: true, “group”: 1003, “groups”: “automate”, “home”: “/home/testuser2”, “item”: {“comment”: “Test2 Admin ID”, “groups”: “automate”, “name”: “testuser2”, “password”: “$6$gs3s6NUC$EwG7Lys4yxSLW8d1bceC1y4JH/ag0wmJt/AKnMg2DNHTy/HMfMYJV06SUyD89ZNioh2IfVmC14bbqFWWpfC9E/”, “uid”: 1003}, “name”: “testuser2”, “password”: “NOT_LOGGING_PASSWORD”, “shell”: “/bin/bash”, “state”: “present”, “system”: false, “uid”: 1003}

    TASK [Add .ssh directories] ***********************************************************************************************************************************************************************************
    fatal: [lb1]: FAILED! => {“msg”: “‘item’ is undefined”}
    fatal: [app2]: FAILED! => {“msg”: “‘item’ is undefined”}

    PLAY RECAP ****************************************************************************************************************************************************************************************************
    app2 : ok=2 changed=1 unreachable=0 failed=1
    lb1 : ok=2 changed=1 unreachable=0 failed=1

Comments are closed.