Fix secdash access to Asterisk config: use ACLs, not chown-fragile groups

Confirmed live on a real droplet: secdash could read pjsip.conf fine but
/api/pstn-status kept returning false even though pstn-trunk-dialplan.conf
genuinely existed. Root cause: the Asterisk container's own entrypoint runs
`chown -R asterisk:asterisk /etc/asterisk` on every container start, and
the numeric UID/GID it resolves to inside the container coincidentally
collided with unrelated host system accounts (config/asterisk ended up
owned by messagebus:uuidd on this box) - silently reverting whatever group
membership _secdash_grant_asterisk_access had granted secdash at install
time. This wasn't a one-time misconfiguration; it would have silently
broken again on every future container restart.

Switched the grant mechanism from chmod + usermod group membership to
POSIX ACLs (setfacl) - chown doesn't touch ACL entries (only chmod
recalculates the ACL mask, and nothing in this flow chmods after install),
so the grant survives the container's own maintenance chown. Default ACLs
(-d) also make newly-created files (a regenerated dialplan, a fresh
personal-DID entry) inherit the same access automatically.

Also added _secdash_grant_ancestor_traversal, which grants execute-only ACL
traversal up the directory tree - needed on any box where DOCKER_DIR sits
under a restrictive parent (e.g. /root on some cloud images defaults to
700, blocking a non-root secdash from ever reaching deeper directories no
matter what those directories themselves grant). Falls back to the old
chmod/group approach with a warning if the 'acl' package is somehow
unavailable (it's installed automatically otherwise).

Verified with a real non-root system test user against a fixture
reproducing the exact failure (a leaf directory with no "other" access,
owned by an unrelated user/group): confirmed blocked before the fix,
confirmed read+write access after, and confirmed access survives a
simulated `chown -R` (the container restart scenario) plus correct
inheritance onto a freshly-created file afterward - all without re-running
the grant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
This commit is contained in:
Claude
2026-07-24 13:26:35 +00:00
parent ff61e8f733
commit dd8d20cbc8
+74 -24
View File
@@ -314,38 +314,88 @@ README_MD
echo ""
}
# Grants secdash execute-only traversal (via a POSIX ACL, not chmod) on
# every ancestor directory between the filesystem root and _leaf, stopping
# early once an ancestor is already reachable. Needed because DOCKER_DIR can
# be /root/docker (any root-run droplet — a fully supported setup, not a
# mistake) and some cloud images ship /root at a bare 700: no matter what
# access the LEAF directory itself grants, secdash (a non-root system user)
# can never reach through a blocking ancestor to get there. setfacl here
# grants ONLY the ability to pass through a path already known in advance —
# it does not grant listing that directory's contents or reading anything
# else inside it.
_secdash_grant_ancestor_traversal() {
local _svc_user="$1" _leaf="$2"
command -v setfacl >/dev/null 2>&1 || return 0
local _dir
_dir="$(dirname "$_leaf")"
while [[ "$_dir" != "/" && -n "$_dir" ]]; do
sudo -u "$_svc_user" test -x "$_dir" 2>/dev/null && break
setfacl -m "u:${_svc_user}:x" "$_dir" 2>/dev/null || true
_dir="$(dirname "$_dir")"
done
}
# Grants secdash read/write access to wherever Asterisk's config lives
# without running the dashboard as root or the actual user — added to the
# group that already owns those directories (ensure_docker_dir_ownership
# elsewhere in this repo sets both owner AND group to ACTUAL_USER, so the log
# dir and config dir normally share one group already; handled separately
# anyway in case that ever changes). Separate function, called from both
# "update" and fresh-install, so a PSTN trunk installed *after* this
# dashboard (or an asterisk-digital-ocean/asterisk swap) reaches an existing
# install on its next update instead of silently only applying to new ones.
# without running the dashboard as root or the actual user. Separate
# function, called from both "update" and fresh-install, so a PSTN trunk
# installed *after* this dashboard (or an asterisk-digital-ocean/asterisk
# swap) reaches an existing install on its next update instead of silently
# only applying to new ones.
#
# Uses POSIX ACLs (setfacl), not chmod + group membership. Confirmed live:
# the Asterisk container's own entrypoint runs `chown -R asterisk:asterisk
# /etc/asterisk` on every container start/restart — and the numeric UID/GID
# that resolves to inside the container can coincidentally collide with
# unrelated system accounts on the host (observed: config/asterisk ending up
# owned by messagebus:uuidd, neither of which secdash has any relationship
# to), silently reverting whatever group grant was applied at install time.
# `chown` does not touch ACL entries (only `chmod` recalculates the ACL
# mask, and nothing in this flow calls chmod after install) — so an
# ACL-based grant survives that reset instead of quietly breaking again on
# the next container restart. `-d` (default ACL) makes new files/directories
# created later (a regenerated dialplan file, a fresh personal-DID entry)
# inherit the same grant automatically. Falls back to the old chmod/group
# approach with a warning if the `acl` package isn't installed for some
# reason (should always be present — installed below).
_secdash_grant_asterisk_access() {
local _svc_user="$1" _log_dir="$2" _config_dir="$3" _ea_config_dir="${4:-}"
command -v setfacl >/dev/null 2>&1 || run_cmd apt-get install -y acl >/dev/null 2>&1
local _have_acl=false
command -v setfacl >/dev/null 2>&1 && _have_acl=true
[ "$_have_acl" = true ] || log_warning "Package 'acl' unavailable — falling back to group-based access, which can silently break again whenever the Asterisk container re-chowns its own config directory. Install 'acl' and re-run to fix that properly."
local _dir
for _dir in "$_log_dir" "$_config_dir" "$_ea_config_dir"; do
[ -n "$_dir" ] && [ -d "$_dir" ] || continue
local _group
_group="$(stat -c '%G' "$_dir" 2>/dev/null || echo "$ACTUAL_USER")"
usermod -aG "$_group" "$_svc_user" 2>/dev/null || true
chmod 750 "$_dir" 2>/dev/null || true
_secdash_grant_ancestor_traversal "$_svc_user" "$_dir"
if [ "$_have_acl" = true ]; then
setfacl -R -m "u:${_svc_user}:rX" "$_dir" 2>/dev/null || true
setfacl -R -d -m "u:${_svc_user}:rX" "$_dir" 2>/dev/null || true
else
local _group
_group="$(stat -c '%G' "$_dir" 2>/dev/null || echo "$ACTUAL_USER")"
usermod -aG "$_group" "$_svc_user" 2>/dev/null || true
chmod 750 "$_dir" 2>/dev/null || true
fi
done
# pstn-permissions.conf specifically needs group WRITE (750 above is
# read+execute for the group, not write) — the file itself is written
# group-writable (664) by services/pstn-trunk.sh, but the containing
# directory also needs the group execute+write bit for a new file save
# (configparser writes a fresh temp file then renames it into place) to
# succeed. 770 only on the config dir, not the log dir (no reason for
# secdash to ever create files in the log dir). _ea_config_dir
# (categories.conf/rooms.conf) deliberately stays 750/read-only — the
# native Asterisk Admin tab writes those through `docker exec ... tee`
# instead (see the ea_* functions), not a direct host-side write, so
# there's no reason to grant this directory group-write at all.
# pstn-permissions.conf/pstn-limits.conf/pstn-personal-dids.conf need
# WRITE access on the containing directory too (configparser writes a
# fresh temp file then renames it into place) — only on the config dir,
# not the log dir (no reason for secdash to ever create files there).
# _ea_config_dir (categories.conf/rooms.conf) deliberately stays
# read-only — the native Asterisk Admin tab writes those through
# `docker exec ... tee` instead (see the ea_* functions), not a direct
# host-side write, so there's no reason to grant it write access at all.
if [ -n "$_config_dir" ] && [ -d "$_config_dir" ]; then
chmod 770 "$_config_dir" 2>/dev/null || true
if [ "$_have_acl" = true ]; then
setfacl -m "u:${_svc_user}:rwx" "$_config_dir" 2>/dev/null || true
setfacl -d -m "u:${_svc_user}:rwx" "$_config_dir" 2>/dev/null || true
else
chmod 770 "$_config_dir" 2>/dev/null || true
fi
fi
}