Skip to content
Back to skills

Slv Nftables

ASecurity

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.

  • 89 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 26, 2026
securitytypescriptgobashnodeapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add ValidatorsDAO/slv --skill slv-nftables --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Slv Nftables?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Slv Nftables
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/validatorsdao-slv-nftables/badge)](https://www.skillsdirectory.com/skills/validatorsdao-slv-nftables)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

1. Renders `/etc/nftables.conf` + fragments under `/etc/nftables.d/`
   and `/etc/nftables.sets.d/` from Jinja2 templates.
2. 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.
3. Applies atomically (`nft -f`).
4. 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.
5. 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

```yaml
restricted_ports:
  - port: 8899              # int or range string ("8000-8020")
    tcp: true               # default true
    udp: false              # default 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

```bash
# Add an IP to an allow list (effective immediately, no playbook re-run)
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 }

# Remove
sudo nft delete element inet filter allowAll_v4 { 198.51.100.42 }

# Inspect
sudo nft list set inet filter allow_8899_v4

# Counter check (how many packets dropped / accepted per port)
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)

```yaml
all:
  hosts:
    hermes-1:
      ansible_host: "<vps-ip>"
      ansible_user: "solv"
  vars:
    mgmt_ips_v4: ["<your-admin-ip>"]
    public_tcp_ports: [7575]         # REST/WS API
    public_udp_ports: [8999]         # Wormhole P2P
```

## 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:

```yaml
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)

```yaml
all:
  hosts:
    pythnet-1:
      ansible_host: "<bm-ip>"
      ansible_user: "solv"
  vars:
    mgmt_ips_v4: ["<your-admin-ip>"]
    public_tcp_ports: [8001, "8000-8020"]   # gossip + TPU
    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`).

Files in this skill

  • SKILL.md7.1 KB
  • examples/inventory.yml1.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.