Merge pull request #207 from outis1one/claude/sip-voip-integration-atsins

Fix ASN-exempt whitelist silently failing with Permission denied
This commit is contained in:
Outis
2026-07-22 23:32:55 -04:00
committed by GitHub
+86 -18
View File
@@ -108,7 +108,7 @@ install_security-dashboard() {
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create system user $SVC_USER"
echo "[DRY-RUN] Would write $APP_DIR/app.py"
echo "[DRY-RUN] Would write /etc/sudoers.d/security-dashboard (scoped cscli/systemctl only)"
echo "[DRY-RUN] Would write /etc/sudoers.d/security-dashboard (scoped cscli/systemctl/set-asn-exempt.sh only)"
echo "[DRY-RUN] Would write a systemd unit and start it on 0.0.0.0:$DASHBOARD_PORT (firewalled via UFW, not interface binding)"
echo "[DRY-RUN] Would grant read/write access to the detected Asterisk config dir (for the PSTN Trunk tab)"
echo "[DRY-RUN] Would configure Caddy + Authelia for a domain you'll be prompted for"
@@ -129,6 +129,7 @@ install_security-dashboard() {
log_info "Refreshing app code + sudoers rule + systemd unit (no Caddy/domain changes)..."
_secdash_grant_asterisk_access "$SVC_USER" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR"
_secdash_write_app "$APP_DIR"
_secdash_write_asn_helper "$APP_DIR"
_secdash_write_sudoers "$SVC_USER"
_secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$ASTERISK_ADMIN_URL"
systemctl restart security-dashboard 2>/dev/null \
@@ -163,6 +164,7 @@ install_security-dashboard() {
mkdir -p "$APP_DIR"
_secdash_write_app "$APP_DIR"
chown -R "$SVC_USER:$SVC_USER" "$APP_DIR"
_secdash_write_asn_helper "$APP_DIR"
_secdash_write_sudoers "$SVC_USER"
_secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$ASTERISK_ADMIN_URL"
@@ -230,13 +232,16 @@ sudo journalctl -u security-dashboard -f
## Security notes
- Runs as a dedicated, unprivileged system user (\`secdash\`), not root.
- Sudo access is scoped to exactly five commands via
- Sudo access is scoped to exactly six commands via
\`/etc/sudoers.d/security-dashboard\`: \`cscli decisions delete --id <digits>\`,
\`cscli decisions list -o json\`, \`cscli alerts list -o json\` (read-only,
used to label ASN exemptions with a carrier name from past alerts and to
find known offending IPs for the "Ban" action), \`cscli decisions add --ip
<ip> --duration <dur> --type ban --reason <text>\` (used only by "Ban"),
and \`systemctl restart crowdsec\`. Nothing else.
\`systemctl restart crowdsec\`, and \`set-asn-exempt.sh\` (root:root, mode
700, installed alongside \`app.py\` — the one thing that edits CrowdSec's
Asterisk-scenario YAMLs, since \`secdash\` has no write access to those
root-owned files directly and shouldn't). Nothing else.
- Listens on all interfaces (Caddy reaches it via \`host.docker.internal\`, a
Docker bridge IP — a loopback-only bind refuses that). Access is scoped by
UFW instead, allowed only from Caddy's internal network, not the internet.
@@ -344,6 +349,7 @@ $_svc_user ALL=(root) NOPASSWD: /usr/bin/cscli decisions list -o json
$_svc_user ALL=(root) NOPASSWD: /usr/bin/cscli alerts list -o json
$_svc_user ALL=(root) NOPASSWD: /usr/bin/cscli decisions add --ip * --duration * --type ban --reason *
$_svc_user ALL=(root) NOPASSWD: /usr/bin/systemctl restart crowdsec
$_svc_user ALL=(root) NOPASSWD: /opt/security-dashboard/set-asn-exempt.sh *
SUDOERS
chmod 440 /etc/sudoers.d/security-dashboard
visudo -c -f /etc/sudoers.d/security-dashboard >/dev/null 2>&1 \
@@ -530,6 +536,69 @@ _secdash_remove_caddy_block() {
log_info "Removed the existing dashboard Caddy block (regenerating it fresh)."
}
# Root-owned helper for editing CrowdSec's Asterisk-scenario YAMLs — the
# secdash service user (--shell /usr/sbin/nologin, no special file grants)
# cannot write /etc/crowdsec/scenarios/*.yaml directly (root:root, mode
# 644): confirmed live, a direct write from app.py failed with "[Errno 13]
# Permission denied". Rather than loosen those files' own permissions,
# route the edit through this one whitelisted root helper via sudo — same
# pattern every other CrowdSec-touching action here already uses (cscli via
# run_sudo), just for a plain file edit instead of a cscli subcommand.
# Mode 700 root:root: secdash can still invoke it (sudoers grants running
# it AS root regardless of the file's own permission bits), but nothing
# else on the box can execute it directly.
_secdash_write_asn_helper() {
local _app_dir="$1"
cat > "$_app_dir/set-asn-exempt.sh" << 'ASNHELPER'
#!/bin/bash
# Auto-generated by services/security-dashboard.sh — do not edit directly,
# re-run the installer instead. Invoked ONLY via sudo, by app.py's
# set_asn_exempt() (see /etc/sudoers.d/security-dashboard for the exact
# grant). Args are ASN numbers (already validated by the caller, but
# re-validated here too since this runs as root — never trust the caller
# alone for a root-executed script).
set -uo pipefail
SCENARIO_FILES=(
/etc/crowdsec/scenarios/local-asterisk_bf.yaml
/etc/crowdsec/scenarios/local-asterisk_user_enum.yaml
)
clean_asns=()
for a in "$@"; do
[[ "$a" =~ ^[0-9]+$ ]] && clean_asns+=("$a")
done
expr="" sep=""
for a in "${clean_asns[@]}"; do
expr="${expr}${sep}'${a}'"
sep=", "
done
found=0
for f in "${SCENARIO_FILES[@]}"; do
if [[ -f "$f" ]]; then
found=1
sed -i "s/ASNNumber in \[[^]]*\])/ASNNumber in [${expr}])/" "$f" || exit 1
fi
done
if [[ "$found" != "1" ]]; then
echo "No CrowdSec scenario files found to update" >&2
exit 1
fi
if ! systemctl restart crowdsec; then
echo "Wrote ASN list but failed to restart CrowdSec" >&2
exit 2
fi
echo "OK"
ASNHELPER
chown root:root "$_app_dir/set-asn-exempt.sh"
chmod 700 "$_app_dir/set-asn-exempt.sh"
}
# Writes the Python app. Separate function so "update" mode (refresh code,
# keep config) and fresh installs share one copy instead of drifting apart.
_secdash_write_app() {
@@ -557,6 +626,12 @@ ASN_SCENARIO_FILES = [
"/etc/crowdsec/scenarios/local-asterisk_bf.yaml",
"/etc/crowdsec/scenarios/local-asterisk_user_enum.yaml",
]
# Root-owned helper for the one write (edit + crowdsec restart) — this
# service user (--shell /usr/sbin/nologin) has no write access to
# ASN_SCENARIO_FILES (root:root, mode 644) and shouldn't; see
# _secdash_write_asn_helper in services/security-dashboard.sh for why this
# goes through sudo instead of loosening those files' permissions.
ASN_HELPER_SCRIPT = "/opt/security-dashboard/set-asn-exempt.sh"
TS_RE = re.compile(r"^\[([^\]]+)\]")
KV_RE = re.compile(r'(\w+)="([^"]*)"')
@@ -706,22 +781,15 @@ def set_asn_exempt(asn_list):
# exempt ASN (the "unwhitelist" action) can actually reach zero instead
# of being stuck refusing an empty save.
clean = sorted(set(a.strip() for a in asn_list if ASN_RE.match(a.strip())))
expr = ", ".join("'%s'" % a for a in clean)
for path in ASN_SCENARIO_FILES:
try:
with open(path) as f:
content = f.read()
except OSError:
continue
new_content = ASN_FILTER_RE.sub("ASNNumber in [%s])" % expr, content)
try:
with open(path, "w") as f:
f.write(new_content)
except OSError as e:
return False, "Failed writing %s: %s" % (path, e)
ok, out, err = run_sudo(["/usr/bin/systemctl", "restart", "crowdsec"])
# Editing ASN_SCENARIO_FILES directly from this process used to fail
# with "[Errno 13] Permission denied" (root:root, mode 644, this
# service user has no write grant) — every ASN whitelist attempt was
# silently a no-op as far as CrowdSec was concerned. Routed through the
# sudoers-whitelisted root helper instead, same pattern every other
# CrowdSec-touching action here already uses.
ok, out, err = run_sudo([ASN_HELPER_SCRIPT] + clean)
if not ok:
return False, "Wrote ASN list but failed to restart CrowdSec: %s" % (err or out)
return False, "Failed updating ASN exemption: %s" % (err or out or "unknown error")
if not clean:
return True, "Cleared — no ASNs exempted, all Asterisk traffic is evaluated normally again."
return True, "Updated: %s" % ", ".join(clean)