2026-06-07 08:34:13 +02:00
|
|
|
---
|
2026-09-01 20:29:18 +02:00
|
|
|
# Operator accounts and their SSH keys.
|
|
|
|
|
#
|
|
|
|
|
# `switch_operators` (host_vars) is the source of truth: one entry per person/seat,
|
|
|
|
|
# each with the group it belongs to and the public keys that may log in as it. The
|
|
|
|
|
# role only holds mechanism.
|
|
|
|
|
#
|
|
|
|
|
# Idempotency: the device is queried FIRST and only genuinely missing keys are
|
|
|
|
|
# uploaded — an unconditional `net_put` would report changed on every run. RouterOS
|
|
|
|
|
# reports a key's comment field as `key-owner`, which is what we match on.
|
|
|
|
|
#
|
|
|
|
|
# Passwords: a RouterOS user with no password can still be reached over WinBox/console
|
|
|
|
|
# without one, so operators are created with a vaulted password even though SSH is
|
|
|
|
|
# key-only (`/ip/ssh always-allow-password-login=no`). See CLAUDE.md.
|
2026-06-08 19:34:58 +02:00
|
|
|
|
2026-09-01 20:29:18 +02:00
|
|
|
- name: Assert every operator has a vaulted password
|
|
|
|
|
ansible.builtin.assert:
|
|
|
|
|
that: switch_operators | map(attribute='name') | difference(vault_operator_passwords | default({}) | list) | length == 0
|
|
|
|
|
fail_msg: >-
|
|
|
|
|
Every entry in switch_operators needs a vault_operator_passwords.<name> entry —
|
|
|
|
|
a passwordless RouterOS user is reachable over WinBox/console without one.
|
|
|
|
|
Missing: {{ switch_operators | map(attribute='name')
|
|
|
|
|
| difference(vault_operator_passwords | default({}) | list) | join(', ') }}
|
|
|
|
|
quiet: true
|
|
|
|
|
|
|
|
|
|
- name: Read the SSH keys currently installed on the device
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- /user/ssh-keys/print detail
|
|
|
|
|
register: switch_ssh_keys_raw
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Build the set of user/key-owner pairs already present
|
|
|
|
|
ansible.builtin.set_fact:
|
|
|
|
|
switch_ssh_keys_present: >-
|
|
|
|
|
{{ switch_ssh_keys_raw.stdout[0]
|
|
|
|
|
| regex_findall('user=(\S+).*?key-owner="([^"]*)"')
|
|
|
|
|
| map('join', '|') | list }}
|
|
|
|
|
|
|
|
|
|
- name: Ensure each operator account exists in the right group
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/user find name="{{ item.name }}"]] = 0)
|
|
|
|
|
do={ /user add name="{{ item.name }}" group="{{ item.group }}"
|
|
|
|
|
password="{{ vault_operator_passwords[item.name] }}" }
|
|
|
|
|
else={ :if ([/user get [find name="{{ item.name }}"] group] != "{{ item.group }}")
|
|
|
|
|
do={ /user set [find name="{{ item.name }}"] group="{{ item.group }}" } }
|
|
|
|
|
loop: "{{ switch_operators }}"
|
|
|
|
|
loop_control:
|
|
|
|
|
label: "{{ item.name }}"
|
|
|
|
|
no_log: true
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Upload the operator public keys that are not on the device yet
|
|
|
|
|
ansible.netcommon.net_put:
|
|
|
|
|
src: "{{ item.1 }}"
|
|
|
|
|
dest: "{{ item.1 | basename }}"
|
|
|
|
|
loop: "{{ switch_operators | subelements('pubkey_files') }}"
|
|
|
|
|
loop_control:
|
|
|
|
|
label: "{{ item.0.name }} <- {{ item.1 | basename }}"
|
|
|
|
|
when: item.0.name ~ '|' ~ (lookup('file', item.1).split() | last) not in switch_ssh_keys_present
|
|
|
|
|
|
|
|
|
|
- name: Import the newly uploaded operator public keys
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
/user/ssh-keys/import public-key-file="{{ item.1 | basename }}"
|
|
|
|
|
user="{{ item.0.name }}"
|
|
|
|
|
loop: "{{ switch_operators | subelements('pubkey_files') }}"
|
|
|
|
|
loop_control:
|
|
|
|
|
label: "{{ item.0.name }} <- {{ item.1 | basename }}"
|
|
|
|
|
when: item.0.name ~ '|' ~ (lookup('file', item.1).split() | last) not in switch_ssh_keys_present
|
|
|
|
|
changed_when: true
|
|
|
|
|
|
|
|
|
|
- name: Remove the uploaded key files from the switch flash
|
2026-06-08 19:34:58 +02:00
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
2026-09-01 20:29:18 +02:00
|
|
|
:if ([:len [/file find name="{{ item.1 | basename }}"]] > 0)
|
|
|
|
|
do={ /file remove [find name="{{ item.1 | basename }}"] }
|
|
|
|
|
loop: "{{ switch_operators | subelements('pubkey_files') }}"
|
|
|
|
|
loop_control:
|
|
|
|
|
label: "{{ item.1 | basename }}"
|
2026-06-08 19:34:58 +02:00
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Disable the default admin user
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/user find name="admin"]] > 0) do={
|
|
|
|
|
/user/set admin disabled=yes }
|
|
|
|
|
when: switch_disable_default_admin | bool
|
|
|
|
|
changed_when: false
|