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
106 lines
6.6 KiB
Markdown
106 lines
6.6 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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).
|
|
- 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) 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.
|