| Version | Supported |
|---|---|
| 2.2.x | Yes |
| 2.1.x | Yes |
| 2.0.x | Yes |
| 1.5.x | No |
| < 1.5 | No |
Use GitHub Private Vulnerability Reporting to report vulnerabilities privately. Do not open public issues for security reports.
The following are documented security-relevant design decisions, not bugs:
The passphrase is stored in /etc/fiero-hotspot.conf (mode 640 root:<target_user>), so the target user can read it.
At runtime it is handed to create_ap through /run/fiero-hotspot/create_ap.conf (mode 0600, root only), which is deleted as soon as the AP is up. It does not appear in the process table.
/run/fiero-hotspot/ itself is mode 0755 so the unprivileged event watcher can read the state/events IPC files. This is safe only because the sensitive children carry their own modes: create_ap.conf is written under umask 077 and is 0600 root-only, and the snapshot/ subdirectory is installed 0700 (install -d -m 0700) so the target user cannot read the saved ip_forward value, the iptables-save ruleset, or the NetworkManager configuration backup. A world-readable parent directory does not weaken a 0600/0700 child.
Exception: create_ap parses its config file with read without -r, which drops backslashes and trims surrounding spaces. The installer rejects such passphrases. If an older config contains one, Fiero-Hotspot falls back to passing it as a command-line argument (visible in /proc/$PID/cmdline to local users while the hotspot is active) and logs a warning. Mitigation: re-run install.sh with a different passphrase, or mount /proc with hidepid=2.
The service runs as root to perform NAT routing via iptables and network configuration. With the systemd sandboxing in fiero-hotspot.service it has an exposure score of 3.4 OK (previously 4.2; systemd-analyze security fiero-hotspot.service), now with NoNewPrivileges=true and RestrictRealtime=true active. Capabilities are bounded to CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID. CAP_SETGID/CAP_SETUID are retained solely because dnsmasq drops to the nobody group/user — without them it aborts with failed to change group-id to nobody: Operation not permitted and create_ap tears the AP down. Nothing in Fiero-Hotspot itself uses them. CAP_KILL, CAP_DAC_OVERRIDE, CAP_SYS_RESOURCE, CAP_CHOWN and CAP_AUDIT_WRITE remain fully dropped. The daemon no longer switches users or opens PAM sessions: user-facing desktop notifications are emitted by the unprivileged fiero-prompt watch daemon, which reads the world-readable IPC files (/run/fiero-hotspot/state, /run/fiero-hotspot/events, mode 0644 in a mode 0755 directory) and calls notify-send from the user's own session.
Three non-blocking flock locks serialise the long-running components, each on a distinct file descriptor:
| FD | Path | Held by | Protects |
|---|---|---|---|
9 |
/run/fiero-hotspot.lock |
daemon, entire lifetime | two daemons racing on one interface. Only start takes it; stop, status, clients, mode do not |
8 |
$XDG_RUNTIME_DIR/fiero-prompt.watch.lock |
watcher, entire lifetime | exactly one event watcher per user session |
9 |
$XDG_RUNTIME_DIR/fiero-prompt.lock |
prompt, debounce step only | the AC-state read/compare/write. Deliberately not held while a notification is on screen |
All three use flock -n, so a duplicate invocation exits 0 immediately rather than blocking. stop deliberately does not take the instance lock, so a manual stop can overlap a starting daemon; that case is coordinated by the stopping flag file and the PID file instead.
Descriptor scoping is load-bearing. Both create_ap launch paths pass 9>&-, closing the lock descriptor in the child. Without it, a surviving create_ap keeps the lock held after the daemon dies and every later start falsely reports "Another instance is running". The watcher's tail coprocess is spawned with 8>&- for the same reason.
The watcher runs tail -n0 -F as a coprocess rather than the left side of a pipeline specifically so it can be killed. With tail | while, a vanished D-Bus socket only ends the reader: tail keeps blocking on the idle event file, the shell stays in the pipeline wait holding fd 8, and the lock is never released — so no new watcher could ever start.
A PID read from a file is never trusted on its own. is_create_ap_pid applies four gates before the daemon will signal, snapshot, or tear down a process:
- the value is numeric;
/proc/$1exists;/proc/$1/statstate is notZ(a zombie is dead — signalling it does nothing and treating it as live would hang the teardown);/proc/$1/cmdlinematches(^|/)create_ap( |$), anchoring both ends.
The final gate is what defeats PID reuse: a recycled PID now owned by some unrelated process fails the create_ap match and is ignored entirely. The same predicate guards the pre-launch snapshot, so a stale create_ap.pid can never cause the daemon to snapshot the wrong process's state. Command-line matching is deliberately anchored rather than a bare substring, so a process merely mentioning create_ap — such as the daemon's own pgrep -f "create_ap..." probe — is not mistaken for the AP.
create_ap runs hostapd and dnsmasq as children. Teardown must reap those children without touching anything else on the machine, and it does so by process tree, not by command line.
descendant_pids performs a breadth-first walk over /proc parent-child edges (pgrep -P) from the create_ap PID. Two properties make it safe:
- The tree is collected while the parent is still alive. Once
create_apdies its children are reparented to init and become unreachable by parent, so collecting afterwards would find nothing and leakhostapd/dnsmasq. Capture happens before theUSR1. - It matches PIDs, not command lines. Walking
/proccan only ever reach processes that are genuinely descendants in our own tree and namespace. It cannot reach ahostapdbelonging to anothercreate_apinstance, nor a process in another mount namespace — both of which apkill -f hostapdwould happily destroy.
The collected tree is reaped with TERM, a 1 second pause, then KILL. When nothing in the tree is alive — the common clean-exit case — the function returns without adding any latency.
- Config generation uses
shquote()ininstall.shfor all ten written values, single-quote escaping so that$, backticks, backslashes and embedded quotes in an SSID cannot execute at source time. - Event messages are sanitised with
tr '\n|' ' 'before being appended, replacing newlines and pipes with spaces. This keeps everyeventsrecord at exactly four pipe-delimited fields regardless of the message text, so a hostile or merely unusual message can never desynchronise the stream. - The watcher reads defensively:
while IFS='|' read -r state msg _binds the first two fields and discards the rest, and the state string is matched against a fixedcaselist. An unrecognised state simply falls through to default urgency rather than being interpolated into a command. tailis not used in a pipeline by the watcher (see above), and the unprivileged script usesnullglobso an absent power-supply glob expands to nothing rather than a literal pattern.
/etc/fiero-hotspot.conf is sourced as root by fiero-hotspot.sh and as the target user by fiero-prompt.sh. Both refuse to source it unless it is owned by root with mode 640 or 600. It must never be world-readable or writable by anyone but root.
/etc/sudoers.d/fiero-hotspot lets the target user run exactly systemctl start fiero-hotspot.service and systemctl stop fiero-hotspot.service without a password. The installer validates the rule with visudo -c before putting it in place.