Skip to content

Security hardening: control-socket auth, updater injection, fail-closed option (#31–#36) - #37

Open
shipdocs wants to merge 11 commits into
masterfrom
claude/project-audit-security-yb3qft
Open

Security hardening: control-socket auth, updater injection, fail-closed option (#31–#36)#37
shipdocs wants to merge 11 commits into
masterfrom
claude/project-audit-security-yb3qft

Conversation

@shipdocs

@shipdocs shipdocs commented Jul 3, 2026

Copy link
Copy Markdown
Owner

User description

Fixes from the 2026-07-02 security audit. Non-destructive; verified with cargo check (clean) and cargo test config (3/3).

Critical / High

Mechanical hardening (#36)

preinst exact -C/-D rule removal (no more -F OUTPUT/-P ACCEPT); postinst fallback includes mode + #[serde(default)]; xhost grant revoked after gufw; import-rules validation; build_deb heredocs expand ${VERSION}/$(date) (metainfo and changelog); daemon version via env!(CARGO_PKG_VERSION); install.sh DEB_FILE from VERSION; .desktop Exec fix; docs updated.

Closes #31, #32, #33, #34, #35. Refs #36 (tracking — systemd sandboxing, group-membership policy, schema convergence, etc. deferred, see checklist).

🤖 Generated with Claude Code

https://claude.ai/code/session_012EvwLaYAzmzyB6G5fw5YQJ


Generated by Claude Code


CodeAnt-AI Description

Harden control access, updater installs, and firewall rule handling

What Changed

  • The control socket now only accepts root and bastion group members, rejects connections when peer identity cannot be verified, and allows only one GUI connection at a time.
  • The update flow now checks the downloaded version string before use, downloads the package directly, and installs it without passing update data through a shell.
  • In enforcement mode, packets that cannot be parsed are now dropped when fail_closed is enabled; by default, traffic still continues if the daemon cannot inspect it.
  • Rule imports now reject malformed keys and non-boolean values instead of merging invalid data.
  • Temporary root display access for Gufw is revoked after it closes, and the desktop launcher now opens the main GUI directly.
  • New installs include a default learning-mode config, and service cleanup removes only Bastion’s own firewall rules instead of resetting the full OUTPUT chain.

Impact

✅ Fewer unauthorized firewall changes
✅ Lower risk of update command injection
✅ Safer firewall recovery after crashes or upgrades

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Added a configurable secure/strict enforcement mode for handling traffic that can’t be inspected.
    • Improved the app’s update flow with safer downloads and installation.
  • Bug Fixes

    • Tightened GUI session cleanup and reduced stale authorization risk after closing tools.
    • Improved import validation so invalid rule files are rejected early.
    • Fixed request handling to better match responses to the correct request.
  • Documentation

    • Expanded security docs with clearer connection, permissions, and enforcement behavior.
  • Chores

    • Updated packaging and launch behavior for more reliable installs and startup.

claude added 5 commits July 3, 2026 11:22
…rcred (#31, #33, #35)

- Create the control socket 0o660 owned root:bastion instead of 0o666;
  the kernel now restricts connect() to root and bastion-group members.
- Verify SO_PEERCRED on every connection: allow only root or a member of
  the bastion group, and reject (fail closed) when credentials cannot be
  read instead of allowing the connection.
- Refuse a second concurrent GUI connection while one is active to prevent
  control-channel hijacking (#33).
- Match cached popup responses by request_id instead of returning an
  arbitrary pending response, so a response is never delivered to the wrong
  session (#35).
- Reject any fetched version that does not match ^\d+\.\d+\.\d+$ before it
  reaches a URL or a subprocess, closing the command-injection hole.
- Download the release .deb in Python and install it with an argv list
  (pkexec apt-get install -y <file>) instead of building a
  'pkexec bash -c ...' string, so no version data is interpreted by a shell.
- Add a fail_closed config flag (default false, preserving current
  fail-open behavior). When set, in enforcement mode the daemon drops
  packets it cannot parse or inspect instead of accepting them.
- Wire the flag through the NFQUEUE rule string: the systemd unit and
  start_daemon.sh omit --queue-bypass when fail_closed is true, so traffic
  is dropped rather than passed when the daemon is down. ExecStopPost now
  removes both rule variants.
- Also (#36): report the daemon version from CARGO_PKG_VERSION and correct
  the GUI-timeout log to the actual 60s value.
- Also (#36): add #[serde(default)] to Config.mode so a config missing the
  mode key falls back to Learning instead of failing to parse.
- preinst: remove only Bastion's own NFQUEUE rules by exact -C/-D match;
  never flush the OUTPUT chain or reset its policy.
- postinst: include "mode" in the fallback config.json.
- gui_qt/main_window: revoke the temporary xhost +si:localuser:root grant
  once gufw exits.
- gui_qt import_rules: validate keys against path:port|* and values as bool
  before merging.
- build_deb.sh: unquote the metainfo and changelog heredocs so ${VERSION}
  and $(date) expand.
- install.sh: derive DEB_FILE from VERSION (or glob) instead of a hardcoded
  nonexistent 2.0.0 filename.
- com.bastion.firewall.desktop: point Exec at /usr/bin/bastion-gui.
…33, #34)

- README/SECURITY: describe the control socket as 0o660 root:bastion with
  SO_PEERCRED verification, bastion-group restriction, fail-closed on
  credential-lookup failure, and single-session enforcement.
- SECURITY: document the fail_closed config option and its fail-open vs
  fail-closed trade-off.
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@codeant-ai

codeant-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.

@codeant-ai

codeant-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@shipdocs, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1a1a5c4d-79ed-494b-8ed2-a18079205d7b

📥 Commits

Reviewing files that changed from the base of the PR and between c0477bc and 81c8c10.

📒 Files selected for processing (11)
  • bastion-firewall.service
  • bastion-gui.py
  • bastion-rs/src/gui.rs
  • bastion-rs/start_daemon.sh
  • bastion/gui/dashboard/main_window.py
  • bastion/gui_qt.py
  • build_rpm.sh
  • debian/DEBIAN/postrm
  • debian/DEBIAN/preinst
  • install.sh
  • uninstall.sh
📝 Walkthrough

Walkthrough

This PR hardens the daemon control socket by restricting permissions to 0o660/root:bastion and adding fail-closed peer credential authorization, introduces a fail_closed config option applied to packet inspection verdicts, fixes GUI response matching, replaces a shell-based GUI update mechanism with a safer threaded downloader, validates imported rules, adds xhost cleanup, and updates related packaging scripts and documentation.

Changes

Control Socket Authorization and Fail-Closed Filtering

Layer / File(s) Summary
fail_closed config field and accessor
bastion-rs/src/config.rs
Adds serde-defaulted fail_closed field to Config, defaults it to false, and adds is_fail_closed() accessor on ConfigManager.
Socket permission tightening and peer authorization
bastion-rs/src/gui.rs
Tightens socket mode to 0o660 with root:bastion ownership, adds bastion_gid/peer_is_authorized helpers, rejects unauthorized/unverifiable peers at accept time, and fixes ask_gui to match responses by request_id.
Fail-closed packet inspection verdicts
bastion-rs/src/main.rs
Introduces an uninspectable verdict applied to empty payloads, IPv4/IPv6 parse failures, unknown IP versions, and TCP/UDP parse failures when fail_closed is enabled; updates version logging and GUI timeout to 60s.
Config-driven NFQUEUE bypass in scripts
bastion-firewall.service, bastion-rs/start_daemon.sh, debian/DEBIAN/preinst, debian/DEBIAN/postinst
Conditionally applies/removes --queue-bypass based on fail_closed, restricts preinst cleanup to exact-match Bastion rules, and adds a "mode": "learning" default.
Documentation updates
README.md, SECURITY.md
Documents socket permissions/ownership, SO_PEERCRED fail-closed rejection, single active GUI session, and fail-open vs. fail-closed filtering behavior.

GUI Update/Install Safety and Rule Import Validation

Layer / File(s) Summary
Safer GUI update flow
bastion-gui.py
Validates release version with a strict regex and installs the .deb via a threaded urllib/pkexec apt-get flow without invoking a shell, replacing the prior bash one-liner.
xhost cleanup after gufw launch
bastion/gui/dashboard/main_window.py, bastion/gui_qt.py
Waits for the gufw subprocess to exit in a background thread and revokes the temporary xhost root display grant.
Rule import validation
bastion/gui_qt.py
Validates imported rule JSON keys/values before merging, aborting with an Import Error dialog on mismatch.

Packaging Script Fixes

Layer / File(s) Summary
Version-aware packaging and launcher fixes
install.sh, build_deb.sh, com.bastion.firewall.desktop
Computes .deb filename dynamically, enables heredoc variable expansion in metadata/changelog templates, and updates desktop entry to launch bastion-gui.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GUIClient
  participant UnixSocket
  participant GuiState
  participant BastionGroup

  GUIClient->>UnixSocket: connect()
  UnixSocket->>GuiState: accept + getsockopt(SO_PEERCRED)
  alt credentials verified
    GuiState->>BastionGroup: peer_is_authorized(peer_uid)
    BastionGroup-->>GuiState: authorized true/false
    alt authorized
      GuiState-->>GUIClient: accept connection
    else not authorized
      GuiState-->>GUIClient: reject connection
    end
  else credential lookup failed
    GuiState-->>GUIClient: reject connection (fail-closed)
  end
Loading

Possibly related issues

Suggested labels: Possible security concern, Review effort 4/5

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely matches the main security-hardening changes in the PR.
Linked Issues check ✅ Passed The PR addresses the linked objectives: socket hardening, updater injection prevention, fail-closed filtering, GUI response routing, and related fixes.
Out of Scope Changes check ✅ Passed The additional packaging, docs, launcher, and script changes appear tied to the stated hardening and correctness follow-ups.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-audit-security-yb3qft

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Jul 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bastion-rs/src/gui.rs (1)

344-392: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Peer-credential fail-closed logic is correct, but .expect() on try_clone() can panic and permanently kill the GUI control channel.

The authorization/fail-closed logic (lines 361-376) and single-active-GUI enforcement (381-390) correctly implement #31/#33. However, line 391 uses s.try_clone().expect("Failed to clone stream") — if try_clone() fails (e.g. fd exhaustion), this panics the thread running run_socket_server's entire accept loop. Since that loop runs on a single dedicated thread with no supervisor/restart, a single failed clone permanently disables all future GUI connections until the daemon is restarted, even though normal packet filtering keeps running.

Notably, GuiState::set_connection (same file, lines 86-92) already handles this exact failure gracefully via match/error!/early-return — the same pattern should be used here instead of .expect().

🛡️ Proposed fix to avoid panicking on clone failure
                 {
                     let mut state = gui_state.lock();
                     if state.is_connected() {
                         warn!("Refusing GUI connection: a GUI is already connected");
                         drop(state);
                         drop(s);
                         continue;
                     }
-                    state.set_connection(s.try_clone().expect("Failed to clone stream"));
+                    match s.try_clone() {
+                        Ok(cloned) => state.set_connection(cloned),
+                        Err(e) => {
+                            error!("Failed to clone stream, refusing GUI connection: {}", e);
+                            drop(state);
+                            drop(s);
+                            continue;
+                        }
+                    }
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bastion-rs/src/gui.rs` around lines 344 - 392, The GUI accept loop in
run_socket_server should not panic on stream cloning failure; replace the
s.try_clone().expect(...) path used before GuiState::set_connection with
graceful error handling. Mirror the existing GuiState::set_connection pattern by
matching on try_clone(), logging an error or warning, dropping the incoming
socket, and continuing the loop so a transient failure does not kill the
dedicated accept thread. Keep the peer-credential checks and single-active-GUI
logic unchanged, and use the same symbols run_socket_server and
GuiState::set_connection to update the right spot.
🧹 Nitpick comments (1)
bastion-rs/src/gui.rs (1)

269-290: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider getgrouplist() for supplementary-group membership.

Manually scanning gr_mem from getgrnam("bastion") only reflects members explicitly listed against that one group entry. getgrouplist() enumerates all groups a user belongs to (following whatever NSS modules — files, LDAP, sssd — are configured) and is the more standard/robust way to check "is this user a member of group X," especially since this same NSS lookup already backs getgrnam, but going through the user's full group list is less prone to omissions in edge-case NSS configurations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bastion-rs/src/gui.rs` around lines 269 - 290, The supplementary-group check
is too narrow because the current membership logic manually scans gr_mem from
getgrnam("bastion"), which can miss users resolved through NSS. Update the
membership check in the GUI auth/group-validation path to use getgrouplist() for
the target user and verify whether the bastion group is present in the returned
group list. Keep the existing early false returns for null pointers, but replace
the direct gr_mem iteration with the more robust group-enumeration flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bastion-firewall.service`:
- Around line 7-10: Add an ownership marker to the Bastion-installed NFQUEUE
rule before removing it, since the current ExecStopPost cleanup in
bastion-firewall.service can delete unrelated OUTPUT/NEW queue 1 rules. Update
the rule install path in ExecStartPre and mirror the same marker-based matching
in every delete path, including debian/DEBIAN/preinst, debian/DEBIAN/postrm, and
uninstall.sh, so only Bastion’s own rule is targeted.

In `@bastion-gui.py`:
- Around line 520-523: The update error handler in the exception block should
not reference the exception object directly inside the delayed Qt callback,
because the caught variable may be cleared before QTimer.singleShot runs. In the
update install failure path, capture the exception message in a local variable
within the except block and use that captured text in both the print statement
and the QMessageBox.critical callback so the error reporting in the update flow
remains reliable.
- Around line 518-519: The installer call in subprocess.Popen currently relies
on PATH to find pkexec and apt-get and then ignores the return code from
proc.wait(). Update the install flow in bastion-gui.py to use fixed, validated
absolute paths for the privileged commands in the existing installer logic, and
after calling proc.wait() check the exit status and raise or report an error if
the install failed. Keep the change localized around the installer execution
path so the surrounding behavior stays the same.

In `@bastion-rs/start_daemon.sh`:
- Around line 28-29: The NFQUEUE setup in start_daemon.sh can leave an old
OUTPUT rule in place when fail_closed changes, so the daemon keeps using the
previous queue-bypass mode. Update the iptables handling around the OUTPUT chain
to explicitly detect and remove the stale NFQUEUE variant before appending the
current one, using the existing shell logic around the BYPASS variable and the
iptables -C/-A calls. Keep the rule order consistent so the active
--queue-bypass state always matches the current configuration.

In `@bastion/gui_qt.py`:
- Around line 1906-1920: The import validation in gui_qt.py only checks the rule
key shape in the imported_rules loop, so it still accepts invalid ports and
unsafe paths. Update the import path in the rule-loading logic to parse the key
into path and port, reject non-absolute or non-executable paths, and enforce
port values in the valid range 1..65535 or the wildcard *. Keep the existing
QMessageBox.warning flow, and make sure the validation happens before merging
into the rule set using the same imported_rules processing block.
- Around line 1309-1320: The gufw launch path in gui_qt.py can fail after
granting root X access but before the cleanup thread starts, leaving xhost open.
Update the subprocess.Popen(cmd, start_new_session=True) flow to catch launch
failures around the gufw start logic, immediately revoke the temporary xhost
grant in that failure path, and then re-raise the error. Keep the existing
_revoke_xhost cleanup thread for the success path so the grant is removed both
when gufw exits normally and when startup fails.

In `@bastion/gui/dashboard/main_window.py`:
- Around line 933-944: The gufw launch path in main_window.py can fail after the
temporary root xhost grant is applied but before the cleanup thread starts,
leaving access open. Update the subprocess.Popen flow and the surrounding
cleanup logic in the same block so that any exception during launch immediately
revokes the xhost grant before re-raising, while still keeping the existing
_revoke_xhost thread for the successful start case.

In `@debian/DEBIAN/preinst`:
- Around line 25-29: The NFQUEUE cleanup in preinst is too broad because the
iptables deletions in the OUTPUT rule loop match any identical rule, not just
Bastion-owned ones. Update the rule insertion and removal logic in the preinst
script so the Bastion rule is tagged with a unique marker/comment and the
cleanup checks in the same iptables block only delete rules carrying that
marker.

In `@install.sh`:
- Around line 12-16: The fallback in install.sh currently uses ls in the
VERSION-missing path, which can select the wrong bastion-firewall_*_all.deb when
multiple packages exist. Update the DEB_FILE selection logic to avoid parsing ls
and instead use a find-based lookup in the VERSION-absent branch, choosing the
highest-version matching .deb rather than the first alphabetical match. Keep the
change localized to the existing VERSION check and DEB_FILE assignment so the
install flow still uses the same variable.

---

Outside diff comments:
In `@bastion-rs/src/gui.rs`:
- Around line 344-392: The GUI accept loop in run_socket_server should not panic
on stream cloning failure; replace the s.try_clone().expect(...) path used
before GuiState::set_connection with graceful error handling. Mirror the
existing GuiState::set_connection pattern by matching on try_clone(), logging an
error or warning, dropping the incoming socket, and continuing the loop so a
transient failure does not kill the dedicated accept thread. Keep the
peer-credential checks and single-active-GUI logic unchanged, and use the same
symbols run_socket_server and GuiState::set_connection to update the right spot.

---

Nitpick comments:
In `@bastion-rs/src/gui.rs`:
- Around line 269-290: The supplementary-group check is too narrow because the
current membership logic manually scans gr_mem from getgrnam("bastion"), which
can miss users resolved through NSS. Update the membership check in the GUI
auth/group-validation path to use getgrouplist() for the target user and verify
whether the bastion group is present in the returned group list. Keep the
existing early false returns for null pointers, but replace the direct gr_mem
iteration with the more robust group-enumeration flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2011b73-2a27-40e0-a443-9b6cae977c1d

📥 Commits

Reviewing files that changed from the base of the PR and between 7f79542 and c0477bc.

📒 Files selected for processing (15)
  • README.md
  • SECURITY.md
  • bastion-firewall.service
  • bastion-gui.py
  • bastion-rs/src/config.rs
  • bastion-rs/src/gui.rs
  • bastion-rs/src/main.rs
  • bastion-rs/start_daemon.sh
  • bastion/gui/dashboard/main_window.py
  • bastion/gui_qt.py
  • build_deb.sh
  • com.bastion.firewall.desktop
  • debian/DEBIAN/postinst
  • debian/DEBIAN/preinst
  • install.sh

Comment thread bastion-firewall.service Outdated
Comment thread bastion-gui.py Outdated
Comment thread bastion-gui.py Outdated
Comment thread bastion-rs/start_daemon.sh Outdated
Comment thread bastion/gui_qt.py Outdated
Comment thread bastion/gui_qt.py Outdated
Comment thread bastion/gui/dashboard/main_window.py Outdated
Comment thread debian/DEBIAN/preinst Outdated
Comment thread install.sh
Comment thread bastion-gui.py Outdated
Comment on lines +520 to +523
except Exception as e:
print(f"[UPDATE] Failed to install update: {e}")
QTimer.singleShot(0, lambda: QMessageBox.critical(
None, "Update Error", f"Failed to install update: {e}"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The lambda scheduled on the Qt event loop closes over exception variable e, but Python clears e after the except block ends. When the callback runs, it can raise a NameError and the error dialog will not be shown. Capture the message string before scheduling the lambda. [incorrect variable usage]

Severity Level: Major ⚠️
- ❌ Update error dialog can crash and not display.
- ⚠️ Users get no GUI feedback on update failures.
Steps of Reproduction ✅
1. Start the Qt tray application via `bastion-gui.py` main block (`bastion-gui.py:84-98`),
which constructs `BastionClient` and its tray menu including the update action wired to
`perform_update()` (`bastion-gui.py:11-32,34-82`).

2. Ensure `self.latest_update_version` is set (for example by the existing update-check
thread) so `show_update_notification()` (`bastion-gui.py:11-32`) inserts the "Install
Update" QAction that calls `perform_update()` when selected.

3. Trigger the update by clicking the menu action, causing `perform_update()` to spawn the
`_download_and_install()` thread (`bastion-gui.py:59-82`) which runs the download/install
logic.

4. Induce an exception inside `_download_and_install()` (for example, by breaking network
access so `urllib.request.urlretrieve` fails, or by making `pkexec`/`apt-get` unavailable)
so execution enters the `except Exception as e:` block at `bastion-gui.py:71-74`.

5. Observe that the `QTimer.singleShot(0, lambda: QMessageBox.critical(... f"... {e}"))`
scheduled at `bastion-gui.py:73-74` runs later on the Qt event loop, after leaving the
`except` block; in Python 3 the exception variable `e` is cleared after the `except`, so
the lambda’s reference to `e` can raise a `NameError`/`UnboundLocalError`, preventing the
error dialog from being shown and instead surfacing a runtime error in the event loop.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** bastion-gui.py
**Line:** 520:523
**Comment:**
	*Incorrect Variable Usage: The lambda scheduled on the Qt event loop closes over exception variable `e`, but Python clears `e` after the `except` block ends. When the callback runs, it can raise a `NameError` and the error dialog will not be shown. Capture the message string before scheduling the lambda.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread bastion-rs/src/gui.rs
Comment on lines +274 to +280
let user_cstr = std::ffi::CStr::from_ptr(user_name);
let grp_name = match std::ffi::CString::new("bastion") {
Ok(n) => n,
Err(_) => return false,
};
let grp = libc::getgrnam(grp_name.as_ptr());
if grp.is_null() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The supplementary-membership loop dereferences members before verifying the pointer itself is non-null. If gr_mem is null for a group entry, this will crash during authorization checks. Check members.is_null() before *members and only dereference inside a guarded loop. [null pointer]

Severity Level: Critical 🚨
- ❌ Control socket authorization can segfault on misconfigured bastion group.
- ❌ Daemon crash terminates firewall control and GUI socket server.
Steps of Reproduction ✅
1. Start the Rust daemon, whose `main()` in `bastion-rs/src/main.rs:64-69` spawns the GUI
control socket thread by calling `run_socket_server(gui_state_server, stats_server,
config_server, rules_server)` (`bastion-rs/src/gui.rs:295-304`).

2. From any GUI client (e.g., `bastion-gui.py` or `bastion/gui_qt.py`), connect to the
Unix control socket at `SOCKET_PATH`, causing `run_socket_server`’s accept loop
(`bastion-rs/src/gui.rs:132-203`) to accept a `UnixStream` and, on Unix, call
`getsockopt(... SO_PEERCRED ...)` and then `peer_is_authorized(peer_uid)`
(`gui.rs:152-155,41-84`).

3. Configure the system such that the `bastion` group exists but its `gr_mem` field is
null or otherwise not set to a valid null-terminated array (this matches the libc-level
scenario the comment warns about, and is realistic with misconfigured or empty groups);
`libc::getgrnam` at `gui.rs:70-72` then returns a `grp` where `(*grp).gr_mem` is a null
pointer.

4. When `peer_is_authorized` executes `let mut members = (*grp).gr_mem; while
!(*members).is_null() { ... }` at `bastion-rs/src/gui.rs:74-80`, it dereferences `members`
before checking whether `members` itself is null, triggering undefined behaviour and
typically a segmentation fault that crashes the entire daemon during an authorization
check.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** bastion-rs/src/gui.rs
**Line:** 274:280
**Comment:**
	*Null Pointer: The supplementary-membership loop dereferences `members` before verifying the pointer itself is non-null. If `gr_mem` is null for a group entry, this will crash during authorization checks. Check `members.is_null()` before `*members` and only dereference inside a guarded loop.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +934 to 945
# Revoke the temporary root display grant once gufw exits so it is
# not left open indefinitely.
import threading

def _revoke_xhost(p=proc):
try:
p.wait()
finally:
subprocess.run(['xhost', '-si:localuser:root'], capture_output=True)

threading.Thread(target=_revoke_xhost, daemon=True).start()
else:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The display grant is opened before spawning gufw, but revocation only happens in a thread created after Popen succeeds. If process launch fails, the revoke path is never executed and root display access remains granted. Wrap launch/revoke in a try/finally (or revoke on launch failure) so cleanup is guaranteed. [missing cleanup]

Severity Level: Major ⚠️
- ⚠️ Root display access may remain granted after launch failure.
- ⚠️ Wayland gufw integration leaves lingering xhost permission.
Steps of Reproduction ✅
1. Open the dashboard GUI implemented in `bastion/gui/dashboard/main_window.py`, which
constructs `DashboardWindow` and provides the "Launch gufw" action wired to
`launch_gufw()` (`main_window.py:49-80,949-77`).

2. Run the application in a Wayland session (environment variable
`XDG_SESSION_TYPE=wayland`), and invoke `launch_gufw()`, which checks for `gufw` and calls
`_launch_gufw_process()` (`main_window.py:49-57,915-947`) when installed.

3. Inside `_launch_gufw_process`, on Wayland it first enables root access to the display
via `subprocess.run(['xhost', '+si:localuser:root'], capture_output=True)` and then
constructs the `pkexec env ... gufw` command (`main_window.py:21-33`) before calling `proc
= subprocess.Popen(cmd, start_new_session=True)` at `main_window.py:34`.

4. If `subprocess.Popen` raises (for example because `pkexec` is missing, authentication
fails early, or there is an OS-level resource issue), execution never reaches the
`_revoke_xhost` thread definition and `threading.Thread(...).start()` at
`main_window.py:35-45`, so the compensating `xhost -si:localuser:root` call in
`_revoke_xhost` is never run; the temporary root display grant made in step 3 remains in
effect indefinitely until manually revoked.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** bastion/gui/dashboard/main_window.py
**Line:** 934:945
**Comment:**
	*Missing Cleanup: The display grant is opened before spawning `gufw`, but revocation only happens in a thread created after `Popen` succeeds. If process launch fails, the revoke path is never executed and root display access remains granted. Wrap launch/revoke in a `try/finally` (or revoke on launch failure) so cleanup is guaranteed.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread bastion/gui_qt.py Outdated
Comment on lines +1309 to +1320
proc = subprocess.Popen(cmd, start_new_session=True)
# Revoke the temporary root display grant once gufw exits so it is
# not left open indefinitely.
import threading

def _revoke_xhost(p=proc):
try:
p.wait()
finally:
subprocess.run(['xhost', '-si:localuser:root'], capture_output=True)

threading.Thread(target=_revoke_xhost, daemon=True).start()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The temporary xhost permission is revoked only from a thread that starts after successful Popen. If launching pkexec env ... gufw throws, cleanup never runs and root keeps display access. Ensure revocation executes on both success and failure paths. [missing cleanup]

Severity Level: Major ⚠️
- ⚠️ Root display access may linger after gufw launch error.
- ⚠️ Wayland Qt launcher leaves xhost grant on failure.
Steps of Reproduction ✅
1. Launch the Qt-based dashboard implemented in `bastion/gui_qt.py`, where
`DashboardWindow.launch_gufw()` (`gui_qt.py:45-76`) provides a "Launch gufw" action that
calls `_launch_gufw_process()` when `gufw` is installed.

2. Run under Wayland (environment `XDG_SESSION_TYPE=wayland`), then trigger
`launch_gufw()` so `_launch_gufw_process()` executes the Wayland branch
(`gui_qt.py:12-24`), calling `subprocess.run(['xhost', '+si:localuser:root'],
capture_output=True)` to grant root display access.

3. `_launch_gufw_process()` builds the `pkexec env ... gufw` command and calls `proc =
subprocess.Popen(cmd, start_new_session=True)` at `bastion/gui_qt.py:30`, which may raise
if `pkexec` is missing, if there is an OS-level resource limit, or if another early
failure occurs before the child process starts.

4. Because the revocation logic `_revoke_xhost` and its background thread are only defined
and started after `Popen` succeeds (`gui_qt.py:31-41`), any exception thrown by `Popen`
prevents the `xhost -si:localuser:root` cleanup from running, leaving the temporary root
display grant active beyond the intended lifetime of the gufw session.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** bastion/gui_qt.py
**Line:** 1309:1320
**Comment:**
	*Missing Cleanup: The temporary `xhost` permission is revoked only from a thread that starts after successful `Popen`. If launching `pkexec env ... gufw` throws, cleanup never runs and root keeps display access. Ensure revocation executes on both success and failure paths.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai

codeant-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@augmentcode

augmentcode Bot commented Jul 3, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Security-hardening pass across the daemon, GUI, and packaging based on the 2026-07-02 audit.

Changes:

  • Hardened the control socket: set mode to 0o660, chown to root:bastion, verify peers via SO_PEERCRED, and fail-closed when creds can’t be read.
  • Prevented GUI control-channel hijacking by refusing a second concurrent GUI connection.
  • Closed updater command-injection by validating release versions and installing via pkexec apt-get … argv (no shell).
  • Added fail_closed config flag to drop uninspectable packets in enforcement mode and omit --queue-bypass when enabled.
  • Updated systemd/service and helper scripts to conditionally include/remove NFQUEUE rules with/without bypass.
  • Improved packaging scripts (preinst/postinst) to remove only Bastion’s rules and to seed config defaults more safely.
  • Revoked temporary xhost display access after GUFW exits to reduce exposure.
  • Validated imported rules file entries (key shape + boolean values) before merging.

Technical Notes: The hardening relies on both filesystem permissions and runtime credential checks; fail_closed trades availability for stricter enforcement when the daemon is down or traffic is unparseable.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 5 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread bastion-rs/src/gui.rs Outdated
return false;
}
let mut members = (*grp).gr_mem;
while !(*members).is_null() {

@augmentcode augmentcode Bot Jul 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bastion-rs/src/gui.rs:284 members can be null if (*grp).gr_mem is null (e.g., empty member list), and while !(*members).is_null() would dereference a null pointer (UB/crash). Consider guarding members before dereferencing so an empty/NULL list fails closed safely.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread bastion-rs/src/gui.rs Outdated
let _ = std::fs::set_permissions(SOCKET_PATH, std::fs::Permissions::from_mode(0o666));
// 0o660 + root:bastion ownership: only root and members of the bastion
// group can connect. The kernel enforces this at connect() time.
let _ = std::fs::set_permissions(SOCKET_PATH, std::fs::Permissions::from_mode(0o660));

@augmentcode augmentcode Bot Jul 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bastion-rs/src/gui.rs:330 set_permissions errors are ignored, so the control socket may keep its default mode/ownership (potentially weakening the intended connect-time restriction or breaking GUI access). Consider handling failures explicitly (at least logging/aborting) since this is part of the hardening.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread bastion-firewall.service Outdated
[Service]
Type=simple
ExecStartPre=/bin/sh -c 'iptables -C OUTPUT -m state --state NEW -j NFQUEUE --queue-num 1 --queue-bypass 2>/dev/null || iptables -I OUTPUT -m state --state NEW -j NFQUEUE --queue-num 1 --queue-bypass'
ExecStartPre=/bin/sh -c 'BYPASS=--queue-bypass; grep -qs "\"fail_closed\"[[:space:]]*:[[:space:]]*true" /etc/bastion/config.json && BYPASS=; iptables -C OUTPUT -m state --state NEW -j NFQUEUE --queue-num 1 $$BYPASS 2>/dev/null || iptables -I OUTPUT -m state --state NEW -j NFQUEUE --queue-num 1 $$BYPASS'

@augmentcode augmentcode Bot Jul 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bastion-firewall.service:7 The grep check for "fail_closed": true only matches when true is on the same line as the key; a pretty-printed JSON with a newline after : would silently fall back to --queue-bypass. Other locations where this applies: bastion-rs/start_daemon.sh:25.

Severity: medium

Other Locations
  • bastion-rs/start_daemon.sh:25

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread bastion/gui_qt.py Outdated
# "path:*", and values must be booleans. This prevents a malformed
# or malicious rules file from injecting junk into the rule set.
import re
key_pattern = re.compile(r'^.+:(\d+|\*)$')

@augmentcode augmentcode Bot Jul 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bastion/gui_qt.py:1910 Import validation accepts any digit port (\d+) without checking range, so keys like app:99999 pass and may later cause unexpected behavior when the daemon/UI assumes a valid TCP/UDP port. Consider rejecting ports outside 1–65535 during import to keep persisted rules consistent.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread bastion-gui.py Outdated
# argv list, never a shell string: no version data is
# interpreted by a shell.
proc = subprocess.Popen(['pkexec', 'apt-get', 'install', '-y', deb_path])
proc.wait()

@augmentcode augmentcode Bot Jul 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bastion-gui.py:519 proc.wait() ignores the installer exit status, so an apt-get failure (non-zero) won't surface to the user unless it raises an exception. Consider checking proc.returncode and showing an error when install fails.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

claude added 6 commits July 3, 2026 12:33
…iew)

- peer_is_authorized: check members.is_null() before dereferencing gr_mem,
  guarding each loop iteration to avoid UB/segfault on a null member list.
- run_socket_server: replace try_clone().expect() with a match that logs and
  continues so fd exhaustion no longer panics the accept thread and permanently
  disables GUI connections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…review)

- Use absolute /usr/bin/pkexec and /usr/bin/apt-get; verify each exists and is
  executable before invoking, and raise on a non-zero apt-get exit code.
- Capture the exception message into a local before the delayed QMessageBox
  lambda so the error dialog no longer raises NameError (Python 3 clears the
  except var after the block).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ules (#37 review)

- Wrap the gufw Popen in try/except that revokes the temporary
  xhost +si:localuser:root grant and re-raises, so a failed launch never leaves
  root display access open (gui_qt.py and dashboard/main_window.py).
- Import-rules validation: parse each key into path:port, reject non-absolute
  paths and ports outside 1..65535 (allowing '*'), before merging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e cleanup (#37 review)

Insert the queue-1 NFQUEUE rule with -m comment --comment "bastion-firewall"
(service ExecStartPre, start_daemon.sh) and require the same comment in every
-C/-D check/delete path (service ExecStopPost, preinst, postrm, uninstall.sh,
build_rpm.sh) so cleanup only ever targets Bastion's own rule, never an
unrelated firewall's identical queue-1 rule. start_daemon.sh now also deletes
both variants (with and without --queue-bypass) before appending so a
fail_closed flip takes effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…review)

The VERSION-absent fallback used ls | head -n1, which picks the
alphabetically-first .deb. Use find -printf | sort -V | tail -n1 to select the
highest version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d detection (#37 review)

- run_socket_server: treat set_permissions(0o660) and chown-to-root:bastion
  failures as fatal — error! and remove the socket then return, instead of
  serving a weakly-permissioned control socket.
- fail_closed detection: strip whitespace/newlines with tr before grep in both
  the service ExecStartPre and start_daemon.sh, so a pretty-printed config with
  a newline after the colon still disables --queue-bypass (was fail-open).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SECURITY][CRITICAL] Daemon control socket is world-connectable and unauthenticated

2 participants