mt-wall
Firewall-as-code for MikroTik RouterOS v7+. Describe your firewall as a
declarative Ruby DSL, keep it in git, and reconcile it onto devices with a
Terraform-style plan / apply workflow: run plan on a pull request to see
the diff, run apply on merge to converge each device. mt-wall owns the
filter/NAT tables wholesale, tags every rule it manages, and applies changes
under a device-side commit-confirm envelope that auto-reverts if an apply locks
you out — so a bad merge heals itself instead of stranding a router.
host "web", address: "10.0.10.5"
host "db", address: "10.0.20.10"
group "frontend" do
member "web"
end
service "mysql", protocol: :tcp, ports: [3306]
rule "frontend" do
to "db", "mysql" # frontend -> db over mysql (allow)
end
policy :forward, :drop # default-drop the forward chain
device "edge-1", host: "192.0.2.1", transport: :rest_api do
policy :input, :drop
management src: "admin", service: "ssh"
input do
allow_established
drop_invalid
accept protocol: :tcp, dst_port: 22, src: "admin"
end
end
60-second quickstart
$ git clone <your firewall-config repo> && cd it
$ bundle install
# 1. Validate the DSL — compiles every device, no network access.
$ bundle exec mt-wall validate config/
OK: configuration is valid (2 device(s) compiled).
# 2. Plan — read-only diff against each live device.
$ export MT_WALL_EDGE_1_USER=ci MT_WALL_EDGE_1_PASSWORD=…
$ bundle exec mt-wall plan config/
Device edge-1 (192.0.2.1):
/ip/firewall/filter
+ create input accept (ssh mgmt)
…
Plan: 12 to create.
Devices: 1 | with changes: 1 | no-change: 0 | failed: 0
# 3. Apply — converge the fleet (prompts for 'yes'; --auto-approve in CI).
$ bundle exec mt-wall apply config/
A ready-to-run sample fleet lives in examples/ — try
bundle exec mt-wall validate examples/config.
The two-layer mental model
mt-wall separates what may talk to what from how each box is firewalled.
- Layer A — portable access policy.
host,group,serviceandrule … to …describe a device-agnostic policy: named address objects and the grants between them. The Compiler injects every grant into the forward chain of every managed device. Write it once; it applies fleet-wide. - Layer B — the per-box firewall. The
deviceblock configures one router's owninput/output/forwardchains, itsnattable, chainpolicydefaults, and themanagementcarve-out. This is where box-specific interfaces, NAT and logging live.
See docs/dsl-reference.md for every verb.
Key safety properties
- Full-table ownership with identity tags. mt-wall owns the
/ip/firewall/filter,/ipv6/firewall/filterand/ip/firewall/nattables wholesale — apply replaces them in full, including RouterOS default-config rules. Every managed rule carries a content-onlymt-wall:<hash>tag in its comment, so the diff matches rules by content + position, never by volatile device.id. Address-lists are owned only for the names mt-wall emits; foreign/static lists are never touched. - Fail-safe ordering. Each chain is assembled with stateful handling first
(
accept established,related→drop invalid), management-protection on the input chain, then operator/grant rules, and the default-drop last — the Compiler fails fast if a locked input chain would lock out management. - Device-side commit-confirm auto-revert.
applybacks up the managed tables on the device and schedules a self-restore; it then applies, runs a manager-side health-check, and only confirms (cancels the revert) if the box is still reachable. If the link drops, the device reverts itself at timeout. - Secrets only in ENV. The DSL references a host + transport, never
credentials. The REST transport reads
MT_WALL_<DEVICE>_USER/MT_WALL_<DEVICE>_PASSWORD, refuses plaintext HTTP without a loud opt-in, and redacts passwords from every log, error and rendered artifact. See docs/security.md.
Install
mt-wall is a Ruby gem with no runtime dependencies (REST over stdlib
net/http, CLI over optparse). Ruby 3.x or newer.
# Gemfile
gem "mt-wall"
$ bundle install
$ bundle exec mt-wall --help
Or build/install from this checkout:
$ gem build mt-wall.gemspec && gem install mt-wall-*.gem
CLI at a glance
| Command | What it does |
|---|---|
mt-wall validate <paths> |
Load + compile the DSL. No device access. Exit 0 / 1. |
mt-wall plan <paths> |
Read-only per-device diff. Exit 0 (no changes) / 2 (changes) / 1 (error). |
mt-wall apply <paths> |
Converge the fleet under commit-confirm. Exit 0 / 1. |
<paths> are *.rb files and/or directories (a directory loads every *.rb
under it, recursively, in sorted order). Flags: --device NAME (repeatable;
target a subset, default all), --json (machine-readable plan/validate),
--auto-approve (non-interactive apply for CI). Full GitOps wiring in
docs/gitops.md.
Status — v1 scope
Feature-complete and validated on RouterOS 7.16.2. The full pipeline (DSL → compile → plan → apply), the firewall primitives (hosts/groups/services/grants, per-box chains, NAT, dual-stack IPv4/IPv6, logging) and the DevOps ergonomics (fleet, JSON, exit codes, commit-confirm) all work end to end.
Deferred to a future release:
- FQDN address-list entries — addresses are IP / CIDR / IP-range only.
- Managing
/interface/list—in_interface_list:/out_interface_list:reference operator-defined lists on the box; mt-wall does not create them. - IPv6 NAT — the
natblock is IPv4-only (no/ipv6/firewall/nat). - Transports beyond REST + offline
.rsc—BinaryApi/Sshare future adapters.
Documentation
- docs/dsl-reference.md — every verb, signature, example and what it compiles to in RouterOS.
- docs/gitops.md — repo layout, plan-on-PR / apply-on-merge, fleets, JSON, exit codes.
- docs/security.md — credentials, TLS, commit-confirm, least-privilege users, ownership implications.
- examples/ — a runnable 2-device sample fleet.
- .github/workflows/mt-wall.yml — a working CI pipeline.
License
Released under the MIT License.