mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 16:56:33 +00:00
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.
This commit is contained in:
parent
0fa797adfa
commit
d149ed68e5
6 changed files with 194 additions and 0 deletions
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
|
@ -197,6 +197,14 @@ jobs:
|
|||
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/rauc-system.conf
|
||||
cp deploy/rauc/betterframe-rauc-boot.sh \
|
||||
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
|
||||
# Firewall ruleset
|
||||
cp deploy/nftables/nftables.conf \
|
||||
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/nftables.conf
|
||||
# First-boot password rotation
|
||||
cp deploy/systemd/betterframe-firstboot.service \
|
||||
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
|
||||
cp deploy/systemd/betterframe-firstboot.sh \
|
||||
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
|
||||
# CA cert is operator-supplied — generated locally via
|
||||
# scripts/gen-rauc-signing-keys.sh and committed at
|
||||
# deploy/rauc/ca-cert.pem. Without it the image installs but
|
||||
|
|
|
|||
75
deploy/nftables/nftables.conf
Normal file
75
deploy/nftables/nftables.conf
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
#!/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;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,3 +17,4 @@ wlr-randr
|
|||
ca-certificates
|
||||
rauc
|
||||
dosfstools
|
||||
nftables
|
||||
|
|
|
|||
|
|
@ -45,6 +45,21 @@ else
|
|||
fi
|
||||
install -m 755 /tmp/bf-files/betterframe-rauc-boot.sh /usr/local/sbin/betterframe-rauc-boot.sh
|
||||
|
||||
# --- Firewall: default-drop inbound, LAN-only kiosk local API ---
|
||||
# Replaces any default nftables config Debian ships.
|
||||
install -m 644 /tmp/bf-files/nftables.conf /etc/nftables.conf
|
||||
systemctl enable nftables.service
|
||||
|
||||
# --- First-boot password rotation ---
|
||||
# Replaces the image-default bfadmin password with a per-device random one
|
||||
# the first time the kiosk boots. Stored at /etc/betterframe/admin-password
|
||||
# (0400 root) AND printed to tty1 banner once. See script for details.
|
||||
install -m 644 /tmp/bf-files/betterframe-firstboot.service \
|
||||
/etc/systemd/system/betterframe-firstboot.service
|
||||
install -m 755 /tmp/bf-files/betterframe-firstboot.sh \
|
||||
/usr/local/sbin/betterframe-firstboot.sh
|
||||
systemctl enable betterframe-firstboot.service
|
||||
|
||||
# Default env file — operator may edit on first boot to point at their server.
|
||||
cat > /etc/default/betterframe-kiosk <<'EOF'
|
||||
# Runtime env for betterframe-kiosk. Edit and `systemctl restart betterframe-kiosk`.
|
||||
|
|
|
|||
19
deploy/systemd/betterframe-firstboot.service
Normal file
19
deploy/systemd/betterframe-firstboot.service
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[Unit]
|
||||
Description=BetterFrame first-boot provisioning (rotate default password)
|
||||
Documentation=https://github.com/BetterCorp/BetterFrame
|
||||
ConditionPathExists=!/var/lib/betterframe/.firstboot-complete
|
||||
DefaultDependencies=no
|
||||
After=local-fs.target
|
||||
Before=getty.target multi-user.target betterframe-kiosk.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/sbin/betterframe-firstboot.sh
|
||||
StandardOutput=tty
|
||||
StandardError=tty
|
||||
TTYPath=/dev/tty1
|
||||
TTYReset=no
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
76
deploy/systemd/betterframe-firstboot.sh
Executable file
76
deploy/systemd/betterframe-firstboot.sh
Executable file
|
|
@ -0,0 +1,76 @@
|
|||
#!/bin/bash
|
||||
# Runs ONCE per device on first boot, before betterframe-kiosk starts.
|
||||
# Purpose: replace the image's baked-in bfadmin password with a per-device
|
||||
# random one so a leaked image (or shared install) doesn't share creds
|
||||
# across kiosks.
|
||||
#
|
||||
# Outputs:
|
||||
# /etc/betterframe/admin-password (mode 0400, root) — plaintext for now
|
||||
# /run/betterframe/firstboot-banner — printed to tty1 so an HDMI-attached
|
||||
# operator can read the new password
|
||||
# once. Cleared on next boot.
|
||||
# /var/lib/betterframe/.firstboot-complete — marker, blocks re-run.
|
||||
#
|
||||
# Future: post the password (encrypted with cluster_key) to the BF server
|
||||
# via the heartbeat so admins can fetch it from the kiosk detail page.
|
||||
# Until that's wired the file + tty banner are the only ways out, and the
|
||||
# operator is expected to read the banner once or pull the file via the
|
||||
# HDMI-attached console.
|
||||
set -euo pipefail
|
||||
|
||||
MARKER_DIR="/var/lib/betterframe"
|
||||
MARKER="${MARKER_DIR}/.firstboot-complete"
|
||||
PASSWORD_FILE="/etc/betterframe/admin-password"
|
||||
BANNER_DIR="/run/betterframe"
|
||||
BANNER="${BANNER_DIR}/firstboot-banner"
|
||||
|
||||
if [ -f "$MARKER" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 32 chars of base64 ≈ 24 bytes entropy. tr away ambiguous glyphs so the
|
||||
# operator transcribing from a TV screen has a fighting chance.
|
||||
NEW_PW="$(LC_ALL=C tr -dc 'A-HJ-NP-Za-km-z2-9' < /dev/urandom | head -c 24)"
|
||||
|
||||
mkdir -p "$MARKER_DIR"
|
||||
mkdir -p "$(dirname "$PASSWORD_FILE")"
|
||||
mkdir -p "$BANNER_DIR"
|
||||
|
||||
# Apply the new password atomically.
|
||||
printf 'bfadmin:%s\n' "$NEW_PW" | chpasswd
|
||||
|
||||
# Persist for first-time retrieval. 0400 root-only so bfkiosk + bfadmin
|
||||
# can't peek at each other's creds. Operator who needs it can `sudo cat`.
|
||||
umask 077
|
||||
printf '%s\n' "$NEW_PW" > "$PASSWORD_FILE"
|
||||
chmod 0400 "$PASSWORD_FILE"
|
||||
chown root:root "$PASSWORD_FILE"
|
||||
|
||||
# Banner printed to tty1 so the operator can read it once before the kiosk
|
||||
# takes over the screen. cage masks /dev/tty1 when betterframe-kiosk
|
||||
# starts, so the banner is only visible during the boot window.
|
||||
cat > "$BANNER" <<EOF
|
||||
|
||||
================================================================
|
||||
BetterFrame first-boot
|
||||
----------------------------------------------------------------
|
||||
bfadmin password rotated from the image default.
|
||||
|
||||
New password: ${NEW_PW}
|
||||
|
||||
Stored at: ${PASSWORD_FILE} (sudo cat to retrieve later)
|
||||
This banner only shows once. Write it down or photograph it.
|
||||
================================================================
|
||||
EOF
|
||||
|
||||
# Echo to tty1 directly. systemd's TTYPath ties stdout to tty1 too but
|
||||
# during early boot output can race; this is a belt+suspenders.
|
||||
if [ -w /dev/tty1 ]; then
|
||||
cat "$BANNER" > /dev/tty1 || true
|
||||
fi
|
||||
|
||||
# Marker last so a crash mid-script doesn't leave us with a half-applied
|
||||
# state that blocks the next boot's rerun.
|
||||
touch "$MARKER"
|
||||
|
||||
exit 0
|
||||
Loading…
Reference in a new issue