_asterisk_configure_caddy_public() used to reverse-proxy Asterisk's own
web admin at its public domain, optionally gated by local or remote
Authelia, flipping WEB_ADMIN_AUTH_DISABLED=true in .env to hand auth
off to it. That coupling was the root cause of a real live exposure:
a box where Authelia protection was accepted once, but the Authelia
import/forward_auth block itself later went missing from the Caddyfile
(e.g. lost on a restore), was left with the web admin's own login off
and nothing else gating it -- extension/device data reachable with no
password at all. A remote Authelia's forward_auth also proved fragile
in practice for something that only ever needed to keep a domain's
cert alive (DNS/routing/access-rule mismatches spanning two boxes,
hard to diagnose from either one alone).
This domain now just serves a minimal keep-alive page (a bare "OK" 200
response) so Caddy can still issue/renew the SIP TLS cert -- cert
issuance only needs Caddy to own the site block, it's unrelated to
what the block serves. Auth is now optional Basic Auth handled
entirely inside Caddy itself, no external subrequest, so it can't fail
this way. Asterisk's own web admin is no longer exposed publicly by
this function at all -- reachable only via the CLI:
docker exec -it <container> easy-asterisk
The Security Dashboard's own domain-takeover offer
(_secdash_offer_asterisk_domain in security-dashboard.sh) is the
supported way to put something meaningful on this domain instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
New installs now name the container "asterisk", matching every other
service's container_name == service name convention, instead of
reusing the vendored easy-asterisk CLI tool's own name (which stays
/usr/local/bin/easy-asterisk inside the container, unrelated and
unchanged).
An existing "easy-asterisk" install is never silently renamed: every
place that resolves the container name (_asterisk_resolve_layout in
asterisk.sh, plus the duplicated copies in security-dashboard.sh,
sms-inbound.sh, pstn-trunk.sh, and tools/pstn-test-check.sh's docker ps
detection) now reads it from the box's own docker-compose.yml instead
of assuming it, falling back to "asterisk" only when there's no
existing install to read. Migrating a live box to the new name is a
one-time manual action (edit docker-compose.yml's container_name for
Asterisk and its coturn sidecar, docker compose down + up -d); every
sibling service then picks it up automatically on its next run.
The DigitalOcean-droplet layout (asterisk-digital-ocean directory,
easy-asterisk-do container) is untouched by this - that naming stays
exactly as documented for pre-merge droplet installs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
keep_alive_interval is a type=global pjsip.conf option, not a
type=transport option -- it never existed on [transport-tls] on any
Asterisk version. The IONOS TLS-keepalive mitigation was inserting it
there, which made sorcery reject the whole transport-tls object
("Could not find option suitable for category 'transport-tls' named
'keep_alive_interval'"), silently killing TLS SIP entirely instead of
just adding a keepalive.
Both _asterisk_patch_keepalive_vendor_files (deployed vendor copies)
and _asterisk_ensure_live_keepalive (live pjsip.conf) now target
[global]/type=global, and both self-heal a box that already picked up
the bad placement by removing it from [transport-tls] first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
The stack health check's Security Dashboard/sms-inbound/ntfy checks only
had visibility into this box's own local Caddyfile — but all three can
legitimately be fronted by a Caddy (and Authelia) on a completely
different box instead, the same remote-Caddy pattern sms-inbound.sh and
ntfy.sh's own installers already support via CADDY_MODE/CADDY_REMOTE_HOST.
A site explicitly configured for remote Caddy was getting a false "Caddy
has no site block for it" for each of them, with a fix offer that would
have been actively wrong: adding a redundant local Caddy block for
something deliberately fronted elsewhere.
Now resolves the same site-wide CADDY_MODE the affected services'
installers themselves use before treating "not found locally" as a real
issue — only counts it, and only offers a fix, when the site is actually
in local Caddy mode. Remote (or no-Caddy) mode gets a plain informational
line instead: not wrong, just not something this box can verify.
Verified: default/local mode still flags a genuinely unwired dashboard as
an issue with a fix prompt; CADDY_MODE=remote (even with a local Caddy
directory also present) correctly downgrades the same finding to
informational with no prompt and no issue counted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
"update" mode's whole promise is leaving already-configured things
alone — but that assumption silently breaks when something was
configured but never fully wired up, and update never re-asks the
questions that would reveal it. This session hit three separate
instances of exactly that on one droplet revert: a domain set with no
Caddy block, a Caddy block with no synced TLS cert (transport-tls fails
to bind — "Unable to retrieve PJSIP transport 'transport-tls'", breaking
every call), and a baked-in external IP left over from before the box
moved. sms-inbound and (potentially) Security Dashboard/ntfy can have
the identical "domain set, nothing serving it" gap with no way to
discover it either, since their own update modes don't re-ask.
_asterisk_run_stack_health_check(), called every "update", replaces the
narrower Caddy-only check added last time:
- Compares pjsip.conf's baked external_signaling_address against this
box's actual current public IP; offers to rewrite it and restart.
- Checks Asterisk's own DOMAIN_NAME has both a Caddy site block and a
matching TLS cert in the container; offers to fix each independently.
- Checks Security Dashboard / sms-inbound / ntfy (whichever are
installed) for a matching Caddy site block, via a new lib/common.sh
helper (caddy_domain_for_upstream) that finds the block without
needing to already know the domain — none of these three services
persist it anywhere. Points at that service's own "Full reinstall"
(the only mode that re-asks) since fixing their config isn't this
file's to script.
The cert-sync fix needed a non-interactive hook into the vendored
easy-asterisk CLI, which only exposed it as an interactive menu item
(Server Settings -> Force re-sync Caddy certs). Added a --sync-caddy-cert
flag via _asterisk_patch_cert_sync_cli(), patching the deployed vendor
copy the same way _asterisk_patch_voicemail_vendor_files and friends
already do — never vendor/ in git.
Every check runs unconditionally (never opt-in, so a gap is never missed
by nobody thinking to ask); every fix is individually opt-in and named
as a real config change, unlike the rest of "update"'s no-side-effects
default.
Also factored the DO-metadata/ifconfig.me/hostname-I public-IP detection
chain (previously duplicated 3 times) into _asterisk_current_public_ip().
Verified: caddy_domain_for_upstream against a multi-block Caddyfile
(distinguishes same-prefix upstreams correctly); the full health check
against fake docker/curl across every combination (all wired, IP
mismatch declined/accepted, cert mismatch declined/accepted, dashboard
unwired, sms-inbound wired vs. placeholder-domain, multi-instance ntfy
with one wired and one not); and _asterisk_patch_cert_sync_cli's
idempotency + resulting syntax against a real copy of the vendor script.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
"update" mode never re-asks the domain/networking/Caddy questions, on the
assumption there's already Caddy/Authelia config in place to leave alone.
That assumption breaks for an install where a domain was set at some
point (DOMAIN_NAME in .env) but Caddy never actually got a site block for
it — declined at install time, DNS wasn't ready yet, or Caddy was
reinstalled/reset separately since. Previously the only way back was a
full reinstall, which re-generates a dedicated coturn container with new
TURN credentials (every already-configured phone needs its QR re-scanned)
— a lot of blast radius just to add one missing Caddy block, and enough
that reaching for it risks the extensions/voicemail data a "fresh"
reinstall can also wipe if the wrong prompt is answered.
"update" mode now detects this specific gap (domain set, no matching
Caddyfile block) and offers to run _asterisk_configure_caddy_public()
right there — the same function "fresh" installs use, but it only ever
touches the Caddyfile and .env's WEB_ADMIN_AUTH_DISABLED line, never
coturn/extensions/anything else "update" already promises not to touch.
Verified in isolation: offers and calls the fix when the domain is set
with no matching Caddyfile block, stays silent when a block already
exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
Sipnetic clients on the user's home WiFi/VLAN lose SIP/TLS registration
every ~5s when Asterisk runs on IONOS, but never on DigitalOcean or over
mobile data — CrowdSec, OPNsense firewall/IDS, TURN-for-registration, and
raw packet loss have all been ruled out via live testing. Leading theory
is an idle-connection timeout inside IONOS's network virtualization layer.
keep_alive_interval sends a periodic double-CRLF over the TLS transport to
keep it from going idle, the standard mitigation for this failure class.
Follows the existing dual-patch pattern (vendor-template copy + live file)
since transport objects aren't picked up by `pjsip reload` and need a
container restart to apply, same as the live_dangerously fix.
AST_CONFIG() silently returns an empty string instead of erroring when
asterisk.conf's [options] section lacks live_dangerously = yes, so a
correct tier_out=full in pstn-permissions.conf still evaluates as no
permission — every outbound/ring-group call gets denied with nothing
in the logs pointing at the real cause. Easy Asterisk's vendor default
ships without this set. Now applied automatically on fresh install and
on every "update" rebuild, restarting only when the file actually
changes.
restore replaces every file under the Asterisk directory with a fresh
extraction from the archive (rm -rf + mv in the new tree) -- every file is
a new inode, so any POSIX ACL grants the Security Dashboard holds on them
(_secdash_grant_asterisk_access's setfacl access to .env/config/logs/spool)
are gone, and were never on the new files to begin with. Confirmed live:
surfaces as "No permission to read .../.env" in the Extensions tab right
after a restore, on a box where the dashboard was already working fine.
The restore script can't fix this itself (runs standalone, no access to
services/*.sh's functions or the dashboard's service-user name), so it now
prints a reminder to re-run `sudo ./setup.sh security-dashboard` at the end
of a restore when the dashboard's systemd unit is present, plus a matching
note in the generated README.
The prior fix (930233c) wrote noload lines for app_voicemail_imap.so/
app_voicemail_odbc.so into config/asterisk/modules.conf at install time, but
vendor's docker/entrypoint.sh regenerates /etc/asterisk/modules.conf
unconditionally on every container start (same bind-mounted file) and
clobbers it within seconds — confirmed live on a fresh install with the
prior fix in place. Move the noload patch into
_asterisk_refresh_vendor_files()'s sed pass over the vendor template
instead, alongside the existing logger.conf/cert-regen patches, so it
survives the container's own regeneration. Drop the now-dead host-side
_asterisk_write_modules_conf() and its call sites.
Live-discovered bug, present on every install using this script, not
specific to any one box or extension: the easy-asterisk image ships
app_voicemail.so, app_voicemail_imap.so, and app_voicemail_odbc.so all
autoloading by default -- three alternative storage backends for the
SAME application (VoiceMail, VoiceMailMain, VMAuthenticate,
VoiceMailPlayMsg, VMSayName, the VM_INFO function, several AMI
actions), which collide registering those names against each other on
every single Asterisk start. This box's own container log showed the
exact signature on every restart: "Already have an application
'VoiceMail'" (and every sibling) followed by "app_voicemail.c:15897
load_module: Failure registering applications, functions or tests" --
app_voicemail never actually finished loading. Confirmed against
Asterisk's own documentation this session rather than assumed: this
is a known multi-backend conflict ("administrators should enable only
one module at a time"), not something specific to this repo's config.
Fix: new _asterisk_write_modules_conf, called from both the fresh-
install and update paths (matching voicemail-dialplan.conf's own
call-site pattern) alongside the other config/asterisk files, all
sharing the already-bind-mounted ./config/asterisk:/etc/asterisk
volume -- no new mount needed. noloads the two backends this repo
never configures (no IMAP/ODBC settings are ever written anywhere in
this script), leaving only the plain file-based app_voicemail.so
(the one voicemail.conf's [default] mailboxes actually target) to
load cleanly. Regenerated on every install/update, unlike
voicemail.conf, since modules.conf carries no per-install state of
its own -- consistent with how messaging-dialplan.conf/voicemail-
dialplan.conf are already handled, and added to their same chmod 644
line.
Requires a container restart to take effect on an existing install
(re-run `sudo ./setup.sh asterisk` -> Update, which regenerates this
file, then restart the container once).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Live blocker: user's actual migration is DigitalOcean droplet
(asterisk-digital-ocean/, container easy-asterisk-do) -> IONOS (plain
asterisk/, container easy-asterisk) -- exactly the case this script
didn't handle. The archive's own top-level directory name and
docker-compose.yml reflect whichever layout produced it
(_asterisk_resolve_layout's two known layouts). The previous restore
extracted straight into $PARENT_DIR, which recreates whatever name is
baked into the archive -- restoring a droplet archive onto a fresh
non-droplet install would land the data at a *second*,
wrongly-named directory (asterisk-digital-ocean) alongside the
freshly-installed one it was meant to replace, with docker-compose.yml
still naming the old project/container(s). Every service that resolves
Asterisk's layout by directory/container name (security-dashboard.sh,
pstn-trunk.sh, CrowdSec's Asterisk acquisition, Caddy) would get
confused by having two candidate layouts on disk, one of them stale
and half-wired.
Fix: extract into a scratch staging directory first. If the archived
docker-compose.yml's container_name differs from this run's own
$CONTAINER (baked in at generation time, so always correct for
whichever layout THIS box's install actually uses), rewrite the
project name, container name, and coturn container name in place
(coturn's is always "$CONTAINER-coturn" on both known layouts, so no
lookup table needed) before the data ever lands at $HERE -- never
lets the archive's own naming leak through. A same-layout restore
(most common case, or two droplet boxes, or two plain boxes) detects
no mismatch and skips the rewrite entirely, unchanged from before.
Verified against the real generated script (extracted from the
heredoc, not reimplemented): a droplet-flavored archive restored onto
a fresh plain-layout box lands at the correct single directory with
no stray second directory, and docker-compose.yml's name/container_name/
coturn container_name all correctly rewritten to the plain layout
(confirmed by diffing the actual restored file, not just checking for
absence of errors); a same-layout restore (droplet archive onto a
droplet box) confirmed to skip the rewrite entirely; the pre-existing
external-IP patch (previous commit) still fires correctly stacked on
top of the layout fix; and the extraction-failure rollback path (a
corrupt/unreadable archive) still restores the pre-restore install
untouched, verified via a marker file surviving the rollback.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
User confirmed via a live pjsip.conf on their actual DO box: both
external_media_address and external_signaling_address are literal
IPs, written by easy-asterisk at first container start (not something
this repo's install script controls directly). A straight restore of
a backup archive from a DIFFERENT box onto the new IONOS box would
leave the OLD DigitalOcean IP baked into pjsip.conf — dialplan and
PJSIP device credentials would come back fine, but RTP media (and
likely SIP signaling/registration) would stay broken, silently, since
nothing in the restore path previously touched these values.
Fix: after extracting the archive, `restore` reads the archive's own
external_signaling_address as "old IP", detects this host's actual
current public IP (same DO-metadata -> ifconfig.me -> hostname -I
fallback chain services/asterisk.sh's own install already uses), and
if they differ, rewrites every occurrence across config/ and .env
(fixed-string match, not a regex, so the IP's dots can't be
misinterpreted). Deliberately does NOT touch spool/, logs/, or lib/ —
those hold voicemail messages and call recordings, and a blind text
substitution across binary audio would corrupt it. A restore onto the
same host (e.g. rolling back a bad config change, no IP change)
leaves every file untouched — the check only fires on an actual
mismatch.
Verified against the real generated script (extracted verbatim from
the heredoc, not a reimplementation) with a full mock backup/restore
cycle: built a fixture archive with pjsip.conf's three transport
blocks (udp/tcp/tls) and .env's TURN_SERVER all hardcoded to a fake
"old box" IP, plus a fake binary voicemail file; restored it onto a
mocked "new box" with a different detected IP via a stubbed curl.
Confirmed every occurrence in both pjsip.conf and .env was correctly
rewritten to the new IP, and confirmed via byte-for-byte comparison
that the binary voicemail file was completely untouched. Separately
verified the same-IP case (mocked curl returning the archive's own
IP) makes no changes at all, matching a same-host config rollback.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Asterisk's whole state (dialplan, pjsip devices, voicemail, recordings,
.env with its coturn credential, docker-compose.yml) already lives under
one self-contained directory, so asterisk-standalone-backup.sh just tars
it — with stop/restart safety around the tar since voicemail/spool write
continuously, and a move-aside-then-extract restore that rolls back
automatically if extraction fails. Written into the install directory at
both fresh-install and update time via _asterisk_write_standalone_backup_script().
Output defaults to ~/asterisk-backups/, deliberately outside ~/docker/, so
a Kopia backup of the box doesn't also back up a backup-of-itself. Meant
for a quick pre-change snapshot or moving this PBX to a new host without
standing up the full backup stack first.
Documented in the generated README's new "Standalone backup/restore"
section. Tested against a mocked EA_DIR (fake docker/docker compose,
config/spool/voicemail files) confirming backup produces a correct tar and
restore replaces content correctly with rollback on extraction failure.
Confirmed live on a box that retired the shared coturn service in favor
of each service running its own dedicated/embedded coturn permanently:
Mattermost's embedded-coturn fallback used relay range 49153-49352,
which overlaps Asterisk's embedded coturn range (49152-49252) by ~100
UDP ports. Both run network_mode: host, so with shared coturn out of
the picture this is the exact same collision CLAUDE.md documents as the
original, already-fixed-once bug that the shared coturn service was
built to solve in the first place — reintroduced here because
Mattermost's embedded-coturn fallback path apparently never got checked
against Asterisk's numbers when it was written.
Moved Mattermost's embedded relay range to 49253-49452 (same 200-port
width, now contiguous with and non-overlapping Asterisk's 49152-49252).
Updated the docker-compose command flags, the matching UFW rule, and
added a comment explaining the offset so it doesn't drift back into
collision — and noting the known residual gap this doesn't cover: two
Mattermost instances *both* falling back to embedded coturn at once
would still collide with each other on these same fixed numbers. Not
fixed here since it requires more than one Mattermost instance to be
running without shared coturn at the same time, which isn't this box's
situation; flagged in-code for whoever hits it.
Also made asterisk.sh's generated README port table stop unconditionally
claiming a TURN relay range it isn't actually publishing when the shared
coturn service (not this install's own container) is fronting TURN
instead — it now branches on USE_EMBEDDED_COTURN, which the function
already receives as a parameter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Shared coturn (services/coturn.sh, ensure_coturn_user in lib/common.sh) is no
longer an installable or usable option anywhere in this repo. It's moved to
attic/coturn.sh (with tools/coturn-test-check.sh alongside it), which is
outside setup.sh's services/*.sh glob, so it never registers, never appears
in the menu, and `sudo ./setup.sh coturn` now fails with "unknown service".
Asterisk and Mattermost each already had an opt-out to run their own
dedicated coturn instead of the shared one; that opt-out is now the only
behavior — the shared-coturn preference, the opt-out prompt, and every
ensure_coturn_user() call site are gone. find_free_coturn_range()
(lib/common.sh) is what makes unconditional dedicated coturn safe: it scans
every coturn-owning service's own .env on the box for already-claimed relay
ranges and picks one that can't collide, so Asterisk + any number of
Mattermost instances can each run their own coturn on one box without the
relay-port collisions this repo's coturn history warns about.
Existing installs still pointed at a shared coturn container are left
running as-is on `update` (no silent migration attempt against a service
that no longer exists to heal against) — a full/fresh reinstall is the
migration path, which generates a new dedicated coturn with fresh
credentials and says so.
Also updates CLAUDE.md's coturn guidance for future service authors,
attic/README.md with the retirement rationale, and stale
services/coturn.sh path references in services/asterisk.sh,
tools/pstn-test-check.sh, README.md, and docs/vps-sizing-recommendations.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crt4ymNEHEbWqscB1qvZgC
Mattermost's own embedded-coturn fallback hardcoded the same relay range
(49153-49352) for every instance, with no per-instance offset -- running
two Mattermost instances without the shared coturn service (or one
alongside Asterisk's own dedicated coturn, now possible via the prior
commit) would silently reproduce the exact pre-merge collision bug this
repo's coturn history warns about, just among Mattermost instances
instead of Asterisk/Mattermost.
Adds find_free_coturn_range() (lib/common.sh, standalone-mode-stubbed in
both services matching the existing port_in_use/find_free_port
convention): a coturn relay range can't be collision-checked with live
socket scanning the way a single port can -- coturn only opens ports
inside its configured range on demand, so an idle range looks the same
as an unclaimed one to ss/netstat. The only reliable check is reading
what every other coturn-owning service's .env on the box actually
claims (COTURN_MAX_PORT for the shared instance, TURN_MAX_PORT for each
dedicated one) and picking a range starting safely past the highest.
Also adds Mattermost's own opt-out prompt for the shared coturn
preference, matching the one just added to Asterisk (fresh-install-only,
never re-asked on update, same as every other coturn-shape decision in
that file). An update now explicitly preserves its existing dedicated
range from .env rather than silently recomputing a new one.
Verified end-to-end: a shared instance + Asterisk's dedicated coturn +
two independent Mattermost instances, each discovering and avoiding
every range already claimed by the others, land on entirely
non-overlapping port blocks.
Answers "can Asterisk run its own coturn while Mattermost keeps using
the shared one" -- yes, but not safely until now: Asterisk's dedicated
coturn hardcoded relay ports 49152-49252, entirely inside the shared
instance's own default range (49152-49452). Running both on the same
box (now possible via the previous commit's opt-out prompt) recreated
the exact pre-merge collision this repo's coturn history warns about.
When a shared instance is present, read its actual configured
COTURN_MAX_PORT from ~/docker/coturn/.env and pick a dedicated range
starting safely past it, so the two can never overlap regardless of
what the shared instance was configured with. No shared instance on
the box means no collision risk, so the historical 49152-49252 default
is left untouched in that case.
Threaded the computed range through every place it was previously
hardcoded: the coturn container's own --min-port/--max-port, the UFW
rule, the DigitalOcean Cloud Firewall rule list, the non-DO firewall
reminder, and the generated README's port table. Verified the
range-shift arithmetic directly: a shared instance configured up to
49452 shifts the dedicated range to 49502-49602 (clear); no shared
instance leaves it at the original default.
ensure_coturn_user always preferred the shared coturn service with no
override once it was reachable -- there was no way to deliberately run
Asterisk's own dedicated coturn again short of stopping the shared
service outright (which would also break every other consumer, e.g.
Mattermost Calls). Useful for reproducing an older install's exact
shape when troubleshooting anything that might be specific to the
coturn-sharing path. Only offered when a shared instance actually
exists, and only reachable via an explicit fresh reinstall, matching
this repo's existing rule that coturn shape never changes silently on
an update.
The read-only bind mount that lets Asterisk's entrypoint auto-sync a
real Let's Encrypt cert from Caddy (instead of falling back to
self-signed) was gated on USE_EMBEDDED_COTURN == true. That condition
conflated two unrelated things: Asterisk's own SIP transport-tls cert
(what this mount is actually for) and coturn's separate TURNS
capability (which the shared coturn service genuinely doesn't support,
but is irrelevant here).
Confirmed live: on a shared-coturn install with a real Caddy-issued
cert already sitting on disk for DOMAIN_NAME, Asterisk kept generating
a self-signed cert on every restart anyway, because /caddy-data was
never mounted into the container -- sync_caddy_cert() had no cert store
to find. Most SIP/TLS clients refuse a self-signed cert outright with
no clear error, which was the actual cause of a "port's open, cert
domain matches, registration still silently fails" case where every
other layer (firewall, coturn reachability, DNS, cert CN/SAN) had
already checked out clean.
Vendor's entrypoint.sh only regenerates the self-signed cert if the file
is missing or lacks a SAN extension -- it never checks whether the SAN
actually matches the currently configured DOMAIN_NAME. Since
/etc/asterisk/certs is a bind-mounted host directory, neither an update
nor a full reinstall ever wipes it, so a domain entered once (even a
placeholder, or one later changed) sticks in the cert indefinitely.
Confirmed live: a box kept presenting a cert for a stale, originally-
entered domain long after DOMAIN_NAME had changed and a full reinstall
had run in between. Most SIP/TLS clients refuse a mismatched cert
outright with no clear error, which was the actual cause of a
"port's open but registration still fails" case -- firewall, coturn, and
DNS had all already checked out clean.
Patches the vendored entrypoint.sh (same guarded-sed pattern as the
existing logger.conf patch) to also regenerate when the existing cert's
SAN doesn't include the current DOMAIN_NAME. Verified against a scratch
copy: missing cert regenerates, a cert already matching the domain is
left alone, a mismatched domain now correctly regenerates and then
stabilizes.
FQDN mode was already available outside DigitalOcean detection (the
home/LAN path's "Networking mode" menu offers it), but only DO installs
got any reminder about a network-edge firewall sitting in front of the
box -- non-DO public VPS installs got no equivalent, and UFW being wide
open gives no signal that a separate provider-managed firewall exists at
all.
Confirmed live on an IONOS VPS: UFW allowed every SIP/TURN/RTP port,
Asterisk's own PJSIP logger showed zero incoming packets, and nothing in
the installer's own output pointed at the cause -- IONOS's own network
firewall (Cloud Panel -> Networking -> Firewall Policies) only allowed
22/80/443/8443/8447 and silently dropped the rest before it ever reached
the box.
Adds _asterisk_remind_non_do_firewall(), fired whenever a fresh install
sets a public FQDN without being in DO/droplet mode: same port list as
what UFW just opened, plus a pointer at the IONOS console location as a
concrete example other providers can generalize from.
Extends the security-dashboard prototype to the shared-coturn trio, since
these three are exactly the case that pattern was built for — a fresh
reinstall of any of them today just overwrote files in place without
stopping old containers first, and coturn's own fresh path never made an
informed choice about the consumer credentials/database it happens to
leave alone (safe today, but by omission rather than design).
- asterisk.sh / mattermost.sh: "Full reinstall" now stops the existing
containers (`docker compose down`) before falling through to the normal
install flow, and asks a single explicit question — delete stored data
(PBX config/spool/voicemail for Asterisk; Postgres db/uploads/config/
plugins for Mattermost) — defaulting to preserve. Their shared-coturn
TURN credential is deliberately left alone either way (reused from
cache via ensure_coturn_user(), same as update) — it's not this
service's own data, and coturn already handles that continuity.
Mattermost's existing "_db_has_data" check already reads the
filesystem to decide whether to reuse or regenerate DB_PASS, so the
wipe/preserve choice composes with that for free — no separate flag
needed. Asterisk's warns to re-run pstn-trunk afterward if data is
wiped, since that's what actually goes stale (its dialplan patch),
not the fabricated "AMI secret" framing an earlier draft of this
warning used before I checked the actual code.
- coturn.sh: "Full reinstall" now lists which consumers are currently
registered (from users/*.env) and asks explicitly whether to also
wipe TURN credentials and the user database, instead of silently
preserving them as an unexamined side effect of never deleting the
directory. Defaults to preserve. If the operator does choose to wipe,
the running container is restarted afterward — it holds the old,
now-deleted turndb file open, so new turnadmin writes to the fresh
file would otherwise go unseen until a restart anyway. Every affected
consumer already self-heals a missing credential on its own next
Update run via ensure_coturn_user()'s existing cache-miss path — no
changes needed there, just confirmed it covers this case.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Two-part change discussed and scoped in this session before touching
anything:
1. Rename "Reinstall in place" (r) -> "Update" (u) and "Full install" (f)
-> "Full reinstall" everywhere the prompt appears: lib/common.sh's
shared prompt_reinstall_mode(), plus the three services that carry
their own duplicated standalone-stub copy of it for standalone
execution (asterisk.sh, coturn.sh, wordpress.sh — per this repo's
documented standalone-bootstrap pattern). Internal state values
(update/fresh/cancel) are unchanged, so no other service's case
statement needed touching. docs/anveo-direct-setup-guide.md's `r`
reference updated to `u` to match. attic/asterisk-digital-ocean.sh
deliberately left alone — this repo's own policy is to not backport
fixes into attic/.
2. security-dashboard.sh's "Full reinstall" now does a real teardown
before reinstalling — stops and removes the systemd unit, sudoers
grant, Caddy site block, and secdash system user, then proceeds
through the normal fresh-install flow — instead of just overwriting
files in place while leaving the old service running underneath.
Prototype for a pattern discussed for other services later: split the
destructive question out explicitly ("also delete
dashboard-admins.conf — per-admin extension scoping?", default n) so
full reinstall doesn't silently discard state a plain "start over"
request wouldn't expect to lose. Verified the backup/restore mechanics
(mktemp, copy out before teardown, copy back after) against a mock
under `set -u` for both the preserve and wipe paths before shipping.
Update mode was already the strongest existing example of surfacing
newer optional prompts (its "Reconfigure Caddy protection?" /
"Reconfigure per-admin scoping?" sub-prompts already cover every setting
fresh-install offers) — no changes needed there for this service.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Direct follow-up to the previous commit's ensure_coturn_user() fix: that
fix is useless for Asterisk specifically unless something actually calls
ensure_coturn_user("asterisk") again, and the update ("Reinstall in
place") branch returns 0 well before the fresh-install path's call to it
— only "Full install" reached it, which re-prompts everything (droplet
detection, domain, etc.) just to fix a credential re-registration.
Added the same call to the update path, gated on NOT having an embedded
coturn (checked via the existing _HAD_EMBEDDED_COTURN detection) — calling
it unconditionally would silently chain-install the shared coturn service
for a box deliberately running Asterisk's own dedicated coturn, exactly
the kind of silent update-time migration CLAUDE.md's coturn guidance
warns against. .env stays untouched either way (self-heal re-registers
with the same cached password, never generates a new one).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Asterisk side (services/asterisk.sh): a [voicemail-access] context reachable
from every extension (*97 checks your own mailbox, *98<ext> drops a message
into another mailbox directly), gated live via AST_CONFIG() on a new
"voicemail" flag in pstn-permissions.conf. voicemail.conf gets a skeleton
[general]+[default] at install/update, then stays dashboard-owned from
there — mailbox lines are never regenerated wholesale by asterisk.sh once
the file exists, matching every other install-time-vs-dashboard-owned file
split in this repo (.env, firewall rules, etc). Vendor files (entrypoint.sh,
easy-asterisk.sh) get patched the same way messaging-dialplan.conf already
does, including the live-extensions.conf patch for boxes with existing
devices.
While tracing the right #include anchor for this, found and then reverted a
theoretical "fix" to messaging's own #include position: pstn-trunk.sh's own
comment (live-confirmed 2026-07-24) directly contradicts the textbook
Asterisk #include semantics I'd assumed, so the safer move was keeping
messaging's anchor exactly as already verified working and using the same
position for voicemail's own #include.
Dashboard side (services/security-dashboard.sh): write_voicemail()/
_apply_voicemail_flag() toggle the flag and a PIN (generated once, kept
across future toggles), regenerate_voicemail_conf() keeps voicemail.conf's
[default] section in sync, and a module reload takes effect without a full
Asterisk restart. Extensions tab gets a Voicemail column next to Messaging,
showing the PIN once generated. New Voicemail tab lists every mailbox's
messages (parsed from Asterisk's own msgNNNN.txt sidecars) with an inline
<audio> player per row — /voicemail/audio validates ext/msg against strict
regexes plus a resolved-path containment check before ever opening a file.
Dashboard gets read-only ACL + systemd ReadOnlyPaths access to the
voicemail spool dir, and a new sudoers-scoped module-reload command.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Extracts the swapfile logic out of services/asterisk.sh (previously
DigitalOcean-droplet-gated) into lib/common.sh's ensure_swapfile() —
provider detection was never really the point, the actual condition
that matters is "modest RAM, no swap yet," which applies just as much
to a non-DO VPS running several Docker services at once as it did to a
single-purpose droplet.
- lib/common.sh: new ensure_swapfile(), same fallocate/mkswap/fstab/
swappiness logic as before, threshold raised from 2048MB to 4096MB
(a 4GB box running a full service stack is exactly the case that
motivated this change — the old threshold would have skipped it).
- services/base.sh: calls it unconditionally so every install gets the
same check regardless of which other services get chosen.
- services/asterisk.sh: swapfile call is no longer gated behind
IS_DO — calls the shared helper directly. Kept a standalone-mode
stub (same pattern as this file's other stubbed helpers) so
`sudo bash asterisk.sh` with no base.sh in the picture still gets
it. Idempotent either way: a box that already has swap, or already
got it from base.sh earlier in the same run, no-ops immediately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBtExJcqxnokyZZKmphdug
Surfaces pstn-trunk.sh's existing pstn-trunk-calls.log (already recording
every PSTN call's numbers, just never shown on the dashboard) plus two new
metadata-only logs: sip-messages.log for internal SIP MESSAGE
deliveries/denials (asterisk.sh) and pstn-sms.log for SMS-over-SIP arrivals
(pstn-trunk.sh). No message bodies are ever logged. The dashboard reads all
three via a new /api/pstn-calls and /api/comms-texts pair, sharing a common
bounded tail helper with the Security Log parser.
Asterisk and Mattermost each used to embed their own dedicated coturn
container (network_mode: host), and their default relay port ranges
overlapped by ~100 UDP ports — running both on one box meant a
coin-flip over which service's active call lost its media relay.
- services/coturn.sh: new shared TURN/STUN relay, one instance for
every consumer instead of one each. Runs --lt-cred-mech with a
SQLite user database (not --use-auth-secret — coturn doesn't
support both auth mechanisms on one instance at once, confirmed via
coturn's own upstream docs/issues) so each consumer gets its own
dedicated username/password without stepping on any other's.
- lib/common.sh: ensure_coturn_user() — chain-installs coturn.sh on
first need (same declare -F guard pattern as the existing
asterisk -> security-dashboard chaining) and registers/reuses a
per-consumer credential, mirroring configure_caddy_for_service's
out-param convention.
- services/asterisk.sh: _asterisk_write_compose gains a
USE_EMBEDDED_COTURN flag. New installs use the shared service;
existing installs keep their dedicated coturn exactly as-is on
every "update" (detected from the existing compose file before
regenerating it, so a rebuild can never silently drop the container
its own .env TURN_PASSWORD still points at) and only switch on an
explicit "fresh" reinstall, with a warning first.
- services/mattermost.sh: same embedded/shared coturn handling, plus
genuine multi-instance support (separate dir/containers/DB/ports per
instance, auto-scanned free ports for extras) for real isolation
between groups, as opposed to Team Edition's built-in Teams feature.
Calls plugin TURN config switched from the HMAC "TURN Static Auth
Secret" field to the verified "ICE Servers Configurations" JSON
field, which accepts the same fixed username/credential shared
coturn issues. Also fixes a latent bug found while adding proper
update-mode detection: DB_PASS/MM_SECRET were regenerated on every
single rerun with no existing-install check at all, silently
breaking Postgres auth on any reinstall.
- CLAUDE.md: documents the ensure_coturn_user pattern (including the
auth-mechanism constraint and the embedded-coturn migration-safety
rule) for any future service that needs TURN.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQkdAn3iG5A4WoqU9FHMaN
services/asterisk-digital-ocean.sh was a near-verbatim copy of
services/asterisk.sh — same vendor refresh, compose template, messaging
dialplan, presence alerts, UFW rules and dashboard/trunk chaining, with the
helper functions renamed _asterisk_do_*. Two copies meant every fix had to
land twice, and several never did.
There is now one `asterisk` service. It reads the DigitalOcean metadata
service and asks either way (so a droplet with metadata blocked, or another
provider's public VM, can still opt in), then gates the genuinely
droplet-specific behaviour on that one answer: swapfile for low-RAM plans,
public-FQDN-only setup with no LAN/VLAN prompts, a Caddy site block pinned
to that FQDN, the remote-Authelia option, and the doctl Cloud Firewall.
Two things that were droplet-only for no real reason now apply everywhere:
the entrypoint patch that writes security-level events to logs/full, and
the logrotate config for that file. Without them the Security Dashboard's
Security Log tab and CrowdSec's Asterisk acquisition were silently empty on
every home/LAN install; crowdsec.sh now detects either install directory.
Existing droplets are left alone: an install at ~/docker/asterisk-digital-ocean
keeps its directory and easy-asterisk-do container names, since its Caddyfile
block, UFW rules, Cloud Firewall, CrowdSec acquisition and PSTN trunk all
name those exactly. New installs use ~/docker/asterisk / easy-asterisk.
`sudo ./setup.sh asterisk-digital-ocean` still works via a new SERVICE_ALIAS
map in setup.sh, without a second menu entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
asterisk.sh and asterisk-digital-ocean.sh now offer, at the end of both
their fresh-install and update-mode paths, to also set up the Security
Dashboard and configure a real PSTN trunk in the same run - one script
walks through the whole stack instead of needing to separately remember
and run `sudo ./setup.sh security-dashboard` / `sudo ./setup.sh pstn-trunk`
afterward.
Both target services keep their own register_service call and stay fully
independently invocable - this is purely an additive convenience layer
(_asterisk_offer_dashboard_and_trunk / _asterisk_do_offer_dashboard_and_trunk),
not a replacement. An already-installed piece is silently refreshed
(install_security-dashboard/install_pstn-trunk each already have their own
update/fresh/cancel reinstall-mode gate, so calling them again just does
the right thing); a not-yet-installed piece gets one y/n instead of every
detailed prompt firing.
Guarded with declare -F so a standalone `sudo bash asterisk.sh` copy (no
sibling services/*.sh files sourced) skips both cleanly with an
explanatory message instead of erroring on an undefined function.
Verified: full sourcing simulation resolves all four install_* functions
correctly, `setup.sh --dry-run --unattended asterisk` and
`asterisk-digital-ocean` both complete cleanly end-to-end, and
`setup.sh --list` still shows all four services as independently
selectable.
Documented the pattern in CLAUDE.md under a new "Chaining into another
service from within your own" section for future contributors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
Supersedes the previous commit's reverse-proxy approach entirely: instead
of Caddy routing to Easy Asterisk's own separate vendored web admin
process, the dashboard now reimplements that admin's functionality
natively - one process, one page, real tab-switching, no separate app to
proxy, patch, or embed. Reverts asterisk.sh/asterisk-digital-ocean.sh's
WEBADMIN_BASE_PATH vendor patching and Caddy-skip logic back to their
pre-proxy state (confirmed identical via diff) since neither is needed
anymore.
security-dashboard.sh additions:
- ea_* functions covering full device/category/room parity with
vendor/easy-asterisk/easy-asterisk-v0.10.0.sh's own web admin: list/add/
delete/rename/change-category for devices, list/create/delete/rename for
categories, list/create/delete/rename/add-member/remove-member for
rooms, plus live registered/unregistered status. Reads go straight
through the host-side bind-mounted config files (same as the existing
list_extensions() already does for pjsip.conf); writes go through
`docker exec -i <container> tee <path>` instead of a direct host-side
write, since Easy Asterisk's container writes these files as its own
internal user and a host-side write would just be fighting that
ownership again on the next container restart.
- Found and fixed a real bug (inherited from the vendored admin's own
template, not introduced here): a plain non-mobile LAN device leaves
both the keepalive and ice template lines empty, producing two
consecutive blank lines inside the endpoint's pjsip.conf stanza instead
of one - which broke the delete/rename/category-change parsers' "blank
line ends this device's block" boundary detection, leaving an orphaned
tail of config behind on delete. Fixed by building the endpoint block
from a filtered line list instead of positional template blanks.
Confirmed via a full synthetic add/rename/category-change/delete cycle
against realistic pjsip.conf/categories.conf/rooms.conf fixtures (with
docker exec mocked to a local file) - round-trips back to the original
fixture correctly.
- New plumbing: _secdash_grant_asterisk_access grants read-only access to
categories.conf/rooms.conf's directory (separate from pjsip.conf's,
confirmed against the vendored source - /etc/easy-asterisk/*, not
/etc/asterisk/*); _secdash_write_sudoers adds six exact (no wildcards)
docker-exec sudoers entries scoped to the one Asterisk container
actually installed, validated live with visudo -c; _secdash_write_systemd_unit
passes the new ASTERISK_EA_CONFIG_DIR/ASTERISK_EA_CONTAINER env vars and
adds the config dir to ReadOnlyPaths, validated live with
systemd-analyze verify.
- New UI: Asterisk Admin tab with Devices/Categories/Rooms cards, sortable
tables matching the existing style, inline category-reassignment
dropdowns, and per-room member chips with an inline add-member picker.
Nav button visibility now checks live container reachability
(/api/ea-status) instead of just Asterisk-install detection.
Still unverified: the actual `docker exec` calls (module reload, dialplan
rebuild, live status) against a real running Easy Asterisk container -
only the file-parsing/transformation logic itself has been exercised, via
mocked writes, not the real container plumbing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
No more cross-origin iframe: the Security Dashboard now reverse-proxies
the real Asterisk web admin natively at /asterisk-admin/ on its own
domain via Caddy's handle_path, instead of embedding a separate site in
a frame. One domain, one login wall, for both.
- services/asterisk.sh / services/asterisk-digital-ocean.sh: patch the
vendored web admin's one hardcoded absolute API path
(`const API_BASE = '/api'`, confirmed via the real vendored source to
be the only absolute-path reference anywhere in its HTML/JS - no other
hrefs, no login-page redirect, plain HTTP Basic Auth instead) so it
resolves correctly when mounted under a sub-path, via a new
WEBADMIN_BASE_PATH env var threaded through entrypoint.sh. Verified
against the real vendored file: patched output is
'/asterisk-admin/api' with the env var set, unchanged '/api' without
it. Skip each service's own dedicated admin Caddy domain when the
Security Dashboard is already installed, since it fronts the admin
instead.
- services/security-dashboard.sh: _secdash_configure_caddy now accepts
the admin's port and Asterisk's own directory/domain, path-routes
/asterisk-admin/* alongside the dashboard's own handle{} block, and
writes WEB_ADMIN_BASE_PATH into Asterisk's .env + restarts that
container once proxying is confirmed live. Defaults the dashboard's
own domain prompt to the droplet's DOMAIN_NAME when detected, since
Caddy's SIP-TLS cert sync already depends on serving that exact
domain. Removed the old CSP frame-ancestors patching and the iframe
itself; the nav is now a plain link, shown only once the proxy is
confirmed wired up.
- Fixed _secdash_remove_caddy_block's marker match to tolerate the
dashboard's reverse_proxy line now living one indent level deeper
(inside its own handle{} block) - verified against a synthetic
Caddyfile that it still finds and removes exactly the right block.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
Ports the internal SIP MESSAGE dialplan enforcement, extension presence
(online/offline) ntfy alerts, and the live-config-include patching from
asterisk-digital-ocean.sh, for feature parity between the two Asterisk
flavors. The pstn-trunk.sh live-include fix already applied to both, since
that's shared code parametrized by which flavor is installed.
Confirmed live: a bare `ufw delete allow <port>` closes it on every
interface, including the caddy_net bridge — Caddy's own request to
host.docker.internal:PORT is ordinary INPUT-chain traffic as far as
UFW is concerned, not something that bypasses it just because the
source is a local container. Closing the port outright silently took
Caddy's reverse-proxy path down with it.
Added ufw_allow_from_caddy_net() to scope the port to caddy_net's own
subnet instead of leaving it fully closed — reachable from Caddy,
still closed to the public internet. Wired into both
asterisk-digital-ocean.sh and asterisk.sh in place of the plain
delete.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
Mirrors the fixes just made in asterisk-digital-ocean.sh:
- Reordered so the Caddy reverse-proxy decision happens before the
UFW rules are built, using the new CADDY_SERVICE_CONFIGURED/
CADDY_SERVICE_MODE signal from configure_caddy_for_service() to
skip opening the web admin port on the LAN when a local Caddy is
already fronting it (still opens it for a remote Caddy machine,
which needs LAN access to reach this host directly).
- Calls the new ensure_ufw_enabled() so UFW actually enforces the
rules this script adds, instead of leaving them queued but inert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
Replaces the y/n "update in place?" prompt in asterisk.sh/asterisk-do.sh
with an explicit r/f/c choice — (r)einstall in place, (f)ull install,
(c)ancel — defaulting to cancel on a bare Enter (or Ctrl-D) instead of
falling through to a destructive full reinstall.
Adds prompt_reinstall_mode to lib/common.sh (plus matching standalone
stubs in both asterisk scripts for when they run without the full repo)
and documents the convention in CLAUDE.md: any service with a persistent
install directory should offer this choice on rerun instead of re-asking
every prompt just to pick up a script fix.
Re-running either installer on an existing install used to re-ask
every prompt (domain, extras, firewall, Authelia) just to pick up a
script fix like the exports mount. Both now detect an existing
docker-compose.yml + .env and offer to update in place instead: only
vendor files and docker-compose.yml are refreshed and the stack is
rebuilt, leaving .env, firewall rules, and Caddy/Authelia config
untouched.
The vendor-copy and docker-compose.yml generation blocks (previously
inline and duplicated between what would have been two near-identical
code paths) are factored into per-file helper functions
(_asterisk_do_refresh_vendor_files/_asterisk_do_write_compose and
_asterisk_refresh_vendor_files/_asterisk_write_compose) so fresh
installs and updates share one copy of the logic instead of drifting
apart — the same problem that caused the /root export path and the
vpn-diagnostics.sh COPY bug to slip through unevenly between the two
services in the first place. Names are per-file since setup.sh sources
every services/*.sh into one process.
The vendor easy-asterisk script hardcodes /root for both export output
and its import file listing, but nothing was mounted there — exports
were being written to the container's ephemeral filesystem and lost on
recreate. Bind-mount ./exports to /root in both asterisk.sh and
asterisk-do.sh so exports/imports land under ~/docker/<service>/exports
on the host.
Confirmed on a real deployment: the template Caddyfile ships with
"admin off" (deliberate — no local API attack surface), which means
`caddy reload` can never work, since it depends on that same admin
endpoint. Every Caddyfile-editing code path was silently failing to
apply changes as a result — `docker logs caddy` showed
"admin endpoint disabled" and the reload command errored, but the
Caddyfile edit itself (which doesn't need the admin API) had already
succeeded, leaving the running config stale until something else
happened to restart the container.
Fixed in the two places that actually matter here: lib/common.sh's
configure_caddy_for_service (used by asterisk.sh and most other
Caddy-fronted services in the full repo) and asterisk-do.sh's own
self-contained Caddy block (both the standalone-bootstrap stub and the
main path). Each now tries the lightweight reload first — harmless,
and still works if a box ever has the admin API enabled — then falls
back to `docker restart caddy` if that fails, rather than leaving an
edited-but-unapplied Caddyfile.
Not fixed: the same duplicated pattern in ~35 other service files that
carry their own standalone-bootstrap copy of this logic. Those only
matter for the rare single-file standalone execution path for each of
those specific services and are unrelated to tonight's actual issue —
out of scope here.
Verified: full regression run on both asterisk.sh and asterisk-do.sh
still completes cleanly end to end.
The 8080->8081 fix from the last commit just moved the collision
risk, not removed it — any hardcoded port can eventually collide with
something else on a box running several services. Both services now
scan for the first genuinely free port starting at 8081 (ss -tlnH
"sport = :$PORT", capped at 100 ports checked) and use whatever they
find — .env, UFW, the DO Cloud Firewall rule, and the Caddy proxy
target all follow the actual chosen port, not a fixed number.
asterisk-do.sh's self-contained Caddy block (unquoted heredoc) reads
the port live. asterisk.sh's README heredoc is quoted (no expansion),
so its generated docs keep the static "8081" default with an added
note to check .env for the real value if it differed — the summary
echo outside that heredoc still reports the live value correctly.
Verified: normal case still lands on 8081; with 8081 deliberately
occupied by another process, both services correctly detect the
collision and fall through to 8082 instead, confirmed via the actual
generated .env in each case.
Real-world failure: CrowdSec's Local API listens on 127.0.0.1:8080 by
default (confirmed against its actual upstream config.yaml), and Easy
Asterisk's web admin also defaults to 8080. Both services in this repo
run with network_mode: host / directly on the host, so whichever one
starts second gets "OSError: [Errno 98] Address already in use" — in
this case CrowdSec (started earlier via the auto-install chain) had
already claimed the port before the web admin tried to start.
Moved the web admin's default to 8081 in both asterisk-do.sh and
asterisk.sh — WEB_ADMIN_PORT in .env, the UFW rule, the DO Cloud
Firewall rule, the Caddy reverse_proxy target, and every doc/summary
reference. 8081 doesn't collide with anything else in either stack
(5060/5061/8088/8089/3478/10000-20000/49152-49252) or with CrowdSec's
LAPI (8080) or Prometheus metrics (6060, localhost-only either way).
Left the vendor files' own internal fallback (WEB_ADMIN_PORT:-8080)
untouched — .env's explicit value overrides it at runtime regardless,
and vendor/ stays pristine per this repo's convention.
Verified: no stray 8080 in any generated .env/docker-compose.yml for
either service after a full install run; the vendor files' own
internal 8080 fallback (never applies here, since .env always sets it
explicitly) is the only remaining occurrence anywhere.
The Dockerfile COPYs scripts/vpn-diagnostics.sh and
scripts/dns-whitelist.sh into the image, but the vendor-file-copying
step in both asterisk.sh and asterisk-do.sh never copied (or
downloaded, in the GitHub-fallback branch) that scripts/ directory —
only Dockerfile, entrypoint.sh, coturn-entrypoint.sh, and the
management script. Every real install hit "docker compose up -d
--build" failing with:
failed to compute cache key: ... "/scripts/dns-whitelist.sh": not found
Confirmed live on a deployed droplet. vendor/easy-asterisk/scripts/
already has both files — this was purely a missed copy step, not a
vendoring gap. Fixed in both files identically (mkdir scripts/, copy
or curl both scripts, chmod +x alongside the existing executables).
Verified at the filesystem level: after a full install run, both
files land in the build context with correct executable permissions,
resolving the exact COPY instructions that were failing. Full
docker build verification wasn't possible in this sandbox (a separate,
unrelated network restriction blocks pulling the ubuntu:24.04 base
image here), but the missing-file root cause is directly fixed.
Defaults install_asterisk() to FQDN networking mode and prompts for VLAN/VPN
subnets (with host-network auto-detection to filter out noise like Docker
bridges) so phones on other networks get correct NAT/SDP handling from the
first boot.
The container now mounts Caddy's cert store read-only when Caddy is
installed, and the entrypoint syncs a matching Let's Encrypt cert for
DOMAIN_NAME automatically, re-checking every 12h to pick up renewals without
a restart. Falls back to self-signed only when no matching cert is found.
Also fixes a real bug hit in the field: a preserved/migrated pjsip.conf could
be missing the transport-udp/transport-tcp sections entirely, with no bind
error logged, silently blocking any device that registers without TLS. Adds
the same migration-injection already used for transport-tls.
koha.sh:
- Replace named volume `koha_db_data` with bind-mount `./data:/var/lib/mysql`
so the MariaDB database is inside ~/docker/koha/ and covered by backup.sh
- Add interactive setup wizard: collects library name, code, admin credentials,
item types (with custom additions), and shelf locations before first start
- Generate ~/docker/koha/post-setup.sh at install time; after completing the
web installer the user runs it to auto-configure the library branch, item
types, shelf locations, and system preferences via the Koha REST API
- Updated README with exact step-by-step web installer instructions and
admin password prominently displayed
asterisk.sh:
- Replace all 5 named volumes (asterisk-config, easy-asterisk-config,
asterisk-logs, asterisk-spool, asterisk-lib) with bind-mounts inside
~/docker/asterisk/ so all config and state is covered by backup.sh
- mkdir -p for all bind-mount dirs before compose up
- Replace SSH-based standalone configure_caddy_for_service stub with the
snippet approach used everywhere else (local Caddy writes Caddyfile;
remote Caddy via VPN/Netbird saves ~/docker/caddy-snippets/<name>.caddy)
https://claude.ai/code/session_01S7UecmQRG6CKTYPoBqbVLj
Mattermost Calls:
- Add 8443/udp to compose ports for the Calls plugin RTC server (WebRTC
direct path; coturn relay is only the fallback, not the sole path)
- Add 8443/udp to UFW rules and router port-forward table
- Warn that WebRTC requires HTTPS — calls silently fail over HTTP
- Prompt for Caddy domain and update MATTERMOST_SITE_URL in .env to match
the HTTPS URL before Caddy is wired (previously SITEURL was written before
the domain was known, leaving it as http://localhost:8065)
- Update README with RTC server address field and corrected port table
Asterisk web admin:
- No built-in auth: add Authelia SSO check matching CLAUDE.md pattern
- Set WEB_ADMIN_AUTH_DISABLED=true in .env when Authelia handles auth
(prevents double-login prompts)
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt