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
This commit is contained in:
Claude
2026-07-25 01:06:38 +00:00
parent 15d70356ac
commit 8b843ca1c1
11 changed files with 971 additions and 1772 deletions
+43 -16
View File
@@ -18,7 +18,7 @@ checklist per group, and calls `install_<name>()` for each selected item.
1. Create `services/<name>.sh` (kebab-case filename) 1. Create `services/<name>.sh` (kebab-case filename)
2. Call `register_service` at the top of the file 2. Call `register_service` at the top of the file
3. Define `install_<name>()` — keep hyphens **literal** in the function name 3. Define `install_<name>()` — keep hyphens **literal** in the function name
(`install_asterisk-digital-ocean`, not `install_asterisk_digital_ocean`). (`install_pstn-trunk`, not `install_pstn_trunk`).
`setup.sh`'s dispatcher calls `install_${name}` with no hyphen→underscore `setup.sh`'s dispatcher calls `install_${name}` with no hyphen→underscore
conversion, so the function name must match the service name exactly. conversion, so the function name must match the service name exactly.
Confirmed live: a mismatched underscore here produces Confirmed live: a mismatched underscore here produces
@@ -29,6 +29,33 @@ That's it. The menu picks it up on the next run.
Also update the **Services table in `README.md`** — add the service name to the Also update the **Services table in `README.md`** — add the service name to the
appropriate group row so the README stays current. appropriate group row so the README stays current.
## Retiring a service name (merging two services)
Deleting `services/<name>.sh` removes it from the menu, but `sudo ./setup.sh
<name>` then fails outright for anyone with that name in their notes, docs, or
shell history. Add the old name to `SERVICE_ALIAS` in `setup.sh` instead —
`run_service` resolves it to the surviving service, says so once, and runs
that. The alias never gets its own menu entry, which is the whole point.
`services/asterisk-digital-ocean.sh` was merged into `services/asterisk.sh`
this way: one installer that detects a DigitalOcean droplet (metadata service,
with a y/n either way) and applies the droplet-only extras — swapfile,
public-FQDN-only flow, hand-built Caddy site block, remote Authelia, Cloud
Firewall — behind that one answer. Two lessons worth reusing:
- **Don't rename a live install's directory or containers.** New installs
land in `~/docker/asterisk` with `easy-asterisk`; a pre-merge droplet keeps
`~/docker/asterisk-digital-ocean` and `easy-asterisk-do`, because its
Caddyfile block, UFW rules, Cloud Firewall, CrowdSec acquisition and PSTN
trunk all name those exact paths. `_asterisk_resolve_layout()` picks
whichever exists, and every sibling service probes both.
- **Check whether a "flavor-specific" behavior was actually flavor-specific.**
The Asterisk security-logging patch and the `logs/full` logrotate config
were droplet-only purely because that's where they got written first — the
Security Dashboard's Security Log and CrowdSec's Asterisk acquisition were
silently empty on every home/LAN install as a result. Both now apply
everywhere.
## Minimal Docker service template ## Minimal Docker service template
```bash ```bash
@@ -175,15 +202,16 @@ the auth server's own access-control rules say. Confirmed live: this was
the actual cause of a "Caddy proxies fine but Authelia never prompts for the actual cause of a "Caddy proxies fine but Authelia never prompts for
login" bug, on a site block that otherwise looked completely correct. If a login" bug, on a site block that otherwise looked completely correct. If a
service builds its own site block instead of using this helper (e.g. service builds its own site block instead of using this helper (e.g.
`services/asterisk-digital-ocean.sh` does, deliberately, see its own `services/asterisk.sh` does in droplet mode, deliberately see
comment for why), put its auth block first there too. `_asterisk_configure_caddy_public`'s comment for why), put its auth block
first there too.
**`forward_auth` to a remote Authelia over a scheme-qualified URL needs **`forward_auth` to a remote Authelia over a scheme-qualified URL needs
explicit `header_up` pins.** A bare `forward_auth authelia:9091` (Authelia on explicit `header_up` pins.** A bare `forward_auth authelia:9091` (Authelia on
the same Docker network, one hop) is fine relying on Caddy's default the same Docker network, one hop) is fine relying on Caddy's default
`X-Forwarded-*` headers. But `forward_auth https://auth.example.com { ... }` `X-Forwarded-*` headers. But `forward_auth https://auth.example.com { ... }`
(Authelia on a *different* machine, reached over its own public domain+TLS — (Authelia on a *different* machine, reached over its own public domain+TLS —
see `services/asterisk-digital-ocean.sh`'s remote-Authelia prompt) is a see `services/asterisk.sh`'s droplet-mode remote-Authelia prompt) is a
second Caddy hop: Caddy rewrites the outgoing request's `Host` header to second Caddy hop: Caddy rewrites the outgoing request's `Host` header to
`auth.example.com` so the remote Caddy can route/SNI-match it, and without an `auth.example.com` so the remote Caddy can route/SNI-match it, and without an
override `X-Forwarded-Host` picks up that rewritten value instead of the override `X-Forwarded-Host` picks up that rewritten value instead of the
@@ -221,9 +249,10 @@ Use this to skip opening a host firewall port for a service Caddy already
fronts *locally* (it reaches the service over `host.docker.internal`, not fronts *locally* (it reaches the service over `host.docker.internal`, not
the network) — but still open it when `CADDY_SERVICE_MODE` is `"remote"`, the network) — but still open it when `CADDY_SERVICE_MODE` is `"remote"`,
since a remote Caddy machine needs to reach this host over the network since a remote Caddy machine needs to reach this host over the network
instead. See `services/asterisk.sh` and `services/asterisk-digital-ocean.sh` instead. See `services/asterisk.sh` for the reference pattern: it decides
for the reference pattern: call `configure_caddy_for_service` *before* the Caddy question *before* building firewall rules, not after, so the
building firewall rules, not after, so the decision is known in time. answer is known in time (in droplet mode it hand-builds its own site block
and sets the same flag itself, for the reasons noted above).
### UFW enable ### UFW enable
@@ -252,8 +281,7 @@ blocks that too and silently breaks the service (confirmed live: closing
the web admin port outright took Caddy down with it). Call the web admin port outright took Caddy down with it). Call
`ufw_allow_from_caddy_net` right after the `delete` to re-open the port `ufw_allow_from_caddy_net` right after the `delete` to re-open the port
scoped to just `caddy_net`'s subnet — reachable from Caddy, not from the scoped to just `caddy_net`'s subnet — reachable from Caddy, not from the
internet. See `services/asterisk-digital-ocean.sh` and internet. See `services/asterisk.sh` for the pattern.
`services/asterisk.sh` for the pattern.
### README generation ### README generation
@@ -298,7 +326,7 @@ on the same machine anyway. See `add_authelia_domain()` in `services/authelia.sh
**Running a genuinely separate instance (e.g. one per machine).** `services/authelia.sh` **Running a genuinely separate instance (e.g. one per machine).** `services/authelia.sh`
runs standalone on any box (`sudo bash authelia.sh`, same pattern as `crowdsec.sh`) and runs standalone on any box (`sudo bash authelia.sh`, same pattern as `crowdsec.sh`) and
`asterisk-digital-ocean.sh` already auto-detects a local install (`if [ -d `asterisk.sh` already auto-detects a local install (`if [ -d
"$DOCKER_DIR/authelia" ]`), switching from the remote-Authelia `forward_auth` flow to the "$DOCKER_DIR/authelia" ]`), switching from the remote-Authelia `forward_auth` flow to the
local `import authelia` snippet automatically — so a second, fully independent instance on local `import authelia` snippet automatically — so a second, fully independent instance on
another machine (e.g. a droplet, for resilience if the first machine goes down) works with another machine (e.g. a droplet, for resilience if the first machine goes down) works with
@@ -429,9 +457,8 @@ rules, or reverse-proxy/SSO config that's already in place. If the
vendor-copy or `docker-compose.yml`-generation logic is more than a few vendor-copy or `docker-compose.yml`-generation logic is more than a few
lines, factor it into a helper function so the fresh-install path and the lines, factor it into a helper function so the fresh-install path and the
update path share one copy instead of drifting apart — see update path share one copy instead of drifting apart — see
`_asterisk_do_refresh_vendor_files`/`_asterisk_do_write_compose` in `_asterisk_refresh_vendor_files`/`_asterisk_write_compose` in
`services/asterisk-digital-ocean.sh` (and their `_asterisk_*` counterparts in `services/asterisk.sh` for the reference pattern.
`services/asterisk.sh`) for the reference pattern.
`cancel` must leave the install completely untouched — it's the default for `cancel` must leave the install completely untouched — it's the default for
a reason (a stray Enter on a service you're just checking on shouldn't a reason (a stray Enter on a service you're just checking on shouldn't
@@ -443,9 +470,9 @@ would, prompts included.
A service can call another service's `install_<name>()` directly as a A service can call another service's `install_<name>()` directly as a
convenience step at the end of its own flow, instead of making the user convenience step at the end of its own flow, instead of making the user
remember to separately run `sudo ./setup.sh <other-name>` afterward. remember to separately run `sudo ./setup.sh <other-name>` afterward.
`services/asterisk.sh`/`services/asterisk-digital-ocean.sh` do this for `services/asterisk.sh` does this for
`services/security-dashboard.sh` and `services/pstn-trunk.sh` — after `services/security-dashboard.sh` and `services/pstn-trunk.sh` — after
Asterisk itself is installed/updated, each asks once whether to also set up Asterisk itself is installed/updated, it asks once whether to also set up
the dashboard and/or a PSTN trunk (or, if either is already installed, the dashboard and/or a PSTN trunk (or, if either is already installed,
silently re-invokes it so it gets refreshed as part of the same run — its silently re-invokes it so it gets refreshed as part of the same run — its
own `prompt_reinstall_mode` gate decides update vs. skip, so this never own `prompt_reinstall_mode` gate decides update vs. skip, so this never
@@ -501,7 +528,7 @@ it, so as long as your `install_<name>()` calls `require_docker` before
`docker compose up` (it always should), the network is guaranteed to exist `docker compose up` (it always should), the network is guaranteed to exist
regardless of whether Caddy itself has been installed yet. regardless of whether Caddy itself has been installed yet.
**`network_mode: host` services (e.g. `asterisk`/`asterisk-digital-ocean`) don't join **`network_mode: host` services (e.g. `asterisk`) don't join
`caddy_net` at all** — Caddy reaching them (or anything else on the host `caddy_net` at all** — Caddy reaching them (or anything else on the host
network) needs `host.docker.internal:PORT` in the Caddyfile, not network) needs `host.docker.internal:PORT` in the Caddyfile, not
`localhost:PORT` or a container name. Caddy's own compose file `localhost:PORT` or a container name. Caddy's own compose file
+1 -2
View File
@@ -67,7 +67,7 @@ a ready-to-copy Caddy config snippet to `~/docker/caddy-snippets/`.
| Group | Services | | Group | Services |
|-------|---------| |-------|---------|
| `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network | | `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network |
| `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant`, `asterisk`, `asterisk-digital-ocean`, `pstn-trunk`, `security-dashboard`, `sunshine` | | `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant`, `asterisk`, `pstn-trunk`, `security-dashboard`, `sunshine` |
| `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` | | `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` |
| `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` | | `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` |
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
@@ -91,7 +91,6 @@ homelab
authelia authelia
homeassistant homeassistant
asterisk asterisk
asterisk-digital-ocean
pstn-trunk pstn-trunk
security-dashboard security-dashboard
sunshine sunshine
+7 -5
View File
@@ -1,8 +1,8 @@
# Anveo Direct + Easy Asterisk — confirmed working setup guide # Anveo Direct + Easy Asterisk — confirmed working setup guide
This is the exact sequence that got a real Anveo Direct DID working end to This is the exact sequence that got a real Anveo Direct DID working end to
end (both outbound and inbound) with `asterisk-digital-ocean.sh` + end (both outbound and inbound) with `asterisk.sh` + `pstn-trunk.sh`,
`pstn-trunk.sh`, confirmed live on a real droplet. confirmed live on a real droplet.
**Steps 1, 3 and 4 are one-time account setup** — the outbound Service **Steps 1, 3 and 4 are one-time account setup** — the outbound Service
Trunk (step 3) and the inbound SIP Trunk (step 4) each cover every DID on Trunk (step 3) and the inbound SIP Trunk (step 4) each cover every DID on
@@ -13,8 +13,8 @@ in the dashboard, and test.
## 0. Prerequisites ## 0. Prerequisites
- `asterisk-digital-ocean.sh` (or `asterisk.sh` for a LAN box) already - `asterisk.sh` already installed and running (droplet or home/LAN — the
installed and running, with at least one extension configured. installer detects which), with at least one extension configured.
- This box's public IP address (`curl -4 ifconfig.me`). - This box's public IP address (`curl -4 ifconfig.me`).
## 1. Anveo Direct account (one-time) ## 1. Anveo Direct account (one-time)
@@ -194,7 +194,9 @@ In the Security Dashboard's PSTN Trunk tab:
answers or 20 seconds pass. answers or 20 seconds pass.
- Watch the live console while testing either direction: - Watch the live console while testing either direction:
``` ```
docker exec -it easy-asterisk-do asterisk -rvvv docker exec -it easy-asterisk asterisk -rvvv
# on a droplet set up before the two Asterisk services were merged, the
# container is named easy-asterisk-do instead
``` ```
## Bugs hit and fixed along the way (informational — already fixed) ## Bugs hit and fixed along the way (informational — already fixed)
+8 -9
View File
@@ -10,9 +10,8 @@ file** — this doc is the design/decision log; that one is the clean
how-to. how-to.
**Implemented** — see `services/pstn-trunk.sh` (run `sudo ./setup.sh **Implemented** — see `services/pstn-trunk.sh` (run `sudo ./setup.sh
pstn-trunk` after `asterisk-digital-ocean` **or** `asterisk` (home/LAN) is pstn-trunk` after `asterisk` is installed — droplet or home/LAN, both are
installed — both are supported, see the file for the static-IP caveat on the supported; see the file for the static-IP caveat on the LAN variant). Generic SIP trunk add-on that defaults to VoIP.ms but isn't
LAN variant). Generic SIP trunk add-on that defaults to VoIP.ms but isn't
hardcoded to it — any provider supporting IP authentication works. Covers: hardcoded to it — any provider supporting IP authentication works. Covers:
- IP-authenticated trunk, US/NANP-only outbound dialplan, no catch-all. - IP-authenticated trunk, US/NANP-only outbound dialplan, no catch-all.
@@ -65,7 +64,7 @@ file.
- **Provider: VoIP.ms.** Chosen for its prepaid-balance model: turn off - **Provider: VoIP.ms.** Chosen for its prepaid-balance model: turn off
auto-recharge in the account's Finances settings and outbound calls simply auto-recharge in the account's Finances settings and outbound calls simply
fail once the balance hits $0 — that's the toll-fraud backstop if the fail once the balance hits $0 — that's the toll-fraud backstop if the
droplet's Asterisk (`asterisk-digital-ocean`) is ever compromised. droplet's Asterisk is ever compromised.
**Update — read VoIP.ms's actual ToS (not just the wiki) on this.** The **Update — read VoIP.ms's actual ToS (not just the wiki) on this.** The
wiki says plainly "only accounts with a balance over $0 are able to send wiki says plainly "only accounts with a balance over $0 are able to send
@@ -173,7 +172,7 @@ estimated spend crosses a threshold, and every hour that call volume in the
last hour looks like a burst. Denied/rejected calls alert immediately, last hour looks like a burst. Denied/rejected calls alert immediately,
separately from that hourly check. separately from that hourly check.
## What it takes technically (asterisk-digital-ocean) ## What it takes technically (asterisk, droplet mode)
- A PJSIP trunk: `endpoint` / `aor` / `identify` sections in the pjsip - A PJSIP trunk: `endpoint` / `aor` / `identify` sections in the pjsip
config. **Implemented with IP authentication** (no `auth` section, no SIP config. **Implemented with IP authentication** (no `auth` section, no SIP
password stored anywhere) — see `services/pstn-trunk.sh`. Provider name, password stored anywhere) — see `services/pstn-trunk.sh`. Provider name,
@@ -357,9 +356,9 @@ generator output. Fixed by quoting every value in that heredoc.
model (internal/restricted/full) managed live via model (internal/restricted/full) managed live via
`pstn-permissions.conf` + the Security Dashboard web UI, no reinstall `pstn-permissions.conf` + the Security Dashboard web UI, no reinstall
needed to change. ~~Generic Asterisk target~~ Done — needed to change. ~~Generic Asterisk target~~ Done —
`services/pstn-trunk.sh` now supports either `asterisk-digital-ocean` or `services/pstn-trunk.sh` supports the `asterisk` install in either mode,
the home/LAN `asterisk` install (the latter with a static-IP caveat for droplet or home/LAN (the latter with a static-IP caveat for the
the provider's IP authentication). Still unresolved: pick pay-per-minute provider's IP authentication). Still unresolved: pick pay-per-minute
vs. unlimited DID plan on VoIP.ms's side based on real expected volume, vs. unlimited DID plan on VoIP.ms's side based on real expected volume,
and decide on E911 (see cost estimate). and decide on E911 (see cost estimate).
5. ~~Concurrent-call cap~~ Done — both directions now (inbound was a real 5. ~~Concurrent-call cap~~ Done — both directions now (inbound was a real
@@ -444,7 +443,7 @@ generator output. Fixed by quoting every value in that heredoc.
`exten => <ext>,1,...` per device, freshly regenerated by Easy `exten => <ext>,1,...` per device, freshly regenerated by Easy
Asterisk's own `rebuild_dialplan()` on every dialplan rebuild — exactly Asterisk's own `rebuild_dialplan()` on every dialplan rebuild — exactly
the collision this doc worried about. Solved by NOT sharing the collision this doc worried about. Solved by NOT sharing
`[intercom]`: `services/asterisk-digital-ocean.sh` now explicitly sets `[intercom]`: `services/asterisk.sh` now explicitly sets
`message_context=sip-messaging` on every endpoint (patched into both of `message_context=sip-messaging` on every endpoint (patched into both of
Easy Asterisk's device-creation code paths — the CLI menu's bash Easy Asterisk's device-creation code paths — the CLI menu's bash
heredoc and the web admin's Python `add_device()` — so new devices pick heredoc and the web admin's Python `add_device()` — so new devices pick
File diff suppressed because it is too large Load Diff
+849 -202
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -435,7 +435,7 @@ auth.${AUTHELIA_DOMAIN} {
# own incoming request (always auth.${AUTHELIA_DOMAIN} itself) and # own incoming request (always auth.${AUTHELIA_DOMAIN} itself) and
# overwrites the value a forward_auth caller (e.g. a remote site's # overwrites the value a forward_auth caller (e.g. a remote site's
# "forward_auth https://auth.${AUTHELIA_DOMAIN}" block, see # "forward_auth https://auth.${AUTHELIA_DOMAIN}" block, see
# services/asterisk-digital-ocean.sh) set for its own domain. Confirmed # services/asterisk.sh's droplet-mode Caddy block) set for its own domain. Confirmed
# live: every forward-auth check evaluated as if it were for # live: every forward-auth check evaluated as if it were for
# auth.${AUTHELIA_DOMAIN} itself (which has policy: bypass in # auth.${AUTHELIA_DOMAIN} itself (which has policy: bypass in
# access_control.rules so its own login portal isn't gated behind # access_control.rules so its own login portal isn't gated behind
+1 -1
View File
@@ -268,7 +268,7 @@ services:
labels: labels:
- "io.podman.annotations.label/crowdsec.enable=true" - "io.podman.annotations.label/crowdsec.enable=true"
# Lets Caddyfile blocks reach services that use network_mode: host # Lets Caddyfile blocks reach services that use network_mode: host
# (e.g. asterisk/asterisk-digital-ocean) via "host.docker.internal:PORT" — Caddy # (e.g. asterisk) via "host.docker.internal:PORT" — Caddy
# itself is on the caddy_net bridge network below, so plain "localhost" # itself is on the caddy_net bridge network below, so plain "localhost"
# in a site block resolves to Caddy's own container, not the host. # in a site block resolves to Caddy's own container, not the host.
extra_hosts: extra_hosts:
+26 -14
View File
@@ -102,7 +102,7 @@ install_crowdsec() {
echo "[DRY-RUN] Would ensure /var/log/caddy exists for log acquisition" echo "[DRY-RUN] Would ensure /var/log/caddy exists for log acquisition"
echo "[DRY-RUN] Would install collections: sshd, linux, caddy, base-http-scenarios" echo "[DRY-RUN] Would install collections: sshd, linux, caddy, base-http-scenarios"
echo "[DRY-RUN] Would write Caddy acquisition /etc/crowdsec/acquis.d/caddy.yaml" echo "[DRY-RUN] Would write Caddy acquisition /etc/crowdsec/acquis.d/caddy.yaml"
echo "[DRY-RUN] Would install crowdsecurity/asterisk + write an acquisition if asterisk-digital-ocean is installed" echo "[DRY-RUN] Would install crowdsecurity/asterisk + write an acquisition if asterisk is installed"
echo "[DRY-RUN] Would optionally wire ntfy ban alerts into the default profile" echo "[DRY-RUN] Would optionally wire ntfy ban alerts into the default profile"
echo "[DRY-RUN] Would optionally register with a remote/central LAPI and disable the local one" echo "[DRY-RUN] Would optionally register with a remote/central LAPI and disable the local one"
echo "[DRY-RUN] Would enable + restart crowdsec and crowdsec-firewall-bouncer" echo "[DRY-RUN] Would enable + restart crowdsec and crowdsec-firewall-bouncer"
@@ -194,17 +194,26 @@ labels:
echo " ✓ Caddy acquisition already exists" echo " ✓ Caddy acquisition already exists"
fi fi
# ── 5b. SIP brute-force/enumeration protection, if asterisk-digital-ocean # ── 5b. SIP brute-force/enumeration protection, if Asterisk is installed.
# is installed (services/asterisk-digital-ocean.sh patches Asterisk to log # services/asterisk.sh patches Asterisk to log security events — auth
# security events — auth failures, registration scanning — to # failures, registration scanning — to $EA_DIR/logs/full, which is what
# $EA_DIR/logs/full. The plain LAN asterisk.sh doesn't emit that file yet, # the acquisition below tails. Both directories are probed: a box set up
# so it's intentionally not detected here.) # before the droplet edition was merged back into `asterisk` still runs
local ASTERISK_LOG_DIR="$DOCKER_DIR/asterisk-digital-ocean/logs" # out of ~/docker/asterisk-digital-ocean. The logging patch used to be
if [ -d "$ASTERISK_LOG_DIR" ]; then # droplet-only; it now applies to every install, so a home/LAN box gets
echo " Detected asterisk-digital-ocean — installing SIP brute-force/enumeration protection..." # SIP protection here too.
local ASTERISK_LOG_DIR=""
local _ea_candidate
for _ea_candidate in "$DOCKER_DIR/asterisk-digital-ocean" "$DOCKER_DIR/asterisk"; do
[ -d "$_ea_candidate/logs" ] && { ASTERISK_LOG_DIR="$_ea_candidate/logs"; break; }
done
if [ -n "$ASTERISK_LOG_DIR" ]; then
echo " Detected Asterisk at ${ASTERISK_LOG_DIR%/logs} — installing SIP brute-force/enumeration protection..."
sudo cscli collections install crowdsecurity/asterisk 2>/dev/null || \ sudo cscli collections install crowdsecurity/asterisk 2>/dev/null || \
echo " ⚠ crowdsecurity/asterisk collection may already be installed" echo " ⚠ crowdsecurity/asterisk collection may already be installed"
# Filename kept as-is so a droplet that already has this acquisition
# isn't given a second one pointing at the same log.
local ASTERISK_ACQUIS="/etc/crowdsec/acquis.d/asterisk-digital-ocean.yaml" local ASTERISK_ACQUIS="/etc/crowdsec/acquis.d/asterisk-digital-ocean.yaml"
if [ ! -f "$ASTERISK_ACQUIS" ]; then if [ ! -f "$ASTERISK_ACQUIS" ]; then
local ASTERISK_ACQUIS_CONTENT="filenames: local ASTERISK_ACQUIS_CONTENT="filenames:
@@ -543,7 +552,7 @@ install. The real configuration lives under `/etc/crowdsec`.
## What it does ## What it does
- Detects malicious behaviour (SSH brute force, web scans, SIP brute - Detects malicious behaviour (SSH brute force, web scans, SIP brute
force/enumeration if `asterisk-digital-ocean` is installed) by parsing logs. force/enumeration if `asterisk` is installed) by parsing logs.
- Bans offending IPs via the **firewall bouncer** (iptables/nftables). - Bans offending IPs via the **firewall bouncer** (iptables/nftables).
- Pulls **community IP reputation** blocklists so known-bad IPs are blocked - Pulls **community IP reputation** blocklists so known-bad IPs are blocked
before they ever touch your services. before they ever touch your services.
@@ -573,9 +582,12 @@ sudo cscli collections list # installed detection collections
- Log acquisition (what to watch): `/etc/crowdsec/acquis.d/` - Log acquisition (what to watch): `/etc/crowdsec/acquis.d/`
- Caddy access logs: `/etc/crowdsec/acquis.d/caddy.yaml` - Caddy access logs: `/etc/crowdsec/acquis.d/caddy.yaml`
(`/var/log/caddy/*.log` — Caddy writes JSON access logs there) (`/var/log/caddy/*.log` — Caddy writes JSON access logs there)
- Asterisk SIP auth events (if `asterisk-digital-ocean` is installed): - Asterisk SIP auth events (if `asterisk` is installed):
`/etc/crowdsec/acquis.d/asterisk-digital-ocean.yaml` `/etc/crowdsec/acquis.d/asterisk-digital-ocean.yaml` (filename kept from
(`~/docker/asterisk-digital-ocean/logs/full` — auth failures, registration scans) when the droplet edition was its own service, so existing droplets aren't
given a duplicate acquisition)
(`~/docker/asterisk/logs/full`, or `~/docker/asterisk-digital-ocean/logs/full`
on a pre-merge droplet — auth failures, registration scans)
- Notifications: `/etc/crowdsec/notifications/` - Notifications: `/etc/crowdsec/notifications/`
- ntfy ban alerts (if enabled): `/etc/crowdsec/notifications/ntfy.yaml`, - ntfy ban alerts (if enabled): `/etc/crowdsec/notifications/ntfy.yaml`,
wired into `/etc/crowdsec/profiles.yaml` wired into `/etc/crowdsec/profiles.yaml`
@@ -609,7 +621,7 @@ sudo cscli collections list # installed detection collections
list directly in that file, then `sudo systemctl restart crowdsec`. This list directly in that file, then `sudo systemctl restart crowdsec`. This
can block Let's Encrypt's out-of-region ACME validation checks; if a cert can block Let's Encrypt's out-of-region ACME validation checks; if a cert
renewal fails mysteriously, check here first. renewal fails mysteriously, check here first.
- ASN-exempt Asterisk brute-force scenarios (if enabled, asterisk-digital-ocean - ASN-exempt Asterisk brute-force scenarios (if enabled, Asterisk installs
only): `/etc/crowdsec/scenarios/local-asterisk_bf.yaml` and only): `/etc/crowdsec/scenarios/local-asterisk_bf.yaml` and
`local-asterisk_user_enum.yaml` — local forks of the stock hub scenarios with `local-asterisk_user_enum.yaml` — local forks of the stock hub scenarios with
specific carrier ASNs excluded from their filter (the hub originals get specific carrier ASNs excluded from their filter (the hub originals get
+24 -23
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# services/pstn-trunk.sh — SIP PSTN trunk add-on for asterisk-digital-ocean # services/pstn-trunk.sh — SIP PSTN trunk add-on for services/asterisk.sh:
# (or the home/LAN asterisk install): US-only outbound (NANP dialplan # US-only outbound (NANP dialplan
# restriction), independent outbound/inbound concurrent-call caps, a 3-tier # restriction), independent outbound/inbound concurrent-call caps, a 3-tier
# permission model per extension (internal-only / restricted to pre-approved # permission model per extension (internal-only / restricted to pre-approved
# numbers / full US calling), a configurable inbound ring-group, # numbers / full US calling), a configurable inbound ring-group,
@@ -15,19 +15,19 @@
# works the same way. VoIP.ms and Anveo Direct are both confirmed working; # works the same way. VoIP.ms and Anveo Direct are both confirmed working;
# see docs/pstn-calling-voipms-plan.md for the design/cost background. # see docs/pstn-calling-voipms-plan.md for the design/cost background.
# #
# Requires an existing services/asterisk-digital-ocean.sh OR services/asterisk.sh # Requires an existing services/asterisk.sh install (either directory layout —
# install — this adds a PSTN trunk on top of one of them and does not stand # ~/docker/asterisk, or ~/docker/asterisk-digital-ocean on a box set up before
# alone. Permission tiers AND concurrency caps are managed live (no restart # the droplet edition was merged back in) — this adds a PSTN trunk on top and
# does not stand alone. Permission tiers AND concurrency caps are managed live (no restart
# needed) via pstn-permissions.conf / pstn-limits.conf — editable by hand, or # needed) via pstn-permissions.conf / pstn-limits.conf — editable by hand, or
# from services/security-dashboard.sh's "PSTN Trunk" tab if that's installed. # from services/security-dashboard.sh's Extensions tab if that's installed.
# #
# Part of the modular post-install system (sourced by setup.sh). # Part of the modular post-install system (sourced by setup.sh).
register_service pstn-trunk homelab "SIP PSTN trunk for asterisk-digital-ocean/asterisk — US-only, per-extension permission tiers, spend/volume alerts (any IP-authenticated provider — VoIP.ms and Anveo Direct both confirmed)" register_service pstn-trunk homelab "SIP PSTN trunk for asterisk — US-only, per-extension permission tiers, spend/volume alerts (any IP-authenticated provider — VoIP.ms and Anveo Direct both confirmed)"
# ── Surviving Easy Asterisk's regeneration ────────────────────────────────── # ── Surviving Easy Asterisk's regeneration ──────────────────────────────────
# Easy Asterisk (the vendor project asterisk-digital-ocean.sh/asterisk.sh # Easy Asterisk (the vendor project services/asterisk.sh builds on) fully OVERWRITES both pjsip.conf and extensions.conf from its own
# build on) fully OVERWRITES both pjsip.conf and extensions.conf from its own
# internal state: # internal state:
# - extensions.conf: rebuilt by rebuild_dialplan() on every container start, # - extensions.conf: rebuilt by rebuild_dialplan() on every container start,
# and whenever a device/room is added or removed via the web admin. # and whenever a device/room is added or removed via the web admin.
@@ -41,9 +41,9 @@ register_service pstn-trunk homelab "SIP PSTN trunk for asterisk-digital-ocean/a
# the #include itself survive regeneration too, _pstn_patch_vendor_files # the #include itself survive regeneration too, _pstn_patch_vendor_files
# (below) patches it into the vendor's *generator functions* — the same # (below) patches it into the vendor's *generator functions* — the same
# technique this repo already uses for the logger.conf security-logging fix # technique this repo already uses for the logger.conf security-logging fix
# in _asterisk_do_refresh_vendor_files (see services/asterisk-digital-ocean.sh). # in _asterisk_refresh_vendor_files (see services/asterisk.sh).
# #
# Caveat: if the base asterisk-digital-ocean/asterisk install is later # Caveat: if the base asterisk install is later
# refreshed ("update in place", which re-copies fresh vendor files) # refreshed ("update in place", which re-copies fresh vendor files)
# independently of this service, the patch is wiped along with it and needs # independently of this service, the patch is wiped along with it and needs
# reapplying — run this service again (fresh or update mode both reapply it) # reapplying — run this service again (fresh or update mode both reapply it)
@@ -459,7 +459,7 @@ EOF
# zero of the outbound NANP patterns either, and `dialplan show # zero of the outbound NANP patterns either, and `dialplan show
# from-pstn-trunk` reported the context didn't exist at all, with no # from-pstn-trunk` reported the context didn't exist at all, with no
# warning or error anywhere (config log, full log, or the reload command's # warning or error anywhere (config log, full log, or the reload command's
# own output) pointing at why. Meanwhile services/asterisk-digital-ocean.sh's # own output) pointing at why. Meanwhile services/asterisk.sh's
# messaging-dialplan.conf — #include'd via the exact same mechanism, right # messaging-dialplan.conf — #include'd via the exact same mechanism, right
# after [intercom] in the same extensions.conf — loaded fine every time. # after [intercom] in the same extensions.conf — loaded fine every time.
# The one structural difference: messaging-dialplan.conf's first real line # The one structural difference: messaging-dialplan.conf's first real line
@@ -1608,7 +1608,7 @@ install_pstn-trunk() {
[[ "$ASTERISK_KIND" == "asterisk-digital-ocean" ]] && CONTAINER_NAME="easy-asterisk-do" [[ "$ASTERISK_KIND" == "asterisk-digital-ocean" ]] && CONTAINER_NAME="easy-asterisk-do"
if [ "$DRY_RUN" = true ]; then if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would require an existing asterisk-digital-ocean OR asterisk (LAN) install" echo "[DRY-RUN] Would require an existing asterisk install (droplet or home/LAN)"
echo "[DRY-RUN] Would prompt for: known-provider quick-pick (Anveo Direct runs a full 5-step" echo "[DRY-RUN] Would prompt for: known-provider quick-pick (Anveo Direct runs a full 5-step"
echo "[DRY-RUN] interactive portal walkthrough — account/funding, DID ordering, both trunk" echo "[DRY-RUN] interactive portal walkthrough — account/funding, DID ordering, both trunk"
echo "[DRY-RUN] objects, confirmed rate — pausing for Enter between each; VoIP.ms pre-fills known" echo "[DRY-RUN] objects, confirmed rate — pausing for Enter between each; VoIP.ms pre-fills known"
@@ -1640,10 +1640,12 @@ install_pstn-trunk() {
fi fi
if [[ -z "$EA_DIR" ]]; then if [[ -z "$EA_DIR" ]]; then
log_error "Neither asterisk-digital-ocean nor asterisk (LAN) is installed — install one first:" log_error "Asterisk is not installed — install it first:"
log_error " sudo ./setup.sh asterisk-digital-ocean (recommended — public droplet, static IP)" log_error " sudo ./setup.sh asterisk"
log_error " sudo ./setup.sh asterisk (home/LAN — see the static-IP caveat below)" log_error "A public droplet (which that installer detects and tunes for) is the"
log_error "This service adds a PSTN trunk on top of one of them; it doesn't stand alone." log_error "recommended host, since IP authentication wants a static IP — see the"
log_error "caveat below for what that means on a home/LAN box."
log_error "This service adds a PSTN trunk on top of it; it doesn't stand alone."
return 1 return 1
fi fi
@@ -1652,7 +1654,7 @@ install_pstn-trunk() {
log_warning "Using the home/LAN asterisk install. IP authentication needs a STABLE public IP —" log_warning "Using the home/LAN asterisk install. IP authentication needs a STABLE public IP —"
log_warning "if this box is behind a dynamic home IP, your provider's IP allow-list goes stale" log_warning "if this box is behind a dynamic home IP, your provider's IP allow-list goes stale"
log_warning "whenever your ISP rotates it, breaking calls until you update it there yourself." log_warning "whenever your ISP rotates it, breaking calls until you update it there yourself."
log_warning "A static IP from your ISP avoids that; asterisk-digital-ocean sidesteps it entirely." log_warning "A static IP from your ISP avoids that; a cloud droplet sidesteps it entirely."
fi fi
log_info "Configuring a SIP PSTN trunk for $ASTERISK_KIND (any IP-authenticated provider —" log_info "Configuring a SIP PSTN trunk for $ASTERISK_KIND (any IP-authenticated provider —"
@@ -2213,12 +2215,11 @@ Asterisk's native SIP \`MESSAGE\` support (extension-to-extension texting —
no carrier SMS, no PSTN, no cost) is gated by a \`messaging=yes\` flag per no carrier SMS, no PSTN, no cost) is gated by a \`messaging=yes\` flag per
extension in \`pstn-permissions.conf\`, independent of the PSTN calling extension in \`pstn-permissions.conf\`, independent of the PSTN calling
tiers above — off by default, same "opt in" posture. Live-editable any tiers above — off by default, same "opt in" posture. Live-editable any
time via the Security Dashboard's "PSTN Trunk" tab, in its own time via the Security Dashboard's Extensions tab, in the Messaging column of
always-available "Internal SIP messaging" card — no dependency on this its always-available extensions table — no dependency on this trunk (or any
trunk (or any PSTN trunk at all) being installed. PSTN trunk at all) being installed.
Actually enforced, not just a flag — \`services/asterisk-digital-ocean.sh\` Actually enforced, not just a flag — \`services/asterisk.sh\` routes messages through a
(and \`services/asterisk.sh\` for the LAN edition) routes messages through a
dedicated \`[sip-messaging]\` dialplan context (separate from \`[intercom]\`'s dedicated \`[sip-messaging]\` dialplan context (separate from \`[intercom]\`'s
own per-device call routing, so there's no collision risk) and checks this own per-device call routing, so there's no collision risk) and checks this
same flag via \`AST_CONFIG()\` before delivering. One caveat still flagged same flag via \`AST_CONFIG()\` before delivering. One caveat still flagged
+11
View File
@@ -27,6 +27,10 @@ export TERM="${TERM:-xterm-256color}"
CATEGORY_ORDER=(base homelab utilities media cameras gaming extras backup) CATEGORY_ORDER=(base homelab utilities media cameras gaming extras backup)
# Service ordering hint within a category (lower = earlier). Default 50. # Service ordering hint within a category (lower = earlier). Default 50.
declare -A SERVICE_PRIORITY=( [caddy]=1 [crowdsec]=2 [authelia]=3 ) declare -A SERVICE_PRIORITY=( [caddy]=1 [crowdsec]=2 [authelia]=3 )
# Retired service names that now resolve to another service. Keeps a name
# that used to work on the command line (and in docs/muscle memory) working
# after a merge, without giving it a second menu entry of its own.
declare -A SERVICE_ALIAS=( [asterisk-digital-ocean]=asterisk )
# ── Parse flags / collect service names ────────────────────────────────────── # ── Parse flags / collect service names ──────────────────────────────────────
DRY_RUN=false; UNATTENDED=false; DO_LIST=false DRY_RUN=false; UNATTENDED=false; DO_LIST=false
@@ -89,6 +93,9 @@ is_installed() {
sync-cc) [ -f "$ACTUAL_HOME/sync-cc/sync_cc.py" ] ;; sync-cc) [ -f "$ACTUAL_HOME/sync-cc/sync_cc.py" ] ;;
sky-cam) [ -d "$ACTUAL_HOME/sky-cam/.git" ] ;; sky-cam) [ -d "$ACTUAL_HOME/sky-cam/.git" ] ;;
sky-cam-frigate) [ -d "$ACTUAL_HOME/sky-cam/.git" ] && [ -f "$ACTUAL_HOME/sky-cam/frigate-retime.sh" ] ;; sky-cam-frigate) [ -d "$ACTUAL_HOME/sky-cam/.git" ] && [ -f "$ACTUAL_HOME/sky-cam/frigate-retime.sh" ] ;;
# Either directory counts: boxes set up before the droplet edition was
# merged back into `asterisk` still run out of ~/docker/asterisk-digital-ocean.
asterisk) [ -e "$DOCKER_DIR/asterisk" ] || [ -e "$DOCKER_DIR/asterisk-digital-ocean" ] ;;
pstn-trunk) [ -f "$DOCKER_DIR/asterisk-digital-ocean/config/asterisk/pstn-trunk-pjsip.conf" ] || [ -f "$DOCKER_DIR/asterisk/config/asterisk/pstn-trunk-pjsip.conf" ] ;; pstn-trunk) [ -f "$DOCKER_DIR/asterisk-digital-ocean/config/asterisk/pstn-trunk-pjsip.conf" ] || [ -f "$DOCKER_DIR/asterisk/config/asterisk/pstn-trunk-pjsip.conf" ] ;;
ssh-config) false ;; # repeatable management tool, never shows [installed] ssh-config) false ;; # repeatable management tool, never shows [installed]
*) [ -e "$DOCKER_DIR/$1" ] ;; *) [ -e "$DOCKER_DIR/$1" ] ;;
@@ -97,6 +104,10 @@ is_installed() {
run_service() { run_service() {
local name="$1" local name="$1"
if [ -n "${SERVICE_ALIAS[$name]:-}" ]; then
log_info "'$name' is now part of '${SERVICE_ALIAS[$name]}' — running that instead."
name="${SERVICE_ALIAS[$name]}"
fi
if [ -z "${SERVICE_GROUP[$name]:-}" ]; then log_error "Unknown service: $name (try --list)"; return 1; fi if [ -z "${SERVICE_GROUP[$name]:-}" ]; then log_error "Unknown service: $name (try --list)"; return 1; fi
declare -F "install_${name}" >/dev/null || { log_error "Service '$name' has no install_${name}"; return 1; } declare -F "install_${name}" >/dev/null || { log_error "Service '$name' has no install_${name}"; return 1; }
log_info "=== ${name} (${SERVICE_DESC[$name]}) ===" log_info "=== ${name} (${SERVICE_DESC[$name]}) ==="