Commit Graph
44 Commits
Author SHA1 Message Date
Claude de5cfe26bd Fix app_voicemail module collision at its real source (entrypoint.sh, not host modules.conf)
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.
2026-08-16 20:14:45 +00:00
Claude 930233cc7f Fix app_voicemail module-load collision via modules.conf
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
2026-08-16 19:50:23 +00:00
Claude be684e114d asterisk-standalone-backup.sh: fix cross-layout restore (DO -> non-DO)
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
2026-08-16 02:34:30 +00:00
Claude 6dc4f0d6c6 asterisk-standalone-backup.sh: fix external IP on cross-box restore
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
2026-08-16 01:02:28 +00:00
Outis 1e96124117 Merge pull request #333 from outis1one/claude/ionos-script-integration-x32ofw
Claude/ionos script integration x32ofw
2026-08-14 15:17:07 -04:00
Claude 7efd087993 Add standalone backup/restore script for Asterisk, independent of Kopia
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.
2026-08-14 13:35:20 +00:00
Claude 158df0d545 Fix embedded-coturn relay-port overlap between Asterisk and Mattermost
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
2026-08-13 03:40:32 +00:00
Claude ca239a3886 Retire the shared coturn service — every WebRTC/SIP service now runs its own
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
2026-08-13 02:12:32 +00:00
Claude d835e0d734 mattermost, asterisk: dynamic, collision-safe dedicated coturn ranges
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.
2026-08-13 00:55:31 +00:00
Claude 85fc172377 asterisk: avoid relay-port collision between dedicated and shared coturn
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.
2026-08-13 00:27:33 +00:00
Claude 3abc46d8a9 asterisk: add opt-out for shared coturn on fresh reinstall
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.
2026-08-13 00:13:09 +00:00
Claude 65b7964411 asterisk: mount Caddy's cert store regardless of coturn mode
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.
2026-08-12 22:11:36 +00:00
Claude b76230b341 asterisk: regenerate self-signed TLS cert when DOMAIN_NAME changes
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.
2026-08-12 21:33:24 +00:00
Claude 893ab19759 asterisk: warn non-DO public installs about provider-side firewalls
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.
2026-08-12 21:09:25 +00:00
Claude aa65b5ef5b Make "Full reinstall" a real teardown for asterisk, mattermost, and coturn
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
2026-08-12 13:07:38 +00:00
Claude 7a1f09a0f7 Rename reinstall-mode prompt; make security-dashboard's "Full reinstall" a real teardown
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
2026-08-12 12:47:31 +00:00
Claude d55a7cc81a Reach the coturn self-heal check from asterisk.sh's update path too
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
2026-08-12 11:44:02 +00:00
Claude 8950810cea Add voicemail: dialplan/mailboxes in Asterisk, Extensions toggle + Voicemail tab with click-to-play in the dashboard
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
2026-08-11 05:58:42 +00:00
Outis a9a7200cb5 Merge pull request #269 from outis1one/claude/vps-capacity-assessment-r57vw3
Claude/vps capacity assessment r57vw3
2026-08-09 18:05:15 -04:00
Claude ad38b96cfe Make swapfile a default for every install, not just Asterisk droplets
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
2026-08-09 18:13:37 +00:00
Outis 39fb198c64 Merge pull request #267 from outis1one/claude/asterisk-security-phone-numbers-osu937
security-dashboard: add Calls & Texts tab for PSTN calls and SIP/SMS …
2026-08-08 21:02:58 -04:00
Claude d4ca8277a6 security-dashboard: add Calls & Texts tab for PSTN calls and SIP/SMS texts
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.
2026-08-08 02:32:42 +00:00
Claude cf3d4bf6de Extract coturn into a shared service; add Mattermost multi-instance support
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
2026-08-04 16:32:08 +00:00
Claude 8b843ca1c1 Fold asterisk-digital-ocean into asterisk with droplet auto-detection
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
2026-07-25 01:06:38 +00:00
Claude ff61e8f733 Chain Security Dashboard + PSTN trunk setup into the Asterisk install flow
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
2026-07-24 12:38:45 +00:00
Claude 6f5ed30469 Replace Caddy path-proxy with a fully native Asterisk Admin tab
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
2026-07-24 12:14:27 +00:00
Claude d449586dde Replace the Asterisk Admin iframe with a native Caddy path-proxy
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
2026-07-24 11:45:11 +00:00
Claude 02bf2fe3a2 Mirror SIP messaging + presence alerts to asterisk.sh (LAN edition)
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.
2026-07-23 17:19:31 +00:00
Claude 9f2a3ddfd9 Fix: closing the web admin port to the internet also blocked Caddy
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
2026-07-20 18:24:02 +00:00
Claude 516e0db66f asterisk.sh: same port-exposure fix and UFW enable as the DO edition
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
2026-07-20 18:20:30 +00:00
Claude 8ee35e1995 Add prompt_reinstall_mode helper and document the update/fresh/cancel convention
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.
2026-07-20 01:23:22 +00:00
Claude 0d719a5cad Add update-in-place mode to asterisk and asterisk-do installers
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.
2026-07-20 01:13:51 +00:00
Claude 21f8e0e096 Persist Easy Asterisk client exports to the docker directory
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.
2026-07-20 01:02:37 +00:00
Claude cb8ff3f4f0 Fall back to a Caddy restart when reload fails (admin API is off)
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.
2026-07-19 17:43:52 +00:00
Claude 4af9cfeae3 Auto-scan for a free web admin port instead of hardcoding one
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.
2026-07-19 17:20:56 +00:00
Claude 2483365141 Fix web admin/CrowdSec port collision: default WEB_ADMIN_PORT to 8081
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.
2026-07-19 16:51:50 +00:00
Claude edf9c7ee25 Fix Docker build failure: copy vendor's scripts/ directory
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.
2026-07-19 16:03:48 +00:00
Claude 225e277abb asterisk: default to FQDN mode, auto-detect VLANs, auto-sync Caddy certs
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.
2026-07-01 17:19:57 +00:00
Claude ec9aa605cf Fix named volumes in koha/asterisk for backup coverage; add Koha setup wizard
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
2026-06-09 18:39:27 +00:00
Claude 7c3f101fe0 Add asterisk, nextcloud, onlyoffice, mattermost services + vendor/easy-asterisk
asterisk.sh (homelab):
- Easy Asterisk PBX with self-hosted coturn TURN server
- Vendored from outis1one/easy-asterisk v0.10.0 for offline install
- LAN-only or FQDN mode (TLS + TURN relay for remote access)
- Auto-answer SIP headers for intercom use case
- Authelia SSO for web admin; WEB_ADMIN_AUTH_DISABLED=true when chosen
- UFW rules: 5060-5061, 8080, 8088-8089, 3478, 10000-20000/udp, 49152-49252/udp
- Builds custom Docker image from vendor/easy-asterisk/

