BetterFrame/deploy/nftables/nftables.conf

76 lines
2.6 KiB
Text
Raw Permalink Normal View History

feat(harden): nftables default-drop firewall + first-boot password rotation Two image-side hardening pieces both small enough to ship together. deploy/nftables/nftables.conf — single ruleset installed at /etc/nftables.conf. Default-drop input. Allowed: loopback, established/related, ratelimited ICMP, kiosk local API :18090 from RFC1918 / RFC4193 / link-local sources only. SSH stays gated by sshd-disabled (image build sets enable-ssh: 0 and 01-run-chroot masks it); the firewall rule for :22 is left commented in for triage scenarios. Forward dropped. Output left wide open — kiosk needs to dial out to arbitrary RTSP cameras + the BF server (which may live on the public internet) without explicit allowlisting. deploy/systemd/betterframe-firstboot.{service,sh} — runs once per device before betterframe-kiosk starts. Generates a 24-char unambiguous-glyph password, applies via chpasswd, stores at /etc/betterframe/admin-password (0400 root), and prints a banner to tty1 so an HDMI-attached operator can transcribe it during the boot window before cage takes over the screen. Marker at /var/lib/betterframe/.firstboot-complete prevents re-run on subsequent boots. Without this, every kiosk built from the same image shipped with bfadmin:betterframe — a single password leak compromises the entire fleet. Future follow-up: post the rotated password (encrypted with cluster_key) to the BF server via heartbeat so admin UI can surface it. Not in this commit; the local file + tty banner are the only retrieval paths today.
2026-05-21 09:18:28 +00:00
#!/usr/sbin/nft -f
# BetterFrame kiosk firewall. Default-drop inbound; only LAN can reach the
# kiosk local HTTP API. Outbound stays wide open — kiosk needs to pull RTSP
# from arbitrary cameras + reach the BF server (which may be on the public
# internet) without explicit allowlisting.
#
# Reload after edits: sudo nft -f /etc/nftables.conf
flush ruleset
table inet betterframe {
# --- helper sets ----------------------------------------------------------
set rfc1918_v4 {
type ipv4_addr
flags interval
elements = {
10.0.0.0/8,
172.16.0.0/12,
192.168.0.0/16,
169.254.0.0/16 # link-local; on by default for DHCP fallback discovery
}
}
set rfc4193_v6 {
type ipv6_addr
flags interval
elements = {
fc00::/7,
fe80::/10 # link-local
}
}
# --- input chain ----------------------------------------------------------
chain input {
type filter hook input priority filter; policy drop;
# Sanity: existing flows always pass.
ct state established,related accept
ct state invalid drop
# Loopback — needed for any IPC, healthchecks, local-only services.
iifname "lo" accept
# ICMP for diagnostics. Rate-limited so we don't help amplification.
ip protocol icmp limit rate 4/second accept
ip6 nexthdr icmpv6 limit rate 4/second accept
# SSH never opens at the firewall — image build sets `enable-ssh: 0` and
# disables/masks sshd. If you intentionally re-enable it for triage,
# uncomment the next line and lock it down further:
# tcp dport 22 ip saddr @rfc1918_v4 accept
# Kiosk local HTTP API (:18090) — LAN-only. Lets house-side automation
# hit `/local/layout/<id>` + `/local/snapshot/<id>` via the local_key.
# Public-internet traffic is dropped here so an exposed-port misconfig
# downstream (e.g. an upstream NAT/UPnP slip) doesn't open it to the
# world.
tcp dport 18090 ip saddr @rfc1918_v4 accept
tcp dport 18090 ip6 saddr @rfc4193_v6 accept
# Everything else: drop (policy already covers this; explicit for clarity
# so future edits can insert above this comment without changing policy).
# counter drop # uncomment when debugging which packets are getting blocked
}
# --- forward / output -----------------------------------------------------
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
# Kiosk is the initiator for everything (RTSP pulls, BF server REST,
# firmware downloads). Allow all outbound.
type filter hook output priority filter; policy accept;
}
}