| name | slv-nftables |
| description | Deploy an OSS-clean nftables ruleset to any slv-managed host (Hermes, Pythnet RPC, validator, RPC, …). Inventory-driven — mgmt IPs, public ports, and per-port allow lists all come from `~/.slv/inventory.{network}.{role}.yml`, nothing is hardcoded. Atomic + rollback-safe (failed validation restores the previous config) and persistent across reboot. Used by `slv hermes firewall` and `slv pythnet firewall`; the same `cmn/deploy_nftables.yml` playbook works against any inventory. |
SLV nftables Skill
Inventory-driven nftables ruleset deployer. One playbook
(cmn/deploy_nftables.yml) serves every slv role — Hermes, Pythnet
RPC, Solana RPC, validators — by reading the firewall description from
the same inventory that already configures the service itself.
What it does
- Renders
/etc/nftables.conf + fragments under /etc/nftables.d/
and /etc/nftables.sets.d/ from Jinja2 templates.
- Validates the rendered config (
nft -c) before applying. If
validation fails, the previous config is restored and the playbook
aborts — you cannot lock yourself out via a bad template.
- Applies atomically (
nft -f).
- Seeds allowlist sets with IPs from inventory, preserving any
elements added at runtime with
nft add element … so re-running the
playbook never wipes manual additions.
- Exports the live ruleset back into
/etc/nftables.conf and enables
nftables.service so the same ruleset is restored on reboot.
Ruleset shape
chain input (default: drop)
├─ accept loopback
├─ accept established/related
├─ accept icmp / icmpv6
├─ banAll_v4 drop (highest priority)
├─ mgmt_ips_v4 accept (full access)
├─ allowAll_v4 accept (runtime additions, full access)
├─ mgmt_ips_v4 → tcp dport ssh_port accept (belt-and-suspenders)
├─ public_tcp_ports / public_udp_ports accept
└─ restricted_ports[]: allow_<port>_v4 accept else drop
Inventory variables
| Variable | Required | Default | Description |
|---|
mgmt_ips_v4 | yes | — | List of IPv4/CIDR with full access. Empty list aborts the playbook. |
mgmt_ips_v6 | no | [] | Same for IPv6 |
ban_ips_v4 | no | [] | Hard-deny list (evaluated first) |
ssh_port | no | 22 | SSH port to keep open from mgmt |
public_tcp_ports | no | [] | TCP ports/ranges accepted from any source |
public_udp_ports | no | [] | UDP ports/ranges accepted from any source |
restricted_ports | no | [] | Per-port allowlist (see below) |
disable_ufw | no | true | Stop/disable ufw to avoid conflict |
preserve_existing | no | true | Preserve runtime nft add element additions |
restricted_ports schema
restricted_ports:
- port: 8899
tcp: true
udp: false
allow_ipv4:
- "203.0.113.10"
- "198.51.100.0/24"
Set + counter names are derived from port automatically:
port | set | counter (allow / drop) |
|---|
8899 | allow_8899_v4 | c_8899_allow / c_8899_drop |
"8000-8020" | allow_8000_8020_v4 | c_8000_8020_allow / c_8000_8020_drop |
Runtime management
sudo nft add element inet filter allowAll_v4 { 198.51.100.42 }
sudo nft add element inet filter allow_8899_v4 { 203.0.113.10 }
sudo nft delete element inet filter allowAll_v4 { 198.51.100.42 }
sudo nft list set inet filter allow_8899_v4
sudo nft list counter inet filter c_8899_drop
sudo nft list counter inet filter c_8899_allow
Runtime changes survive reboot only after the next playbook run,
which re-exports the live ruleset back into /etc/nftables.conf. If
you need an addition to persist immediately, run the playbook again
(it's idempotent and preserves runtime state by design).
CLI Command ↔ Playbook Mapping
| CLI Command | Inventory | Description |
|---|
slv hermes firewall | mainnet_hermes | Apply to Hermes hosts |
slv pythnet firewall | mainnet_pythnet | Apply to Pythnet RPC hosts |
The playbook itself (cmn/deploy_nftables.yml) is role-agnostic.
Adding a slv <other> firewall subcommand is one TypeScript dispatch
- an
InventoryType variant — see how slv hermes firewall is wired
in cli/src/hermes/index.ts.
Example: Hermes node (public API)
all:
hosts:
hermes-1:
ansible_host: "<vps-ip>"
ansible_user: "solv"
vars:
mgmt_ips_v4: ["<your-admin-ip>"]
public_tcp_ports: [7575]
public_udp_ports: [8999]
Example: Hermes node behind Cloudflare (:443 whitelist)
The slv templates ship cmn/files/cf_ipv4.yml containing the official
Cloudflare IPv4 ranges (refreshed against
https://www.cloudflare.com/ips-v4). The playbook auto-loads it via
include_vars if present, exposing the cf_ipv4 list to your
inventory:
all:
hosts:
hermes-1: { ansible_host: "<vps-ip>", ansible_user: "solv" }
vars:
mgmt_ips_v4: ["<your-admin-ip>"]
public_tcp_ports: [7575]
public_udp_ports: [8999]
restricted_ports:
- port: 443
tcp: true
allow_ipv4: "{{ cf_ipv4 | default([]) }}"
Refresh the list any time CF updates its ranges — edit
template/<version>/jinja/cmn/files/cf_ipv4.yml, re-run
slv hermes deploy (or just re-copy the file into ~/.slv/cmn/files/),
then re-run slv hermes firewall.
Example: Pythnet RPC (restricted JSON-RPC + public gossip)
all:
hosts:
pythnet-1:
ansible_host: "<bm-ip>"
ansible_user: "solv"
vars:
mgmt_ips_v4: ["<your-admin-ip>"]
public_tcp_ports: [8001, "8000-8020"]
public_udp_ports: [8001, "8000-8020"]
restricted_ports:
- port: 8899
tcp: true
allow_ipv4: ["<hermes-vps-ip>"]
- port: 8900
tcp: true
allow_ipv4: ["<hermes-vps-ip>"]
Safety properties
- Lockout-resistant.
mgmt_ips_v4 empty → playbook aborts before
any change.
- Atomic.
nft -c validates before nft -f applies.
- Rollback. Validation failure restores
/etc/nftables.conf.bak.
- Idempotent. Re-runs preserve runtime allowlist additions.
- Reboot-survivable. Live ruleset exported back to
/etc/nftables.conf; nftables.service enabled.
Known gotchas
- UFW conflicts.
disable_ufw: true (default) stops + disables
ufw before applying. If you have a custom ufw setup you want to
keep, set disable_ufw: false — but at that point you should
probably not be using nftables directly anyway.
ssh_port ≠ inventory's ansible_port. If you SSH on a
non-default port, set ssh_port to match — the playbook only
carves out the value you pass it.
- Set names with hyphens are illegal in nftables. We derive set
names from
port by regex_replace('[^0-9]+', '_'), so any range
string ("8000-8020") becomes a valid identifier (allow_8000_8020_v4).