mt-wall 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +55 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +166 -0
  5. data/docs/dsl-reference.md +388 -0
  6. data/docs/gitops.md +173 -0
  7. data/docs/security.md +142 -0
  8. data/examples/README.md +67 -0
  9. data/examples/config/devices/edge-1.rb +44 -0
  10. data/examples/config/devices/edge-2.rb +41 -0
  11. data/examples/config/objects.rb +46 -0
  12. data/examples/config/policy.rb +30 -0
  13. data/examples/config/services.rb +19 -0
  14. data/exe/mt-wall +6 -0
  15. data/lib/mt/wall/cli.rb +473 -0
  16. data/lib/mt/wall/compiler.rb +613 -0
  17. data/lib/mt/wall/configuration.rb +123 -0
  18. data/lib/mt/wall/desired_state.rb +200 -0
  19. data/lib/mt/wall/dsl/chain_builder.rb +112 -0
  20. data/lib/mt/wall/dsl/device_builder.rb +149 -0
  21. data/lib/mt/wall/dsl/group_builder.rb +36 -0
  22. data/lib/mt/wall/dsl/host_builder.rb +64 -0
  23. data/lib/mt/wall/dsl/nat_builder.rb +114 -0
  24. data/lib/mt/wall/dsl/policy_scope.rb +31 -0
  25. data/lib/mt/wall/dsl/root_builder.rb +141 -0
  26. data/lib/mt/wall/dsl/rule_builder.rb +86 -0
  27. data/lib/mt/wall/dsl/rule_scope.rb +35 -0
  28. data/lib/mt/wall/dsl/validators.rb +306 -0
  29. data/lib/mt/wall/dsl.rb +61 -0
  30. data/lib/mt/wall/errors.rb +19 -0
  31. data/lib/mt/wall/model/address_object.rb +35 -0
  32. data/lib/mt/wall/model/device.rb +54 -0
  33. data/lib/mt/wall/model/filter_rule.rb +66 -0
  34. data/lib/mt/wall/model/group.rb +27 -0
  35. data/lib/mt/wall/model/nat_rule.rb +49 -0
  36. data/lib/mt/wall/model/policy.rb +27 -0
  37. data/lib/mt/wall/model/rule.rb +50 -0
  38. data/lib/mt/wall/model/service.rb +42 -0
  39. data/lib/mt/wall/plan.rb +304 -0
  40. data/lib/mt/wall/reconciler.rb +148 -0
  41. data/lib/mt/wall/transport/base.rb +79 -0
  42. data/lib/mt/wall/transport/rest_api.rb +464 -0
  43. data/lib/mt/wall/transport/rsc.rb +99 -0
  44. data/lib/mt/wall/version.rb +7 -0
  45. data/lib/mt/wall.rb +56 -0
  46. metadata +91 -0
