Skip to main content

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.

What a plain export does NOT contain
  • 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 D state, 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

  1. 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.
  2. Reset to a blank config: /system reset-configuration no-defaults=yes skip-backup=yes (the device reboots).
  3. Log in (blank admin password after reset), then upload the .rsc file: scp docs/secretzone/mikrotik/ccr2004-192.168.88.1.rsc admin@<ip>:restore.rsc
  4. Import it: /import file-name=restore.rsc
  5. Recreate what the export lacks: admin password (secretzone calypso.md) and the Wireguard private key (/interface wireguard set wireguard1 private-key="...", value in secretzone wireguard.md).
  6. Verify: /ip address print, /ip dhcp-server lease print, a VPN handshake from a client, and dig @192.168.88.1 rumba from 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 .rsc procedure above.
  • Different model: the .rsc is 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

warning
set [find where dst-port=80] is a no-op, not an error

dst-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

DeviceRoleBackup fileLast export
CCR2004-1G-12S+2XScore router, WAN, NAT, Wireguard hub, DNS, DHCPccr2004-192.168.88.1.rsc2026-08-03 (RouterOS 7.20.4)
CRS326-24G-2S+nodes access switchcrs326-192.168.88.254.rsc2026-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.