MikroTik configuration backup & restore
The two MikroTik devices that run the ISC³ network — the CCR2004-1G-12S+2XS core router (192.168.88.1) and the CRS326-24G-2S+ access switch (192.168.88.254) — have their configurations backed up as plain-text RouterOS exports in this repository.
Where the backups live
The exports are stored in the secretzone (tracked in git, never published on the site):
docs/secretzone/mikrotik/ccr2004-192.168.88.1.rsc
docs/secretzone/mikrotik/crs326-192.168.88.254.rsc
Each file is a complete /export of the device: interfaces, bridge, IP addressing, DHCP server and static leases, DNS static entries, firewall, Wireguard peers, routes. The first line records the RouterOS version and export date. Admin credentials for both devices are in the secretzone (calypso.md).
Refreshing the backups
After any configuration change on either device (see the change process), refresh the export and commit it — git history then doubles as a configuration changelog:
S=docs/secretzone/mikrotik
provisioning/network/mikrotik-export.exp 192.168.88.1 2>/dev/null \
| tr -s '\r' | awk '/by RouterOS/{p=1} p' > $S/ccr2004-192.168.88.1.rsc
provisioning/network/mikrotik-export.exp 192.168.88.254 2>/dev/null \
| tr -s '\r' | awk '/by RouterOS/{p=1} p' > $S/crs326-192.168.88.254.rsc
git add $S && git commit -m "MikroTik config backup"
A plain ssh admin@host /export > file does not work: both accounts are password-only, so
there is nothing to answer the prompt outside a terminal. routeros-api.py, otherwise the
reliable way to drive these devices, has no /export — it is a console command with no API
equivalent on RouterOS 6. Hence the expect wrapper.
Two filters matter. tr -s '\r' collapses the carriage return the pty echoes on every line;
without it every line ends \r\r\n and git reports the entire file as rewritten, burying the
few lines that changed. awk drops the login exchange before the export header. Keep the files
CRLF — that is how the router emits them, and normalising them would produce one unreadable
diff.
- User accounts and passwords — never exported; recreate from the secretzone.
- Sensitive values (Wireguard private keys, SNMP communities, RADIUS/PPP secrets) — omitted unless exported with
/export show-sensitive. The Wireguard server key matters most here: without it, every peer (86!) would need a new server public key. It is recorded in the secretzone (wireguard.md). - Certificates and dynamic state (DHCP leases in
Dstate, ARP, logs).
For a byte-exact safety net that includes everything (passwords, certificates, keys), additionally take a binary backup on the device — /system backup save name=ccr2004-YYYY-MM-DD — and copy it off with scp admin@192.168.88.1:ccr2004-YYYY-MM-DD.backup . onto the NAS. Binary backups are only restorable on the same model/architecture.
Restore process
Same device, bad configuration
- Connect out-of-band if possible (serial console, or MAC-Winbox from the same L2), so you don't saw off the branch you sit on.
- Reset to a blank config:
/system reset-configuration no-defaults=yes skip-backup=yes(the device reboots). - Log in (blank admin password after reset), then upload the
.rscfile:scp docs/secretzone/mikrotik/ccr2004-192.168.88.1.rsc admin@<ip>:restore.rsc - Import it:
/import file-name=restore.rsc - Recreate what the export lacks: admin password (secretzone
calypso.md) and the Wireguard private key (/interface wireguard set wireguard1 private-key="...", value in secretzonewireguard.md). - Verify:
/ip address print,/ip dhcp-server lease print, a VPN handshake from a client, anddig @192.168.88.1 rumbafrom the VPN.
Replacement device (hardware failure)
- Same model: restore the binary backup if one exists (
/system backup load name=...) — complete, including passwords. Otherwise follow the.rscprocedure above. - Different model: the
.rscis the only option; port numbers/names (sfp-sfpplus5,ether8, bonds) will need manual adjustment to the new hardware. The network page documents which machine hangs off which port.
Scripting RouterOS: find silently matches nothing
set [find where dst-port=80] is a no-op, not an errordst-port and to-ports are port-range typed, so find cannot compare them to a plain number.
The expression matches zero rules, set over an empty result succeeds without output, and the
configuration is unchanged — the script looks like it worked. This cost a debugging round while
repointing the inbound 80/443 forwards.
Select on something find compares reliably (the comment), and always assert the match count:
# WRONG - matches nothing, changes nothing, reports nothing
/ip firewall nat set [find where chain=dstnat and dst-port=80] to-addresses=192.168.88.150
# RIGHT - select by comment, and print the count before writing
:put [:len [/ip firewall nat find where comment~"Ingoing HTTP to"]]
/ip firewall nat set [find where comment~"Ingoing HTTP to"] to-addresses=192.168.88.150
Get a rule's stable internal id with /ip firewall nat print detail (the .id=*7 field) and use
that when a comment is not distinctive. Note that the printed row numbers (0, 1, …) are display
indices only and shift as rules are added — never script against them.
Current state
| Device | Role | Backup file | Last export |
|---|---|---|---|
| CCR2004-1G-12S+2XS | core router, WAN, NAT, Wireguard hub, DNS, DHCP | ccr2004-192.168.88.1.rsc | 2026-08-03 (RouterOS 7.20.4) |
| CRS326-24G-2S+ | nodes access switch | crs326-192.168.88.254.rsc | 2026-08-03 (RouterOS 6.49.19) |
Both exports were refreshed on 2026-08-03, after turning the NetBird overlay's masquerade off — the
CCR2004 gained the 100.64.0.0/10 return route via srv-netbird (192.168.88.159). The same day's
earlier refreshes cover the internal DNS entries added for srv-runner01, srv-status,
learn-test, the GitLab Pages test domain, and the split-horizon entry for sso.isc-vs.ch.