Syncing SSH keys on Cisco IOS XR with a custom Ansible module
Vincent Bernat
The cisco.iosxr
collection from Ansible Galaxy
provides an iosxr_user
module to manage local users,
along with their SSH keys. However, the module is quite slow, do not
display a diff for changed SSH keys, never signal change when a key is
modified, and does not delete obsolete keys. Let’s write a custom
Ansible module managing only the SSH keys while fixing these issues.
Notice
I recommend that you read “Writing a custom Ansible module” as an introduction.
How to add an SSH key to a user#
Adding SSH keys to users in Cisco IOS XR is quite undocumented. First, you need to encode the key with the “ssh-rsa” key ASN.1 format, like an OpenSSH public key, but without the base64-encoding:
$ awk '{print $2}' id_rsa.pub \ > | base64 -d \ > > publickey_vincent.raw
Then, you upload the key with SCP to harddisk:/publickey_vincent.raw
and import it for the current user with the following IOS command:
crypto key import authentication rsa harddisk:/publickey_vincent.b64
However, if you want to import a key for another user, you need to be
part of the root-system
group:
username vincent group root-lr group root-system
With the following admin command, you can attach a key to another user:
admin crypto key import authentication rsa username cedric harddisk:/publickey_cedric.b64
Code#
The module has the following signature and it installs the specified key for each user and remove keys from retired users—the ones we do not specify.
iosxr_users: keys: vincent: ssh-rsa AAAAB3NzaC1yc2EAA[…]ymh+YrVWLZMJR cedric: ssh-rsa AAAAB3NzaC1yc2EAA[…]RShPA8w/8eC0n
Prerequisites#
Unlike the iosxr_user
module, our custom module only handles SSH
keys, one per user. Therefore, the user definitions have to already
exist in the running configuration.1 Moreover, the user defined
in ansible_user
needs to be in the root-system
group. The
cisco.iosxr
collection must also be installed as the module relies
on its code.
When running the module, ansible_connection
needs to be set to
network_cli
and ansible_network_os
to iosxr
. These variables are
usually defined in the inventory.
Module definition#
Starting from the skeleton described in the previous article, we define the module:
module_args = dict( keys=dict(type='dict', elements='str', required=True), ) module = AnsibleModule( argument_spec=module_args, supports_check_mode=True ) result = dict( changed=False )
Getting the installed keys#
The next step is to retrieve the keys currently installed. This can be done with the following command:
# show crypto key authentication rsa all Key label: vincent Type : RSA public key authentication Size : 2048 Imported : 16:17:08 UTC Tue Aug 11 2020 Data : 30820122 300D0609 2A864886 F70D0101 01050003 82010F00 3082010A 02820101 00D81E5B A73D82F3 77B1E4B5 949FB245 60FB9167 7CD03AB7 ADDE7AFE A0B83174 A33EC0E6 1C887E02 2338367A 8A1DB0CE 0C3FBC51 15723AEB 07F301A4 B1A9961A 2D00DBBD 2ABFC831 B0B25932 05B3BC30 B9514EA1 3DC22CBD DDCA6F02 026DBBB6 EE3CFADA AFA86F52 CAE7620D 17C3582B 4422D24F D68698A5 52ED1E9E 8E41F062 7DE81015 F33AD486 C14D0BB1 68C65259 F9FD8A37 8DE52ED0 7B36E005 8C58516B 7EA6C29A EEE0833B 42714618 50B3FFAC 15DBE3EF 8DA5D337 68DAECB9 904DE520 2D627CEA 67E6434F E974CF6D 952AB2AB F074FBA3 3FB9B9CC A0CD0ADC 6E0CDB2A 6A1CFEBA E97AF5A9 1FE41F6C 92E1F522 673E1A5F 69C68E11 4A13C0F3 0FFC782D 27020301 0001 […]
ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.iosxr
contains a run_commands()
function we can use:
command = "show crypto key authentication rsa all" out = run_commands(module, command) out = out[0].replace(' \n', '\n')
A common library to parse a command output is textfsm: a Python module using a template-based state machine for parsing semi-formatted text.
template = r""" Value Required Label (\w+) Value Required,List Data ([A-F0-9 ]+) Start ^Key label: ${Label} ^Data\s+: -> GetData GetData ^ ${Data} ^$$ -> Record Start """.lstrip() re_table = textfsm.TextFSM(io.StringIO(template)) got = {data[0]: "".join(data[1]).replace(' ', '') for data in re_table.ParseText(out)}
got
is a dictionary associating key labels, considered as usernames,
with a hexadecimal representation of the public key currently
installed. It looks like this:
>>> pprint(got) {'alfred': '30820122300D0609[…]6F0203010001', 'cedric': '30820122300D0609[…]710203010001', 'vincent': '30820122300D0609[…]270203010001'}
Comparing with the wanted keys#
Let’s now build the wanted
dictionary using the same structure. In
module.params['keys']
, we have a dictionary associating usernames
to public SSH keys in the OpenSSH format:
>>> pprint(module.params['keys']) {'cedric': 'ssh-rsa AAAAB3NzaC1yc2[…]', 'vincent': 'ssh-rsa AAAAB3NzaC1yc2[…]'}
We need to convert these keys in the same hexadecimal representation
used by Cisco above. The ssh-keygen
command and some glue can do the
conversion:2
$ ssh-keygen -f id_rsa.pub -e -mPKCS8 \ > | grep -v '^---' \ > | base64 -d \ > | hexdump -e '4/1 "%0.2X"' 30820122300D06092[…]782D270203010001
Assuming we have a ssh2cisco()
function doing that, we can build the
wanted
dictionary:
wanted = {k: ssh2cisco(v) for k, v in module.params['keys'].items()}
Applying changes#
Back to the skeleton described in the previous article, the last step is to apply the changes if there
is a difference between got
and wanted
when not running with check
mode. The part comparing got
and wanted
is taken verbatim from the
skeleton module:
if got != wanted: result['changed'] = True result['diff'] = dict( before=yaml.safe_dump(got), after=yaml.safe_dump(wanted) ) if module.check_mode or not result['changed']: module.exit_json(**result)
Let’s copy the new or changed keys and attach them to their respective
users. For this purpose, we reuse the get_connection()
and
copy_file()
functions from
ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.iosxr
.
conn = get_connection(module) for user in wanted: if user not in got or wanted[user] != got[user]: dst = f"/harddisk:/publickey_{user}.raw" with tempfile.NamedTemporaryFile() as src: decoded = base64.b64decode( module.params['keys'][user].split()[1]) src.write(decoded) src.flush() copy_file(module, src.name, dst) command = ("admin crypto key import authentication rsa " f"username {user} {dst}") conn.send_command(command, prompt="yes/no", answer="yes")
Then, we remove obsolete keys:
for user in got: if user not in wanted: command = ("admin crypto key zeroize authentication rsa " f"username {user}") conn.send_command(command, prompt="yes/no", answer="yes")
The complete code is available on GitHub. Compared to the
iosxr_user
module, this one displays a diff when
running with --diff
, correctly signals a change, is faster,
3 and deletes unwanted SSH keys. However, it is unable to
create users and cannot configure passwords or multiple SSH keys.
-
In our environment, the Ansible playbook pushes a full configuration, including the user definitions. Then, it synchronizes the SSH keys. ↩︎
-
Despite the argument provided to
ssh-keygen
, the format used by Cisco is not PKCS#8. This is the ASN.1 representation of a Subject Public Key Info structure, as defined in RFC 2459. Moreover, PKCS#8 is a format for a private key, not a public one. ↩︎ -
The main factors for being faster are:
- not creating users, and
- not reuploading existing SSH keys.