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, service and rule … 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 device block configures one router's own input / output / forward chains, its nat table, chain policy defaults, and the management carve-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/filter and /ip/firewall/nat tables wholesale — apply replaces them in full, including RouterOS default-config rules. Every managed rule carries a content-only mt-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,relateddrop 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. apply backs 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/listin_interface_list: / out_interface_list: reference operator-defined lists on the box; mt-wall does not create them.
  • IPv6 NAT — the nat block is IPv4-only (no /ipv6/firewall/nat).
  • Transports beyond REST + offline .rscBinaryApi / Ssh are future adapters.

Documentation

License

Released under the MIT License.