nextcloud.sh (utilities):
- Custom Dockerfile: nextcloud:apache + smbclient (SMB external storage)
- MariaDB 10.11 sidecar with matching env vars
- OVERWRITEPROTOCOL/OVERWRITECLIURL/TRUSTED_PROXIES set for Caddy
- Enables files_external app after first-run init (waits up to 90s)

onlyoffice.sh (utilities):
- JWT generated once, preserved across re-runs
- _ensure_yq: auto-installs yq v4 for FileBrowser config patching
- _wire_nextcloud: idempotent occ wiring (DocumentServerUrl, jwt_secret)
- _wire_filebrowser: patches config.yaml + restarts container
- Caddy block overrides X-Frame-Options to allow iframe embedding

mattermost.sh (utilities):
- PostgreSQL 15-alpine + Mattermost Team Edition + coturn (port 3479)
- 8443/udp for Calls plugin RTC server
- coturn uses --use-auth-secret HMAC mode (required by Calls plugin)
- SITE_URL computed from SITE_DOMAIN, promptable
- UFW: 8443/udp, 3479, 49153-49352/udp

vendor/easy-asterisk/:
- All upstream source files vendored for offline/self-contained installs
- Dockerfile, docker/entrypoint.sh, docker/coturn-entrypoint.sh
- easy-asterisk-v0.10.0.sh (6929-line management script)
- scripts/vpn-diagnostics.sh, scripts/dns-whitelist.sh
- .env.example

https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
2026-06-09 00:28:38 +00:00
Claude ab778868df Fix Mattermost Calls, add Authelia to Asterisk web admin
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
2026-06-08 18:41:46 +00:00
Claude dc1552f5d3 Fix asterisk.sh: symlink→cp, management script mount, TURN_SERVER, ports
- Replace symlink with real cp for easy-asterisk-v0.10.0.sh: Docker COPY
  doesn't reliably follow symlinks; using a real copy is safer.
- Add ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro bind mount so the
  management script can be updated without rebuilding the image.
- Add TURN_SERVER to .env (empty in LAN-only mode, domain:3478 in FQDN mode)
  and reference it in compose instead of building the value inline — fixes
  malformed "":3478 in LAN-only mode.
- Add provisioning ports 8088/8089 to UFW rules; these are Asterisk's built-in
  HTTP server for Linphone XML provisioning (not the web admin, not Caddy).
- Document in README that Caddy has no role in calls: SIP/RTP use host
  networking. Caddy only proxies the web admin (8080). Provisioning ports
  (8088/8089) must be accessed directly, not through Caddy.

https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
2026-06-08 18:41:46 +00:00
Claude 0d81839c80 Vendor easy-asterisk source files; fix asterisk.sh and onlyoffice.sh
vendor/easy-asterisk/: All source files from outis1one/easy-asterisk v0.10.0
vendored so the repo is self-contained — no internet required at install time.
Includes the real Dockerfile (FROM ubuntu:24.04 + full Asterisk stack),
entrypoint.sh (IP detection, TLS cert gen, pjsip/rtp config, web admin),
coturn-entrypoint.sh (robust IP detection wrapper), and the management
script + diagnostic utilities.

services/asterisk.sh: Rewritten to copy from vendor/ instead of downloading
at runtime. Uses the upstream Dockerfile verbatim. Symlinks
easy-asterisk-v0.10.0.sh → easy-asterisk.sh for build context compatibility.

services/onlyoffice.sh: Complete rewrite with correct standalone bootstrap.
_ensure_yq() installs yq v4 automatically (arch-aware). JWT secret is
preserved across re-runs so rotating is explicit. _wire_nextcloud() and
_wire_filebrowser() run on every install invocation (idempotent), skipping
gracefully when containers aren't running rather than failing.

https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
2026-06-08 18:41:46 +00:00
Claude 9f667705ae Add Easy Asterisk PBX service with self-hosted coturn TURN server
Integrates https://github.com/outis1one/easy-asterisk into the post-install
system. Downloads the management script and coturn entrypoint at install time,
generates docker-compose.yml with host-networking Asterisk + coturn, writes
a randomised TURN password, and opens UFW ports for SIP/RTP/TURN.

Interactive FQDN setup chooses between LAN-only (UDP, no TLS) and FQDN mode
(TLS+SRTP+TURN) and prints required router port-forward instructions.

https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
2026-06-08 18:41:17 +00:00