mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-20 09:59:09 +00:00
Review follow-up. Two findings, both right, and the first is a hole I opened myself. `NOPASSWD: iptables *` is a root shell for the web user by another name. `iptables --modprobe=/path/to/anything` runs that path as root, so a wildcard grant on iptables escalates rather than restricts. I added that rule while fixing a permissions gap, which is a worse outcome than the gap. It is gone, and a test now fails on any trailing-wildcard grant to a tool that can execute another program -- iptables, nft, tcpdump, find, awk, sed, perl, python, env. The other finding: checking only the binary made the coverage test far weaker than it looked. With `sysctl` present anywhere in the allow-list, deleting the `net.ipv4.ip_forward=0` grant still passed -- and the portal would then be unable to restore forwarding on teardown. Each required command is now matched in full, and each is mutation-checked individually, including that exact single-line case. Scope pulled in deliberately. The first version of this test tried to assert that *every* sudo call in the codebase is granted. Run honestly, it showed the portal also runs iptables, nft, `ip addr`, `ip link` and `cp` with arguments built at runtime -- an interface name, a port. Those cannot be granted safely in a sudoers file: the rule needs a trailing wildcard, and that is the escalation above. Closing that half needs a privileged helper that builds the rules itself and takes only an interface and a port, granted the way safe_plugin_rm.sh already is. That is a design decision, not a one-line grant, so the test now pins the four commands this change actually grants and the docstring says plainly what it does not cover. Better a narrow test that is true than a broad one that is not.
192 lines
7.1 KiB
Bash
Executable File
192 lines
7.1 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# LED Matrix WiFi Management Permissions Configuration Script
|
|
# This script configures both sudo and PolicyKit permissions for WiFi management
|
|
|
|
set -e
|
|
|
|
# Cleanup function for temp files
|
|
cleanup() {
|
|
rm -f "$TEMP_SUDOERS" "$TEMP_POLKIT" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "Configuring WiFi management permissions for LED Matrix Web Interface..."
|
|
|
|
# Get the current user (should be the user running the web interface)
|
|
WEB_USER=$(whoami)
|
|
|
|
echo "Detected web interface user: $WEB_USER"
|
|
|
|
# Check if running as root
|
|
if [ "$EUID" -eq 0 ]; then
|
|
echo "Error: This script should not be run as root."
|
|
echo "Run it as the user that will be running the web interface."
|
|
exit 1
|
|
fi
|
|
|
|
# Get the full paths to commands
|
|
NMCLI_PATH=$(which nmcli || echo "/usr/bin/nmcli")
|
|
SYSTEMCTL_PATH=$(which systemctl)
|
|
|
|
echo "Command paths:"
|
|
echo " nmcli: $NMCLI_PATH"
|
|
echo " systemctl: $SYSTEMCTL_PATH"
|
|
|
|
# Step 1: Configure sudo permissions for nmcli
|
|
echo ""
|
|
echo "Step 1: Configuring sudo permissions for nmcli..."
|
|
SUDOERS_FILE="/etc/sudoers.d/ledmatrix_wifi"
|
|
SYSCTL_PATH=$(command -v sysctl || echo /usr/sbin/sysctl)
|
|
NFT_PATH=$(command -v nft || echo /usr/sbin/nft)
|
|
RFKILL_PATH=$(command -v rfkill || echo /usr/sbin/rfkill)
|
|
MKDIR_PATH=$(command -v mkdir || echo /usr/bin/mkdir)
|
|
|
|
# Create a temporary sudoers file using mktemp (handles permissions better)
|
|
TEMP_SUDOERS=$(mktemp) || {
|
|
echo "✗ Failed to create temporary file"
|
|
exit 1
|
|
}
|
|
|
|
cat > "$TEMP_SUDOERS" << EOF
|
|
# LED Matrix WiFi Management passwordless sudo configuration
|
|
# This allows the web interface user to run nmcli commands without a password
|
|
|
|
# Allow $WEB_USER to run nmcli commands without a password for WiFi management
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $NMCLI_PATH device wifi connect *
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $NMCLI_PATH device wifi disconnect *
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $NMCLI_PATH device disconnect *
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $NMCLI_PATH device connect *
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $NMCLI_PATH radio wifi on
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $NMCLI_PATH radio wifi off
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH start hostapd
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH stop hostapd
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart hostapd
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH start dnsmasq
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH stop dnsmasq
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart dnsmasq
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart NetworkManager
|
|
# The captive portal turns IP forwarding on while the access point is up and
|
|
# restores the previous value when it comes down (wifi_manager._setup_iptables_
|
|
# redirect / _teardown_iptables_redirect). Without this rule that sudo call
|
|
# needs a password, so forwarding stays off and clients associate to the AP but
|
|
# cannot route. It goes unnoticed on a stock Raspberry Pi image, where
|
|
# /etc/sudoers.d/010_pi-nopasswd grants the default user blanket NOPASSWD and
|
|
# masks every gap in this file -- it only bites once that blanket rule is
|
|
# removed.
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSCTL_PATH -w net.ipv4.ip_forward=0
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $SYSCTL_PATH -w net.ipv4.ip_forward=1
|
|
# The portal's redirect lives in its own nftables table, created when the AP
|
|
# comes up and deleted when it goes down, and the radio has to be unblocked
|
|
# before the AP can start at all. Same story as the sysctl rules above: called
|
|
# with sudo, never granted here, and invisible on a stock Pi image.
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $NFT_PATH add table ip ledmatrix
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $NFT_PATH delete table ip ledmatrix
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $RFKILL_PATH unblock wifi
|
|
# NetworkManager's dnsmasq drop-in directory, exact path.
|
|
$WEB_USER ALL=(ALL) NOPASSWD: $MKDIR_PATH -p /etc/NetworkManager/dnsmasq-shared.d
|
|
#
|
|
# iptables is deliberately NOT granted here. Its rules are built from the live
|
|
# interface name and port, so a rule covering them needs a trailing wildcard --
|
|
# and `iptables --modprobe=/path/to/anything` runs that path as root, so
|
|
# `NOPASSWD: iptables *` is a root shell for the web user by another name. That
|
|
# is a worse outcome than the gap it would close, which today is masked anyway
|
|
# by the blanket NOPASSWD rule on stock Pi images.
|
|
#
|
|
# Closing it safely means a wrapper script that builds the rules itself and
|
|
# takes only an interface and a port, granted the way safe_plugin_rm.sh already
|
|
# is. That belongs in its own change rather than being smuggled into this one.
|
|
|
|
# Allow copying hostapd and dnsmasq config files into place
|
|
$WEB_USER ALL=(ALL) NOPASSWD: /usr/bin/cp /tmp/hostapd.conf /etc/hostapd/hostapd.conf
|
|
$WEB_USER ALL=(ALL) NOPASSWD: /usr/bin/cp /tmp/dnsmasq.conf /etc/dnsmasq.d/ledmatrix-captive.conf
|
|
$WEB_USER ALL=(ALL) NOPASSWD: /usr/bin/rm -f /etc/dnsmasq.d/ledmatrix-captive.conf
|
|
EOF
|
|
|
|
echo "Generated sudoers configuration:"
|
|
echo "--------------------------------"
|
|
cat "$TEMP_SUDOERS"
|
|
echo "--------------------------------"
|
|
|
|
# Apply the sudoers configuration
|
|
echo ""
|
|
echo "Applying sudoers configuration..."
|
|
if sudo cp "$TEMP_SUDOERS" "$SUDOERS_FILE"; then
|
|
sudo chmod 440 "$SUDOERS_FILE"
|
|
echo "✓ Sudoers configuration applied successfully!"
|
|
else
|
|
echo "✗ Failed to apply sudoers configuration"
|
|
rm -f "$TEMP_SUDOERS"
|
|
exit 1
|
|
fi
|
|
|
|
rm -f "$TEMP_SUDOERS"
|
|
|
|
# Step 2: Configure PolicyKit permissions for NetworkManager
|
|
echo ""
|
|
echo "Step 2: Configuring PolicyKit permissions for NetworkManager..."
|
|
|
|
POLKIT_RULES_DIR="/etc/polkit-1/rules.d"
|
|
POLKIT_RULE_FILE="$POLKIT_RULES_DIR/10-ledmatrix-wifi.rules"
|
|
|
|
# Create PolicyKit rule using mktemp (handles permissions better)
|
|
TEMP_POLKIT=$(mktemp) || {
|
|
echo "✗ Failed to create temporary file"
|
|
exit 1
|
|
}
|
|
|
|
cat > "$TEMP_POLKIT" << EOF
|
|
// LED Matrix WiFi Management PolicyKit rules
|
|
// This allows the web interface user to control NetworkManager without authentication
|
|
|
|
polkit.addRule(function(action, subject) {
|
|
if (action.id.indexOf("org.freedesktop.NetworkManager.") == 0 &&
|
|
subject.user == "$WEB_USER") {
|
|
return polkit.Result.YES;
|
|
}
|
|
});
|
|
EOF
|
|
|
|
echo "Generated PolicyKit rule:"
|
|
echo "--------------------------------"
|
|
cat "$TEMP_POLKIT"
|
|
echo "--------------------------------"
|
|
|
|
# Apply the PolicyKit rule
|
|
echo ""
|
|
echo "Applying PolicyKit rule..."
|
|
if sudo cp "$TEMP_POLKIT" "$POLKIT_RULE_FILE"; then
|
|
sudo chmod 644 "$POLKIT_RULE_FILE"
|
|
echo "✓ PolicyKit rule applied successfully!"
|
|
else
|
|
echo "✗ Failed to apply PolicyKit rule"
|
|
rm -f "$TEMP_POLKIT"
|
|
exit 1
|
|
fi
|
|
|
|
rm -f "$TEMP_POLKIT"
|
|
|
|
# Step 3: Test permissions
|
|
echo ""
|
|
echo "Step 3: Testing permissions..."
|
|
|
|
# Test sudo access
|
|
if sudo -n "$NMCLI_PATH" device status > /dev/null 2>&1; then
|
|
echo "✓ nmcli device status - OK"
|
|
else
|
|
echo "✗ nmcli device status - Failed (this is expected if not connected)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Configuration complete!"
|
|
echo ""
|
|
echo "The web interface user ($WEB_USER) now has:"
|
|
echo "- Passwordless sudo access to nmcli commands"
|
|
echo "- PolicyKit permissions to control NetworkManager"
|
|
echo ""
|
|
echo "You may need to restart the web interface service for changes to take effect:"
|
|
echo " sudo systemctl restart ledmatrix-web.service"
|
|
echo ""
|
|
echo "Or if running manually, restart your Flask application."
|
|
|