Replace the single `switch_admin_user` day-2 task with a `switch_operators` list in host_vars: one RouterOS user per person, each with the public keys that may log in as it. Logins become attributable and revocable per person instead of accreting as extra keys on `sjat`. Adds claus@stjerno.dk as user `claus` (group full), and codifies the two keys already on the device (sjat's mamba seat, the claude@ubongo automation seat that was hand-imported earlier today). Two things the mechanism has to get right: - Idempotency: query /user/ssh-keys first and upload only missing keys. An unconditional net_put reports changed on every run. - No passwordless accounts: WinBox/console accept an empty password and WinBox is deliberately left on for recovery, so each operator gets a vaulted password. users.yml asserts one exists before touching anything. Verified: yamllint, ansible-lint (production), syntax-check, and the play run twice against crs310-maker — second run changed=0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EyYJWABgSEHzbjsZGrkxVE
6.6 KiB
MakerFLOSS_Mikrotik
Ansible IaC for one MikroTik CRS310-8G+2S+IN switch (RouterOS 7) at the makerspace,
managed over SSH with community.routeros. Sibling project to AnsibleBaobabV4 (whose
conventions this repo copies); independent repo on forgejo.makerfloss.eu.
Tech stack
- Ansible 10.x / ansible-core 2.17,
community.routeros3.x +ansible.netcommon - Connection:
ansible.netcommon.network_cli,ansible_network_os: community.routeros.routeros, SSH key auth - Vault identity
makerfloss(~/.ansible/vault-keys/makerfloss.txt) - Lint:
ansible-lint(profile: production),yamllint
Structure
inventories/prod/hosts.yml— groupmikrotik, hostcrs310-makergroup_vars/mikrotik.yml— connection vars +switch_*_enabledflagsgroup_vars/mikrotik.vault.yml— encrypted password (excluded from linters)host_vars/crs310-maker.yml— device facts, real addressing, VLAN/port map, operatorsfiles/operators/*.pub— operator public keys (public, safe to commit)roles/makerfloss.mikrotik_switch/— one role, per-domain task files gated by flagsplay_switch.yml(day-2),play_bootstrap.yml(first contact),play_backup.ymldocs/— field guide, design spec, implementation plan
Essential commands
yamllint . && ansible-lint && ansible-playbook play_switch.yml --syntax-check
ansible-playbook play_switch.yml # day-2 (key auth)
ansible-playbook play_switch.yml --tags vlans # one domain
ansible-vault view group_vars/mikrotik.vault.yml # read a secret
Access (on-site / bench)
The switch is reachable only via the makerspace laptop mamba. Ansible's network_cli
uses paramiko, which ignores ProxyJump, so port-forward instead of double-hopping:
ssh -N -L 2222:192.168.88.1:22 mamba # tunnel to the switch
ansible-playbook play_switch.yml -e ansible_host=127.0.0.1 -e ansible_port=2222
ssh-keygen -R '[127.0.0.1]:2222' # if the tunnel host key changed
mambais an ssh alias from boma'sssh_clientdrop-in (10.99.0.10:7576over the wg overlay). It superseded the old-J kuku … sjat@10.8.0.4hop; that path is dead.ansible_user: sjatalready comes fromhost_vars, so don't pass it on the CLI.- Every operator seat needs its own key on the device — key auth is the only way in
(see Rules). Don't hand-import: add the person to
switch_operatorsinhost_vars, drop their.pubinfiles/operators/, add avault_operator_passwords.<name>entry, thenansible-playbook play_switch.yml --tags users. The task compares against the device'skey-owner(= the key's comment) and uploads only what is missing, so it is idempotent. Revoke with/user/ssh-keys/remove [find key-owner="…"]plus the var. mambais the mgmt station on switch port 8 (MGMT VLAN); it must be on port 8 to reach192.168.88.1. From a data port it gets10.2.30.xand cannot reach mgmt.- NM profiles on
mambaenp0s31f6:crs310-bench(static.2) andWired connection 1(DHCP). Moving the cable flaps the link and NM re-selects a profile — pin the intended one sticky (autoconnect yes+ higher priority) and the other off, or it reverts. Either profile works now that the mgmt VLAN serves DHCP (.253from the pool). - The
.venvwas built under/home/sjat/…; on a checkout at another path its console scripts fail with exit 126 (stale shebang). Fix the shebangs or rebuild the venv — the systemansibleis not a substitute (no paramiko, newercommunity.general).
Rules
- Idempotency: RouterOS tasks use
community.routeros.commandwith:if [find]guards. Run every device-touching play twice; the second run must report no changes. - Lockout safety: keep an independent recovery channel (serial/WinBox-MAC) when
touching mgmt/services/VLANs; enable
vlan-filteringlast. For lockout-prone changes over the network (vlan-filtering, moving the mgmt IP), run them as a detached self-reverting job —:execute { …; :delay 240s; :if ($mgmtok=false) do={ revert } }, then:global mgmtok trueonce verified. (Auto-healed a hard lockout during the cutover.) - RouterOS
find ... address=<prefix>never matches an ip/address or dhcp-network value (returns 0 even on an exact string) — match by[find interface=X]or:foreach+/ip/address/get $a address. Bit the mgmt-IP move (duplicated the IP). - All real values go in
host_vars; the role holds only mechanism + placeholders. - Secrets go to the
makerflossvault, never plaintext. Encrypt withansible-vault encrypt --encrypt-vault-id makerfloss <file>. vault_switch_admin_passwordcannot log in over SSH and is console/recovery-only. RouterOS refuses password auth for any user that has an SSH key while/ip/ssh always-allow-password-login=no(the default, and deliberately kept). Soplay_bootstrap.yml's password is a one-shot for user creation; after the key import the only SSH path is key auth. Never "fix" a failed login by flipping that flag.- Operators are users, not extra keys on one account — one RouterOS user per person
(
switch_operators), so logins are attributable and revocable one at a time. The exception issjat, which carries a second key for theubongo/claudeautomation seat rather than a separate account. - Never create a passwordless RouterOS user. SSH is key-only, but WinBox/console will
accept an empty password, and WinBox is deliberately left enabled for recovery — so
every
switch_operatorsentry needs avault_operator_passwordsentry.users.ymlasserts this before touching the device. - New work: branch first, implement, verify (lint + syntax + run-twice), then merge.
Status / next
Live on the device (2026-06-09): flat L2 switch on 10.2.30.0/24 — DATA VLAN 30
(ether1 copper uplink + ether2-7 + SFP+), isolated MGMT VLAN 99 on ether8
(mgmt 192.168.88.1/24, no gateway/NTP/DNS), vlan-filtering on. The mgmt port also
serves DHCP (192.168.88.10-.254) + the web UI as a makerspace experiment (flags
switch_web_enabled, switch_mgmt_dhcp_enabled). Default admin disabled. Operators
(2026-09-01): sjat (keys: mamba seat + claude@ubongo automation seat) and claus
(claus@stjerno.dk), both group full, both managed by switch_operators in host_vars.
All task files + play_bootstrap/play_backup are idempotency-verified. Design + cutover runbook:
docs/superpowers/specs/2026-06-09-crs310-flat-mgmtvlan-design.md.
Next: SFP+ 10G uplink and real VLAN segmentation once connectors + a VLAN plan are ready.