MakerFLOSS_Mikrotik/CLAUDE.md
sjat 1205c139f0 docs: record lars as operator + the NM no-lease-after-replug trap
Applied to the device and idempotency-verified (run 1 changed=1, run 2
changed=0). Also notes the NetworkManager behaviour that looked like a
mis-plugged cable: after a re-plug the wired profile stays active with no
IPv4 until the connection is cycled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EyYJWABgSEHzbjsZGrkxVE
2026-09-01 22:16:32 +02:00

6.9 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.routeros 3.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 — group mikrotik, host crs310-maker
  • group_vars/mikrotik.yml — connection vars + switch_*_enabled flags
  • group_vars/mikrotik.vault.yml — encrypted password (excluded from linters)
  • host_vars/crs310-maker.yml — device facts, real addressing, VLAN/port map, operators
  • files/operators/*.pub — operator public keys (public, safe to commit)
  • roles/makerfloss.mikrotik_switch/ — one role, per-domain task files gated by flags
  • play_switch.yml (day-2), play_bootstrap.yml (first contact), play_backup.yml
  • docs/ — 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
  • mamba is an ssh alias from boma's ssh_client drop-in (10.99.0.10:7576 over the wg overlay). It superseded the old -J kuku … sjat@10.8.0.4 hop; that path is dead.
  • ansible_user: sjat already comes from host_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_operators in host_vars, drop their .pub in files/operators/, add a vault_operator_passwords.<name> entry, then ansible-playbook play_switch.yml --tags users. The task compares against the device's key-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.
  • mamba is the mgmt station on switch port 8 (MGMT VLAN); it must be on port 8 to reach 192.168.88.1. From a data port it gets 10.2.30.x and cannot reach mgmt.
  • NM profiles on mamba enp0s31f6: crs310-bench (static .2) and Wired 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 (.253 from the pool). After a re-plug the link often comes up with no IPv4 at all (profile active, DHCP never retried). nmcli connection down/up "Wired connection 1" fixes it — check for an address before concluding the cable is in the wrong port.
  • The .venv was 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 system ansible is not a substitute (no paramiko, newer community.general).

Rules

  • Idempotency: RouterOS tasks use community.routeros.command with :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-filtering last. 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 true once 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 makerfloss vault, never plaintext. Encrypt with ansible-vault encrypt --encrypt-vault-id makerfloss <file>.
  • vault_switch_admin_password cannot 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). So play_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 is sjat, which carries a second key for the ubongo/claude automation 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_operators entry needs a vault_operator_passwords entry. users.yml asserts 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), claus (claus@stjerno.dk) and lars (lars@hrossen.dk, the same key he uses on the MakerFLOSS forgejo and VPS), all group full, all 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.