nftui is a Terminal User Interface for managing nftables on Linux. Browse
the live ruleset, edit rules with full structured editors for every condition
and action type, and apply changes back to the kernel — without ever touching
the nft CLI directly.
Built in Go with the Bubble Tea
framework. Talks to the kernel over netlink via the
google/nftables library.
- Tree view of all tables and chains with live data fetched from the kernel.
The skeleton (tables, chains, sets, named objects) renders immediately on
startup; per-chain rule counts fill in asynchronously, so a ruleset with
many chains stays interactive while the rule lists arrive in the
background (each chain row briefly shows
[loading rules...]until its fetch lands). - Per-chain rule listing with human-readable rendering of every parsed
expression. The list is windowed — only the rules that fit on screen are
serialized and drawn, so a chain with 1000+ rules costs the same to scroll
as one with 10. The inline filter (
/) caches each rule's lowercase haystack the first time it's matched, so subsequent keystrokes stay responsive on large chains. - Per-rule detail view organised into tabs by condition category.
- Full CRUD on tables, chains and rules: create, rename / edit properties, delete (with confirmation), reorder rules up / down within a chain, insert before / append at end.
| Category | Matches |
|---|---|
| CT (conntrack) | state, direction, status, mark, secmark, expiration, helper, l3proto, protocol, proto-src, proto-dst, labels, eventmask, ip saddr / daddr, bytes, packets, avgpkt (with direction), zone, count |
| IPv4 header | saddr, daddr (CIDR), protocol, ttl, length, dscp, version, hdrlength, id, frag-off, checksum |
| IPv6 header | saddr, daddr (CIDR), length, nexthdr, hoplimit, version, dscp (6-bit), flowlabel (20-bit) |
| TCP | sport, dport, sequence, ackseq, flags (MultiSelect), window, checksum, urgptr, doff |
| UDP / UDPLITE | sport, dport, length, checksum |
| SCTP | sport, dport, vtag, checksum, chunk (RFC 4960 chunk-type match: data / init / init-ack / sack / heartbeat / heartbeat-ack / abort / shutdown / shutdown-ack / error / cookie-echo / cookie-ack / ecne / cwr / shutdown-complete / auth / asconf-ack / i-data / forward-tsn / asconf / i-forward-tsn — bare presence and per-type sub-field constraints both supported: chunk-type Select drives a sub-field Select (tsn / stream / ssn / ppid for DATA; init-tag / a-rwnd / os / mis / init-tsn for INIT; cum-tsn-ack / a-rwnd / num-gap-ack-blocks / num-dup-tsns for SACK; etc.) plus a value input that BE-encodes into the matching 1 / 2 / 4 byte width) |
| Meta (interface) | iifname, oifname, iif, oif, iiftype, oiftype, iifgroup, oifgroup |
| Meta (proto / socket / packet) | length, protocol (EtherType), nfproto, l4proto, mark, priority, skuid, skgid, cgroup, rtclassid, pkttype, cpu |
- Verdicts:
accept,drop,return,jump <chain>,goto <chain>— with chain name input for jump / goto targets. - Reject:
with icmp type,with icmpx type,with tcp reset— family- aware (the ICMP type Select changes for ip / ip6 / inet / bridge tables). - Log: prefix, level (emerg…debug), NFLOG group, snaplen, queue-threshold —
with pre-save validation against kernel-rejected combinations (e.g.
levelforbidden in NFLOG mode). - Counter: edit packet and byte counts on an anonymous counter (typical use is reset to 0).
- Limit: rate, unit (second/minute/hour/day/week), burst, type (packets/bytes), over.
- Each tab groups related fields; Tab / Shift+Tab moves the focus between sub-inputs.
- Modified fields are highlighted; cleared inputs remove the underlying match.
- F2 validates and applies all changes through netlink (
NLM_F_REPLACE). - A footer help line always lists every available key binding in the current view.
- Linux with a kernel that has
nftablessupport. - Go 1.25+ to build from source.
CAP_NET_ADMINat runtime (run viasudoor grant the capability withsetcap).- A terminal at least 80x24 characters. Below that nftui shows a resize prompt instead of a cramped layout.
The runtime does not require the nft CLI for the core read / edit /
write path — communication is direct over netlink. The nft binary is only
used by a few targeted operations where round-tripping through the CLI is
safer than reconstructing kernel state (table rename, base-chain
recreation).
git clone https://github.com/aafeher/nftui.git
cd nftui
go build -o nftui .Each release attaches native
packages for amd64 and arm64, all built from the same binary as the
archives and listed in checksums.txt (so the cosign signature covers them):
| Format | Distros | Install |
|---|---|---|
.deb |
Debian / Ubuntu | sudo apt install ./nftui_<ver>_linux_amd64.deb |
.rpm |
Fedora / RHEL / openSUSE | sudo dnf install ./nftui_<ver>_linux_amd64.rpm |
.apk |
Alpine | sudo apk add --allow-untrusted ./nftui_<ver>_linux_amd64.apk |
.pkg.tar.zst |
Arch | sudo pacman -U ./nftui_<ver>_linux_amd64.pkg.tar.zst |
.ipk |
OpenWrt (opkg) | opkg install ./nftui_<ver>_linux_amd64.ipk |
Every package installs nftui to /usr/bin, the man page to
/usr/share/man/man1, and declares the nftables runtime dependency. The
binaries are static (CGO-free), so they run on glibc and musl systems alike.
OpenWrt is migrating from opkg to apk, so on matching architectures the
.apk should serve newer apk-based OpenWrt while the .ipk covers the existing
opkg releases. Routers on other architectures (mips, armv7) are out of scope —
build from source there.
Arch / AUR: the release .pkg.tar.zst installs directly with pacman -U,
no AUR needed. nftui does not publish to the AUR itself; a community maintainer
is welcome to adopt the reference packaging/aur/PKGBUILD
(a -bin package over the release tarball).
Gentoo: the repo is a standard Go module, so go build -o nftui . is the
simplest path. Two community-maintainable reference ebuilds are provided for a
local overlay: nftui-1.1.0.ebuild
builds from source via go-module.eclass, and
nftui-bin-1.1.0.ebuild installs the
prebuilt release binary; install one or the other (they share /usr/bin/nftui
and block each other). See
packaging/gentoo/README.md for overlay setup.
nftui does not maintain a Portage / GURU entry.
The repository ships a flake.nix with a buildGoModule package
for x86_64-linux and aarch64-linux, a devShell that mirrors the CI
toolchain, and a runnable apps.default:
nix build # builds into ./result/bin/nftui (+ man page)
nix run # builds and runs (needs CAP_NET_ADMIN at runtime)
nix develop # toolchain: go, gopls, goreleaser, nftables, mandocOn the first nix build, the vendorHash is intentionally set to
lib.fakeHash — Nix prints the real sha256-… in the error and the user
pastes it into flake.nix (re-pin whenever go.sum changes). This keeps
binary releases (Goreleaser) and Nix builds independent: the Nix path does
not block release publishing.
A Dockerfile builds a small (~17 MB) image that bundles the
nft(8) CLI nftui needs at runtime:
docker build -t nftui:local .
# versioned build (sets `nftui --version`):
docker build -t nftui:1.1.0 --build-arg VERSION=1.1.0 .nftui manages the host ruleset, so the container needs the host network
namespace, the NET_ADMIN capability, and an interactive TTY:
docker run --rm -it --network host --cap-add NET_ADMIN nftui:localFlags pass straight through, e.g. … nftui:local --read-only.
A docker-compose.yml wires the same options up. Use
run (not up) so the TUI gets a real TTY:
docker compose run --rm nftuiThe container runs as root and relies on --cap-add NET_ADMIN plus the
container boundary for isolation; with --network host it edits the host's
nftables — the same privilege footprint as running the binary on the host (see
Privilege model & deployment hardening).
Either with sudo:
sudo ./nftui…or grant the binary the required capability once:
sudo setcap cap_net_admin=ep ./nftui
./nftuisudo install -m 0644 man/nftui.1 /usr/share/man/man1/
sudo mandb # if your system uses man-db (Debian / Ubuntu / Fedora …)
man nftui # then it's available everywherePreview it from the source tree without installing:
man -l man/nftui.1| Flag | Description |
|---|---|
--table <name> |
Restrict the tree to a single table — its chains, sets, and named objects. The match is by name across every family, so --table filter will include both inet filter and ip filter if both exist. Unknown names exit before the TUI starts with the list of available tables. |
--config <file> |
Apply the given nftables ruleset via nft -f <file> before the TUI starts. This mutates the running ruleset — the file may contain flush ruleset. Use to bring up a known state for testing. Resolved before --table so the post-load kernel state is what --table validates against. |
--read-only |
Disable every write path: no rule add / insert / move / delete / edit / save, no chain / table / set create / delete, no counter reset. Blocked keys dim out of the footer (per the footer-completeness invariant) and a [READ-ONLY MODE] marker rides next to the title in every main view. Useful for safe browsing, auditing, or pairing with --config to inspect a fixture without risk of accidental edits. |
--help (also -h) |
Print the full flag list with one-line descriptions and usage examples, then exit. Goes to stdout (so you can pipe to less); explicit --help exits 0. Invalid flags emit the same usage to stderr and exit 2. |
--version |
Print nftui <version> to stdout and exit 0. The version is injected at release-build time; a binary built from source reports the Go build-info module version, or dev for a plain go build. |
Examples:
sudo ./nftui --table filter # show only table(s) named 'filter'
sudo ./nftui --table missing # exits: "table 'missing' not found. Available tables: …"
sudo ./nftui --config examples/example-nftables-01.conf # load the manual-test fixture, then browse it
sudo ./nftui --read-only # safe browsing — every write key is dimmed and inert
sudo ./nftui --config new.conf --table filter # apply new.conf, then restrict the view to its 'filter' table
./nftui --version # print the version and exit (no privileges needed)Without --config, the running ruleset is left untouched. Without --table, every table is shown. Without --read-only, every CRUD action is available.
nftui reads and writes the kernel's nftables ruleset over netlink, which needs
the CAP_NET_ADMIN capability. It has no authentication or authorization
of its own: any user who can launch nftui with that capability can rewrite the
firewall. nftui is therefore only as safe as the way you grant that privilege —
grant it too broadly and the binary becomes a confused deputy. Enforce access
at the OS layer. Two patterns are recommended.
Run nftui through sudo and limit who may do so. Create a dedicated group
(e.g. nftadm), add the trusted operators, and add a rule with visudo:
# /etc/sudoers.d/nftui (edit with: visudo -f /etc/sudoers.d/nftui)
# Let the nftadm group run nftui as root — and nothing else.
%nftadm ALL=(root) /usr/local/bin/nftui
- Use the absolute path so a different
nftuiearlier onPATHcan't be substituted. - Keep password prompts on (no
NOPASSWD) for interactive use:sudowrites an auth-log entry for every invocation, giving you a who-and-when record. - Operators then run
sudo nftui. nftui readsSUDO_USER, so with the audit log enabled, every applied change records the human behindsudo, not justroot.
For a read-only/browse role, grant a wider group the --read-only form only.
sudo matches the command and its arguments exactly, so this rule permits
sudo nftui --read-only but not the unrestricted sudo nftui:
%nftview ALL=(root) /usr/local/bin/nftui --read-only
If you must run without sudo (e.g. automation), grant the capability to the
file but restrict who can execute it — never leave it world-executable:
sudo chown root:nftadm /usr/local/bin/nftui
sudo chmod 750 /usr/local/bin/nftui # root: rwx, nftadm: r-x, others: none
sudo setcap cap_net_admin+ep /usr/local/bin/nftui- The capability rides with the file, not the user, so
chmod 755+setcapeffectively hands firewall-rewrite power to every local account.chmod 750with a dedicated group is what keeps it contained. - A
setcapbinary bypassessudo, so there is no sudo auth-log entry andSUDO_USERis empty — rely onNFTUI_AUDIT_LOGfor the change record (it still captures the real UID/user). - Keep the binary and its parent directories writable only by
rootso the capability-bearing file can't be swapped.
- Turn on the audit log (
NFTUI_AUDIT_LOG) so every mutation is attributed and timestamped — the OS controls who can run nftui; the audit log records what they changed. - Use
--read-onlyfor inspection/audit roles that should never mutate state. sudointegrates with PAM, so re-authentication, MFA, or time/host restrictions (pam_time,pam_access) are configured at the PAM layer — this is the "PAM wrapping" for nftui; the tool deliberately adds no access control of its own.
For change-management and compliance (e.g. SOC 2 / PCI-DSS), nftui can record
every ruleset mutation it applies. Set the NFTUI_AUDIT_LOG environment
variable to a writable file path:
sudo NFTUI_AUDIT_LOG=/var/log/nftui-audit.log ./nftuiWhen the variable is unset or empty, auditing is off and nftui behaves
exactly as before — there is no file I/O on the mutation path. When set, every
applied change (create / delete / rename table, chain and set; add / insert /
move / delete / edit rule; add / delete set element; delete / reset named
object; --config load; ruleset flush) appends one JSON object per line:
{"time":"2026-06-19T10:30:00.12Z","uid":0,"user":"root","sudo_user":"alice","op":"delete-rule","target":"ipv4 filter input handle 7","result":"ok"}Each record carries the UTC timestamp, the effective UID and user, the human
operator behind sudo (sudo_user, from SUDO_USER), the operation, the
target object, and the outcome (result is ok or error, with an error
field on failure — rejected attempts are logged too). Properties:
- Append-only — nftui only ever appends; it never rotates, truncates, or
reads the file back. Rotate it with
logrotateor ship the lines to a SIEM. - 0600 — the file is created owner-read/write only.
- Fail-open — if the path cannot be opened nftui prints one warning and continues without auditing; a broken audit path never blocks firewall management. Ensure the path is writable by the nftui process.
| Key | Action |
|---|---|
↑ / k |
move selection up |
↓ / j |
move selection down |
Enter / → / ← |
expand / collapse |
F3 |
open chain (rule list) |
n |
new table |
c |
new chain |
e |
edit selected table or chain |
d |
delete selected table or chain |
/ |
search |
r |
refresh from kernel |
q / Esc / Ctrl+C |
quit |
| Key | Action |
|---|---|
↑ / k |
move selection up |
↓ / j |
move selection down |
F3 |
view rule |
F4 |
edit rule |
a |
append rule at end |
i |
insert rule before selected |
K (Shift+k) |
move selected rule up |
J (Shift+j) |
move selected rule down |
d |
delete rule |
/ |
filter rules by substring (verdict, condition keyword, comment) |
Esc |
back |
q |
quit |
While the filter is active, ↑ / ↓ navigate the filtered list, Enter /
F3 open the selected rule for viewing, F4 opens the editor, and Esc
clears the filter.
| Key | Action |
|---|---|
F5 / F6 |
previous / next tab |
Tab / Shift+Tab |
next / previous field |
F2 |
save (validate + apply to kernel) |
Esc / F3 |
back |
q / Ctrl+C |
quit |
examples/example-nftables-01.conf is the canonical manual-test fixture. It
covers every feature documented above and is verified with nft -c -f against
the host kernel. For a realistic, good-practice starting point rather than a
feature showcase, examples/example-host-firewall.conf is a hardened
single-host firewall (default-deny inbound except SSH/HTTP/HTTPS, unrestricted
outbound, forwarding denied). Load either explicitly only on a system where
overwriting the nftables state is OK:
sudo nft -c -f examples/example-nftables-01.conf # syntax check
sudo nft flush ruleset # reset (DANGER on prod)
sudo nft -f examples/example-nftables-01.conf # apply
nftuiitself does not mutate the running ruleset on startup — it only reads the current kernel state and writes changes the user explicitly makes.
main.go program entry point
nft/ kernel-talking core
rule.go expression → Rule structure parser
nft_linux.go netlink CRUD operations (Linux build tag)
nft_stub.go no-op stubs for non-Linux builds
expr/ per-expression format helpers
nftserializer/ ruleset → human-readable output
ui/ Bubble Tea TUI
main_window.go top-level model (tree view)
chain_view.go rule list
rule_view.go rule detail (read-only)
rule_edit.go rule editor with tabbed FieldEditors
field_*.go one file per FieldEditor
examples/example-nftables-01.conf manual-test fixture
man/nftui.1 man page (groff/mandoc; see "Installation")
CHANGELOG.md per-version release notes (Keep a Changelog format)
go test ./... # unit tests (no kernel required)
sudo nft -c -f examples/example-nftables-01.conf # validate the fixtureTests under the integration build tag exercise the live netlink read and
write paths with the same helpers the TUI uses: applying a ruleset via nft -f
and reading it back, plus creating / renaming / deleting tables and chains and
adding / inserting / moving / deleting rules, asserting the kernel state read
back after each step. They're excluded from the default go test ./... and
skip themselves when not running as root, so a plain go test stays portable.
sudo -E go test -tags=integration ./nft/ -vEach test creates a uniquely-named table (timestamp-suffixed, so concurrent
runs and leftover state don't collide) and tears it down in t.Cleanup, even
when assertions fail. The nft binary must be on PATH; install it from the
nftables package on your distro if missing.
.github/workflows/ci.yml runs the same checks
on every push and pull request to main / develop:
- Build & unit tests —
gofmt -l,go vet ./...(default andintegrationbuild tags),go build ./..., andgo test -race ./.... - Integration tests — installs the
nftablespackage, then runssudo -E go test -tags=integration -v ./nft/so the harness has theCAP_NET_ADMINit needs to apply a live ruleset. Writes a coverage profile over thenfttree (-coverpkg=./nft/...) and prints the total in the job log — the live netlink path is invisible to the unit-test profile, so this is where its coverage is observable. Runs only after the unit-test job is green. - Vulnerability scan — runs
govulncheck ./...against the module and the Go standard library. As its own check (parallel to the build), it fails the run only when a known vulnerability is reachable from nftui's call graph. - Reproducible build check — builds the release binaries twice with
goreleaser build --snapshotand fails if the two differ, verifying themod_timestamp/-trimpath/ CGO-free build is byte-for-byte reproducible. - Nix flake build — on a Nix runner,
nix flake check+nix build .#defaultbuildflake.nixend-to-end (compiling nftui and running its unit suite in the sandbox), so the flake can't silently break. The first run must pinflake.nix'svendorHash— it ships as a placeholder and the failing build prints the real value to paste in.
Dependency and GitHub-Actions updates are automated with Dependabot
(.github/dependabot.yml, weekly), which opens PRs as upstream releases and
security fixes land. github.com/google/nftables is excluded from those PRs
because it is intentionally held at a pinned snapshot.
The Go version comes from go.mod via actions/setup-go@v6 with
go-version-file: go.mod, so bumping the module's Go version updates CI in
the same commit. Concurrent runs on the same ref cancel earlier in-flight
runs (cancel-in-progress: true).
Releases are driven by Goreleaser and a tag-trigger
workflow (.github/workflows/release.yml):
- Promote the
[Unreleased]section inCHANGELOG.mdto[X.Y.Z] - <date>. git tag vX.Y.Zandgit push --tags.- The Release workflow extracts the matching
[X.Y.Z]section fromCHANGELOG.md, then runs Goreleaser, which builds reproducible Linuxamd64/arm64binaries (CGO_ENABLED=0 -trimpath -ldflags='-s -w',mod_timestamppinned to the commit time), bundles each withLICENSE,README.md,CHANGELOG.md, andman/nftui.1into atar.gz, also emits.deb/.rpm/.apk/ Arch.pkg.tar.zst/ OpenWrt.ipkpackages (nfpm, same binary), writes a SHA-256checksums.txtcovering every artifact, and publishes the GitHub Release with the curated notes as the body. - The release is hardened with supply-chain attestation:
checksums.txtis signed with cosign (keyless — the signature is bound to the workflow's OIDC identity via Fulcio/Rekor, no stored private key), a Syft SBOM is emitted per archive, and a SLSA build-provenance attestation is recorded for the archives, checksums, and the dependency tarball below. - A reproducible
nftui-<X.Y.Z>-deps.tar.xz(the Go module cache, fromscripts/gen-deps-tarball.sh) is uploaded for offline source builds — chiefly the Gentoo source ebuild, whosego-module.eclassforbids network access at build time. Its contents are pinned bygo.sum, so it rides on the build-provenance attestation rather thanchecksums.txt(already signed).
Verifying a downloaded release:
# 1. signature over the checksum file (keyless cosign). Pin the signer to this
# repo's release workflow AND GitHub's OIDC issuer — a wildcard identity/issuer
# ('.*') only proves the signature is internally valid, not that *we* produced
# it, so it would accept a signature from any Fulcio identity and defeat the
# purpose of keyless verification.
cosign verify-blob --certificate checksums.txt.pem --signature checksums.txt.sig \
--certificate-identity-regexp '^https://github\.com/aafeher/nftui/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
checksums.txt
# 2. the archive against the trusted checksums
sha256sum --check --ignore-missing checksums.txt
# 3. build provenance (binds the bytes to this repo's release workflow)
gh attestation verify nftui_<ver>_linux_amd64.tar.gz --repo <owner>/nftuiTo validate the config locally without publishing:
goreleaser check # config syntax only
goreleaser release --snapshot --clean --skip=publish,sign,sbom # build into dist/sign / sbom are skipped locally because they need the CI runner's cosign
OIDC identity and syft; the provenance attestation is workflow-only. Snapshot
output (dist/) is gitignored, so the working tree stays clean.
Per-version release notes live in CHANGELOG.md in Keep a Changelog format. Major milestones to date:
- v0.1.0 (2026-05-24) — first publishable release: full CT / meta / IP / port matches, every verdict action, full ruleset CRUD.
- v0.2.0 (2026-05-24) — NAT statements (
snat,dnat,masquerade),queue,quota. - v0.3.0 (2026-05-24) — extended protocol matches (ICMP / ICMPv6, SCTP, DCCP, AH, ESP, COMP, Ethernet, VLAN, ARP, IPv6 extension headers).
- v0.4.0 (2026-05-24) — sets, maps and named objects.
- v0.5.0 (2026-05-25) — sets / maps / named objects polish & hardening (interval-set delete fix, dynset flag, CIDR support, verdict maps).
- v0.6.0 (2026-05-29) — feedback-channel consistency and transient-hint UX: auto-fading tree hints, unified Reset / Delete error routing.
- v0.7.0 (2026-05-29) — error messaging (
CAP_NET_ADMINadvice, rejected-rule display) and navigation (/search in the tree,/filter inchainView). - v0.8.0 (2026-05-30) — CLI flags (
--table,--config,--read-only,--help), release polish (CHANGELOG, man page),sctp chunkeditor, async incremental loading. - v0.9.0 (2026-06-19) — release infrastructure (integration test harness, CI
workflow, virtualized rule list, Goreleaser release pipeline, Nix flake
packaging) plus an enterprise-readiness hardening pass: supply-chain
attestation (cosign / SBOM / SLSA provenance), CI vulnerability scanning, an
optional mutation audit log, defense-in-depth identifier validation, and
governance & deployment docs (
SECURITY.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md). - v1.0.0 (2026-06-20) — first stable release: broadened install paths
(Debian / RPM, Alpine / Arch / OpenWrt packages, a Docker image, community
Gentoo / AUR references), proven reproducibility and Nix-flake CI lanes, the
--versionflag, a Go-module dependency tarball for offline source builds, and IPv6 source / destination address rendering. - v1.1.0 (2026-06-21) — terminal-fit & navigation UX plus a security/CI hardening wave: an 80x24 minimum with frame clamping and a resize prompt, alternate-screen rendering, scroll-to-focus in the rule editor and scrolling in the rule view, a compact chain header; fixes for quit flushing the ruleset and rules rendering twice; OpenSSF Scorecard / CodeQL / Codecov, Go fuzz targets, and SHA-pinned actions.
MIT — see LICENSE.