data/docs/gitops.md ADDED
@@ -0,0 +1,173 @@
1
+ # GitOps workflow
2
+
3
+ mt-wall is built for a Terraform-style loop: your firewall lives as DSL in git,
4
+ **plan runs on every pull request** (read-only), and **apply runs on merge to
5
+ the main branch**. The CLI's exit codes and `--json` output are designed to wire
6
+ straight into CI.
7
+
8
+ ## Recommended repo layout
9
+
10
+ The CLI loads a directory **recursively**, picking up every `*.rb` under it in
11
+ deterministic sorted order and folding them into **one** Configuration. Split
12
+ the config by concern:
13
+
14
+ ```
15
+ firewall-repo/
16
+ ├── config/
17
+ │ ├── objects.rb # hosts + groups (Layer A)
18
+ │ ├── services.rb # named protocol/port definitions
19
+ │ ├── policy.rb # access grants + global chain defaults
20
+ │ └── devices/
21
+ │ ├── edge-1.rb # per-box firewall (Layer B)
22
+ │ └── edge-2.rb
23
+ ├── Gemfile # gem "mt-wall"
24
+ └── .github/workflows/mt-wall.yml
25
+ ```
26
+
27
+ Then every command targets the directory:
28
+
29
+ ```console
30
+ $ mt-wall validate config/
31
+ $ mt-wall plan config/
32
+ $ mt-wall apply config/
33
+ ```
34
+
35
+ A complete working example is in [`../examples/`](../examples/).
36
+
37
+ > Load order is deterministic: files passed directly load in the order given;
38
+ > a directory's `*.rb` files load in lexicographic order. Because all files
39
+ > fold into one Configuration, hosts/services/grants defined in one file are
40
+ > visible to device blocks in another.
41
+
42
+ ## The loop
43
+
44
+ ### On a pull request — validate + plan (read-only)
45
+
46
+ ```console
47
+ $ mt-wall validate config/ # compiles every device; no network access
48
+ $ mt-wall plan config/ # diffs each device against its live state
49
+ ```
50
+
51
+ `validate` never touches a device — it only loads and compiles the DSL, so it
52
+ catches typos, unknown references, cycles, lockout risks and zero-resolution
53
+ errors. `plan` is **read-only**: it fetches current state and prints the diff,
54
+ but mutates nothing. Both are safe to run on untrusted PR branches.
55
+
56
+ ### On merge to main — apply
57
+
58
+ ```console
59
+ $ mt-wall apply --auto-approve config/
60
+ ```
61
+
62
+ `apply` converges each changed device under the device-side commit-confirm
63
+ envelope (see [security.md](security.md)). Run it only on the trusted path
64
+ (merge to main), with credentials available as CI secrets.
65
+
66
+ ## Fleets and targeting
67
+
68
+ `plan` and `apply` operate on **every device** in the loaded Configuration by
69
+ default, printing a per-device section followed by a rollup line:
70
+
71
+ ```
72
+ Devices: 3 | with changes: 1 | no-change: 2 | failed: 0
73
+ ```
74
+
75
+ Use `--device NAME` to target a subset; it is **repeatable**:
76
+
77
+ ```console
78
+ $ mt-wall plan --device edge-1 config/
79
+ $ mt-wall apply --device edge-1 --device edge-2 --auto-approve config/
80
+ ```
81
+
82
+ An unknown device name fails fast. During a fleet run, a transport failure on
83
+ one device is captured (reported as `failed`) so the rest of the fleet is still
84
+ planned/applied.
85
+
86
+ ## Posting the plan to a PR — `--json`
87
+
88
+ `plan --json` and `validate --json` emit machine-readable output instead of
89
+ text — ideal for posting the plan as a PR comment or gating a downstream step.
90
+
91
+ `plan --json` produces a top-level object keyed by device name; each carries its
92
+ host, a `changed` flag, summary counts, and the per-path operations:
93
+
94
+ ```json
95
+ {
96
+ "edge-1": {
97
+ "host": "192.0.2.1",
98
+ "changed": true,
99
+ "summary": { "create": 12, "update": 0, "move": 0, "delete": 0, "total": 12 },
100
+ "operations": [
101
+ { "action": "create", "path": "/ip/firewall/filter",
102
+ "identity": "input accept (ssh mgmt)", "comment": "mt-wall:… | ssh mgmt",
103
+ "payload": { "chain": "input", "action": "accept", "...": "..." } }
104
+ ]
105
+ }
106
+ }
107
+ ```
108
+
109
+ `validate --json` emits `{ "valid": true, "devices": [...] }` (or `valid:
110
+ false` with per-device `errors`). Exit codes are identical to text mode.
111
+
112
+ A typical PR step runs `mt-wall plan --json config/ > plan.json` and posts it as
113
+ a comment with your CI's GitHub API helper.
114
+
115
+ ## Exit codes
116
+
117
+ Aggregated across the fleet, Terraform-style:
118
+
119
+ | code | meaning |
120
+ | ---- | ------------------------------------------------------------------- |
121
+ | `0` | success / no device has pending changes |
122
+ | `2` | **`plan` only** — success, but at least one device has changes |
123
+ | `1` | error: invalid DSL, unknown device, transport/plan failure, bad args, apply aborted/refused, or any device failed during apply |
124
+
125
+ CI uses these directly:
126
+
127
+ - **PR gate:** run `mt-wall validate` then `mt-wall plan`. Exit `1` fails the
128
+ build; exit `2` means "there is work to apply" (surface it, don't fail);
129
+ exit `0` means in sync.
130
+ - **Apply gate:** run `mt-wall apply --auto-approve`. Exit `0` = converged,
131
+ `1` = something failed (the device-side auto-revert protects against lockout).
132
+
133
+ ## Non-interactive apply — `--auto-approve` and TTY behavior
134
+
135
+ `apply` is gated by a confirmation prompt (Terraform-style):
136
+
137
+ ```
138
+ Apply these changes to edge-1, edge-2? Only 'yes' will be accepted:
139
+ ```
140
+
141
+ Only the exact string `yes` proceeds; anything else cancels (exit `1`, no
142
+ changes made). In CI there is no TTY, so:
143
+
144
+ - **without `--auto-approve` on a non-interactive stdin, apply is refused** —
145
+ it prints `Refusing to apply without a TTY; re-run with --auto-approve …` and
146
+ exits `1`. mt-wall never blocks waiting for input in a pipeline.
147
+ - **`--auto-approve` skips the prompt** — use it for the merge-to-main job.
148
+
149
+ If no device has pending changes, `apply` reports up-to-date and exits `0`
150
+ without prompting.
151
+
152
+ ## Adapting to GitLab CI / others
153
+
154
+ The contract is the CLI's exit codes and stdout/JSON — nothing GitHub-specific.
155
+ A GitLab `.gitlab-ci.yml` mirrors the GitHub workflow:
156
+
157
+ ```yaml
158
+ plan:
159
+ rules: [{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' }]
160
+ script:
161
+ - bundle exec mt-wall validate config/
162
+ - bundle exec mt-wall plan config/ # exit 2 = changes; allow it
163
+ allow_failure: { exit_codes: [2] }
164
+
165
+ apply:
166
+ rules: [{ if: '$CI_COMMIT_BRANCH == "main"' }]
167
+ script:
168
+ - bundle exec mt-wall apply --auto-approve config/
169
+ # MT_WALL_EDGE_1_USER / _PASSWORD etc. set as masked CI/CD variables
170
+ ```
171
+
172
+ See [.github/workflows/mt-wall.yml](../.github/workflows/mt-wall.yml) for the
173
+ fully-commented GitHub Actions version.
data/docs/security.md ADDED
@@ -0,0 +1,142 @@
1
+ # Security & safety
2
+
3
+ mt-wall treats security as a first-class concern: credentials never live in the
4
+ DSL, plaintext transport is refused by default, applies are protected by a
5
+ device-side auto-revert, and the tool's ownership model is explicit about what
6
+ it replaces.
7
+
8
+ ## Credentials live only in ENV — never in the DSL or git
9
+
10
+ The `device` block references a host + transport. It does **not** carry
11
+ credentials. The REST transport reads them from environment variables at apply
12
+ time, using a canonical, collision-checked derivation from the device name.
13
+
14
+ **Name derivation:** the device name is upcased, every run of non-`[A-Z0-9]`
15
+ characters collapses to a single `_`, and leading/trailing `_` are stripped:
16
+
17
+ | device name | ENV prefix | variables read |
18
+ | ----------- | ------------------- | --------------------------------------------- |
19
+ | `edge-1` | `MT_WALL_EDGE_1` | `MT_WALL_EDGE_1_USER`, `MT_WALL_EDGE_1_PASSWORD` |
20
+ | `core.fw` | `MT_WALL_CORE_FW` | `MT_WALL_CORE_FW_USER`, `MT_WALL_CORE_FW_PASSWORD` |
21
+ | `dc1-edge` | `MT_WALL_DC1_EDGE` | `MT_WALL_DC1_EDGE_USER`, `MT_WALL_DC1_EDGE_PASSWORD` |
22
+
23
+ A missing or empty expected variable **fails fast** with a `TransportError`
24
+ naming the variable to set. If two distinct device names would collapse to the
25
+ same prefix (silently sharing one credential), that is also a fail-fast error.
26
+
27
+ `validate` and `plan` that don't reach a device need no credentials; `plan`
28
+ against a live device and `apply` do.
29
+
30
+ ## Wiring credentials into CI
31
+
32
+ Store each device's user/password as a **secret** in your CI provider and map it
33
+ into the job's environment — never echo it, never commit it.
34
+
35
+ ```yaml
36
+ # GitHub Actions (apply job)
37
+ env:
38
+ MT_WALL_EDGE_1_USER: ${{ secrets.MT_WALL_EDGE_1_USER }}
39
+ MT_WALL_EDGE_1_PASSWORD: ${{ secrets.MT_WALL_EDGE_1_PASSWORD }}
40
+ MT_WALL_EDGE_2_USER: ${{ secrets.MT_WALL_EDGE_2_USER }}
41
+ MT_WALL_EDGE_2_PASSWORD: ${{ secrets.MT_WALL_EDGE_2_PASSWORD }}
42
+ ```
43
+
44
+ mt-wall **redacts credentials centrally**: the password never appears in any
45
+ URL, log line, exception message, or rendered `.rsc` artifact (a dedicated test
46
+ enforces this). The transport's `inspect`/`to_s` prints host/port/user and a
47
+ `secure?` flag only.
48
+
49
+ ## Transport security: TLS by default, plaintext refused
50
+
51
+ The REST transport talks to `https://<host>/rest/…` over HTTP Basic auth.
52
+ Because Basic-auth credentials would travel in clear text otherwise:
53
+
54
+ - **Plaintext HTTP is refused.** Targeting port 80 raises a `TransportError`
55
+ unless you opt in *loudly* — either the device option `insecure_http: true`
56
+ or the env var `MT_WALL_INSECURE_HTTP` (`1`/`true`/`yes`). Reserve this for a
57
+ lab; it is never appropriate against a production device.
58
+ - **TLS verification is on.** Prefer pinning over disabling verification:
59
+
60
+ | device option | effect |
61
+ | ------------------------ | ----------------------------------------------------- |
62
+ | `ca_file: "ca.pem"` | verify against a specific CA bundle / pinned cert |
63
+ | `tls_fingerprint: "ab:…"`| pin the server leaf cert's SHA-256 fingerprint |
64
+ | `verify_tls: false` | blanket-disable verification — discouraged, explicit opt-out |
65
+
66
+ ```ruby
67
+ device "edge-1", host: "192.0.2.1", transport: :rest_api,
68
+ tls_fingerprint: "AA:BB:CC:…" do
69
+ # …
70
+ end
71
+ ```
72
+
73
+ ## Fail-safe apply: device-side commit-confirm
74
+
75
+ mt-wall owns and *replaces* the whole filter/NAT table — over a network path
76
+ that often runs **through that very firewall**. A client-side rollback is
77
+ undeliverable if the link drops, and RouterOS REST has no firewall
78
+ transaction/safe-mode. So `apply` uses a **device-side** commit-confirm
79
+ envelope:
80
+
81
+ 1. **Arm.** The transport takes a full backup **on the device** and schedules a
82
+ `/system/scheduler` job that restores it after `revert_timeout` seconds
83
+ (default 120, per-device via `revert_timeout:`). The backup is taken *before*
84
+ the revert machinery is created, so a fired revert also discards itself.
85
+ 2. **Apply.** Operations run in fail-safe order (open access + management-protect
86
+ before tightening; create-before-delete; default-drop last — encoded by the
87
+ planner).
88
+ 3. **Health-check.** The manager re-reaches the device and confirms the input
89
+ chain still admits management traffic, retrying briefly to tolerate a
90
+ post-apply blip.
91
+ 4. **Confirm.** On success the transport cancels the scheduled revert. **If the
92
+ health-check fails or the link is lost, confirm never runs and the device
93
+ self-restores the backup at timeout** — a bad apply heals itself.
94
+
95
+ The offline `:rsc` transport has no live link to protect, so it renders the
96
+ script and skips the envelope.
97
+
98
+ ## Recommended least-privilege RouterOS user
99
+
100
+ Create a dedicated user for mt-wall rather than reusing `admin`. It needs to
101
+ read/write the firewall tables and operate the commit-confirm machinery
102
+ (backup + scheduler/script), reachable over the REST service:
103
+
104
+ - **Group permissions:** `api`, `rest-api`, `read`, `write`, `policy`, `test`,
105
+ and (for the auto-revert) `sensitive` to create the backup/scheduler/script.
106
+ - **Restrict the source.** Limit the user (or the `www-ssl`/`api` service) to
107
+ the management subnet you declared via `management src:`, so the credential is
108
+ useless from anywhere else.
109
+ - **Enable only the TLS REST service** (`www-ssl`) with a real certificate;
110
+ disable plaintext `www` if you are not in a lab.
111
+
112
+ ```routeros
113
+ /user group add name=mt-wall policy=api,rest-api,read,write,policy,test,sensitive
114
+ /user add name=mt-wall group=mt-wall password=… address=10.0.0.0/24
115
+ /ip service set www-ssl disabled=no address=10.0.0.0/24
116
+ /ip service set www disabled=yes
117
+ ```
118
+
119
+ ## Full-table ownership — know what apply replaces
120
+
121
+ Ownership is **per-table, not uniform**:
122
+
123
+ - **Filter & NAT tables** (`/ip/firewall/filter`, `/ipv6/firewall/filter`,
124
+ `/ip/firewall/nat`) are owned **wholesale**. `apply` makes the device's table
125
+ match the compiled desired state **in full** — including removing RouterOS
126
+ default-config rules and any rule mt-wall did not emit. Everything you want on
127
+ these chains must be expressed in the DSL.
128
+ - **Address-lists** (`/ip/firewall/address-list`, `/ipv6/firewall/address-list`)
129
+ are owned **only for the list names mt-wall emits** from your hosts/groups.
130
+ Foreign/static lists (operator- or script-maintained) are never read into the
131
+ diff, never modified, never deleted. Address-list rows diff by the natural key
132
+ `(list, address)`.
133
+
134
+ Every managed filter/NAT rule carries a content-only `mt-wall:<hash>` identity
135
+ tag in its comment, so the diff matches rules by content + position rather than
136
+ volatile device `.id`. RouterOS-generated `dynamic` rows are excluded from the
137
+ fetched state and are never diffed or deleted.
138
+
139
+ > **Implication:** before adopting mt-wall on a device with hand-built rules,
140
+ > capture those rules into the DSL first — otherwise the first `apply` will
141
+ > remove anything not represented. Use `plan` (read-only) to preview exactly
142
+ > what would change.
@@ -0,0 +1,67 @@
1
+ # Example: a 2-device edge fleet
2
+
3
+ A small, **runnable** mt-wall configuration for a two-router fleet (`edge-1`,
4
+ `edge-2`). It exercises the full DSL surface: hosts/groups/services, Layer-A
5
+ access grants, per-box chains, NAT, management carve-outs, logging, dual-stack
6
+ rules and per-device options.
7
+
8
+ ## Layout
9
+
10
+ ```
11
+ examples/config/
12
+ ├── objects.rb # Layer A: hosts + groups (address objects)
13
+ ├── services.rb # Layer A: named protocol/port definitions
14
+ ├── policy.rb # Layer A: access grants + global chain defaults
15
+ └── devices/
16
+ ├── edge-1.rb # Layer B: primary edge — chains, NAT, management
17
+ └── edge-2.rb # Layer B: branch edge — fasttrack off, output chain
18
+ ```
19
+
20
+ All files fold into **one** Configuration when the directory is loaded, so the
21
+ hosts/services/grants in the top three files are visible to both device blocks.
22
+
23
+ ## What it shows
24
+
25
+ - **Layer A** (`objects.rb`, `services.rb`, `policy.rb`): hosts holding single
26
+ addresses, arrays and a CIDR; group membership both group-side (`member`) and
27
+ host-side (positional, auto-creating `trusted-resolvers`); a multi-protocol
28
+ service (`dns` = tcp+udp); a port range (`web-highports`); allow + an explicit
29
+ logged `:deny` grant; global `policy` defaults.
30
+ - **edge-1** (`devices/edge-1.rb`): locked input chain with a required
31
+ `management` carve-out, IPv4 + pinned IPv6 rules, interface-list drop with
32
+ logging, a logged forward default, and a `nat` block (masquerade +
33
+ port-forward `dst_nat`).
34
+ - **edge-2** (`devices/edge-2.rb`): per-device options (`fasttrack: false`,
35
+ `revert_timeout: 180`), a `reject` rule, an `output` chain, and a built-in
36
+ management service (`winbox`).
37
+
38
+ ## Run it
39
+
40
+ From the gem checkout root:
41
+
42
+ ```console
43
+ # Validate — compiles every device, no network access. Exits 0.
44
+ $ bundle exec mt-wall validate examples/config
45
+ OK: configuration is valid (2 device(s) compiled).
46
+
47
+ # Machine-readable validation.
48
+ $ bundle exec mt-wall validate --json examples/config
49
+ ```
50
+
51
+ `plan` and `apply` would reach the (fictional) devices over REST and therefore
52
+ need credentials in ENV — they won't connect to `192.0.2.x`, but this is the
53
+ shape:
54
+
55
+ ```console
56
+ $ export MT_WALL_EDGE_1_USER=ci MT_WALL_EDGE_1_PASSWORD=secret
57
+ $ export MT_WALL_EDGE_2_USER=ci MT_WALL_EDGE_2_PASSWORD=secret
58
+ $ bundle exec mt-wall plan examples/config # read-only diff
59
+ $ bundle exec mt-wall plan --device edge-1 examples/config
60
+ $ bundle exec mt-wall apply --auto-approve examples/config
61
+ ```
62
+
63
+ To render an offline RouterOS `.rsc` script instead of touching a device,
64
+ switch a device's `transport:` to `:rsc`.
65
+
66
+ See [../docs/dsl-reference.md](../docs/dsl-reference.md) for every verb and
67
+ [../docs/gitops.md](../docs/gitops.md) for the CI loop.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Layer B — the per-box firewall for the primary edge router.
4
+ #
5
+ # The device block configures THIS router's own input/output/forward chains and
6
+ # NAT. The Layer-A grants from policy.rb are injected into its forward chain
7
+ # automatically. Credentials are NEVER here — the REST transport reads them from
8
+ # MT_WALL_EDGE_1_USER / MT_WALL_EDGE_1_PASSWORD at apply time.
9
+ device "edge-1", host: "192.0.2.1", transport: :rest_api do
10
+ policy :input, :drop
11
+ policy :forward, :drop, log: true, log_prefix: "fwd-drop" # log the default
12
+
13
+ # Declare the mgmt traffic the safe chain must keep open so an apply can never
14
+ # lock you out. REQUIRED once the input chain is locked (policy :input, :drop).
15
+ # REPEATABLE: each call adds a protected path; the compiler emits their union.
16
+ management src: "admin", service: "ssh" # human admins over SSH
17
+ management src: "ci", port: 443 # CI runner applying over REST/HTTPS
18
+
19
+ input do
20
+ allow_established # accept established,related
21
+ drop_invalid # drop invalid
22
+ accept protocol: :icmp # IPv4 ping
23
+ accept protocol: :icmpv6, family: :ip6 # IPv6 ping (pinned)
24
+ accept protocol: :tcp, dst_port: 22, src: "admin", comment: "ssh mgmt"
25
+ accept protocol: :tcp, dst_port: 8291, src: "admin", comment: "winbox mgmt"
26
+ # in_interface_list references an operator-defined /interface/list on the box
27
+ # (mt-wall does not manage /interface/list in v1).
28
+ drop in_interface_list: "WAN", log: true, log_prefix: "wan-input-drop"
29
+ end
30
+
31
+ forward do
32
+ allow_established
33
+ drop_invalid
34
+ # Layer-A grants are injected here, then the forward default policy.
35
+ end
36
+
37
+ nat do
38
+ masquerade out_interface: "ether1-wan" # hide LAN behind WAN
39
+ dst_nat protocol: :tcp, dst_port: 443, # publish web (port-fwd)
40
+ in_interface: "ether1-wan",
41
+ to_addresses: "10.0.10.5", to_ports: 8443,
42
+ comment: "publish web"
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Layer B — the per-box firewall for the branch edge router.
4
+ #
5
+ # Shows per-device options: `fasttrack: false` suppresses the default IPv4
6
+ # forward fasttrack rule (so mangle/queues/IPsec see every packet), and
7
+ # `revert_timeout:` tunes how long the device-side auto-revert waits before
8
+ # self-restoring if an apply never confirms. Credentials come from
9
+ # MT_WALL_EDGE_2_USER / MT_WALL_EDGE_2_PASSWORD.
10
+ device "edge-2", host: "192.0.2.2", transport: :rest_api,
11
+ fasttrack: false, revert_timeout: 180 do
12
+ policy :input, :drop
13
+ policy :forward, :drop
14
+
15
+ management src: "admin", service: "winbox"
16
+
17
+ input do
18
+ allow_established
19
+ drop_invalid
20
+ accept protocol: :icmp
21
+ accept protocol: :tcp, dst_port: 8291, src: "admin", comment: "winbox mgmt"
22
+ # reject (sends an ICMP/TCP-reset) external DNS probing on the WAN list.
23
+ reject protocol: :udp, dst_port: 53, in_interface_list: "WAN",
24
+ comment: "reject external dns"
25
+ drop in_interface_list: "WAN", log: true, log_prefix: "wan-drop"
26
+ end
27
+
28
+ forward do
29
+ allow_established
30
+ drop_invalid
31
+ accept protocol: :icmpv6, family: :ip6
32
+ end
33
+
34
+ output do
35
+ accept comment: "allow all router-originated traffic"
36
+ end
37
+
38
+ nat do
39
+ masquerade out_interface: "ether1-wan", comment: "branch nat"
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Layer A — portable address objects (hosts) and groups.
4
+ #
5
+ # A `host` is a named address object: one or more IPs / CIDR subnets / IP
6
+ # ranges. It compiles to entries in /ip firewall address-list (IPv4) and/or
7
+ # /ipv6 firewall address-list (IPv6); family is inferred per address.
8
+ #
9
+ # A `group` bundles hosts (and/or other groups) under one name. Groups have no
10
+ # native RouterOS equivalent (address-lists are not nestable), so they are
11
+ # FLATTENED at compile time into the union of their members' addresses.
12
+
13
+ # The management network — operators / CI run from here.
14
+ host "admin" do
15
+ address "10.0.0.0/24"
16
+ end
17
+
18
+ host "monitoring", address: "10.0.0.50"
19
+
20
+ # The CI runner that applies the firewall over the REST transport. It reaches
21
+ # the box from a different source than the human admins, so it needs its own
22
+ # management carve-out (see edge-1.rb) or an apply would lock the runner out.
23
+ host "ci", address: "198.51.100.10"
24
+
25
+ # Frontend tier (one host may hold several addresses).
26
+ host "web", address: ["10.0.10.5", "10.0.10.6"]
27
+ host "api", address: "10.0.10.20"
28
+
29
+ # Backend tier.
30
+ host "db", address: "10.0.20.10"
31
+ host "cache", address: "10.0.20.11"
32
+
33
+ # A host can join groups from its own side — trailing positional args after the
34
+ # name are group names (here "trusted-resolvers" is created on demand).
35
+ host "ext-dns", "trusted-resolvers", address: ["1.1.1.1", "8.8.8.8"]
36
+
37
+ # Group membership can also be declared group-side; both sides are unioned.
38
+ group "frontend" do
39
+ member "web"
40
+ member "api"
41
+ end
42
+
43
+ group "backend" do
44
+ member "db"
45
+ member "cache"
46
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Layer A — access grants and global chain defaults.
4
+ #
5
+ # Access grants are device-agnostic: the Compiler injects them into every
6
+ # managed device's FORWARD chain. `rule <source>` opens a block of `to` lines;
7
+ # each `to` is one grant to a destination, with an optional service (omitted =
8
+ # any) and an optional action (:allow by default, or :deny).
9
+
10
+ rule "frontend" do
11
+ to "db", "mysql" # frontend -> db over mysql (allow)
12
+ to "cache", "redis"
13
+ to "trusted-resolvers", "dns" # frontend -> upstream resolvers (tcp + udp)
14
+ end
15
+
16
+ rule "admin" do
17
+ to "frontend", :any # admins reach the frontend over anything
18
+ to "backend", "ssh"
19
+ end
20
+
21
+ # An explicit, logged deny. The action is the optional last positional arg, so
22
+ # to set it you must also give a service (or :any).
23
+ rule "monitoring" do
24
+ to "backend", "ssh", :deny, log: true, log_prefix: "mon-ssh-deny"
25
+ end
26
+
27
+ # Global chain defaults — the trailing default rule of each chain. Overridable
28
+ # per device inside the `device` block.
29
+ policy :forward, :drop
30
+ policy :input, :drop
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Layer A — named protocol/port definitions, referenced by name from rules.
4
+ #
5
+ # `protocol:` (singular) and `protocols:` (multi) are both accepted. A
6
+ # multi-protocol service (e.g. DNS = tcp + udp) compiles to ONE filter rule per
7
+ # protocol. Ports keep their spec form (Integer / Array / Range / "a-b"), so a
8
+ # range stays a range (dst-port=8000-8100) instead of exploding into a list.
9
+
10
+ service "https", protocol: :tcp, ports: [443]
11
+ service "http", protocol: :tcp, ports: [80]
12
+ service "ssh", protocol: :tcp, ports: [22]
13
+
14
+ service "mysql", protocol: :tcp, ports: [3306]
15
+ service "redis", protocol: :tcp, ports: [6379]
16
+
17
+ service "dns", protocols: %i[tcp udp], ports: [53] # -> two filter rules
18
+
19
+ service "web-highports", protocol: :tcp, ports: ["8000-8100"] # port range
data/exe/mt-wall ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "mt/wall"
5
+
6
+ exit(Mt::Wall::CLI.start(ARGV))