From 79b27a1594cc1c7d7902435526da2efdb670d80c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 15:19:19 +0000 Subject: [PATCH 1/3] crowdsec: default ntfy ban-alert URL to a real hosted instance Step 7 (ntfy ban alerts) always defaulted straight to the public ntfy.sh, regardless of whether the box (or a homelab) already had a real ntfy instance. Confusing in practice: this step runs before asterisk-do's own ntfy extra is dispatched, so even selecting it wouldn't have helped at prompt time. Now checks the local ntfy install's own config/server.yml for a configured base-url (skipping it if it's still the ntfy.sh-written placeholder) and uses /crowdsec-alerts as the default. If there's no configured local instance, it says so explicitly and prompts toward a hosted instance elsewhere (e.g. a homelab) instead of silently assuming the public service. Verified all three cases (configured local, unconfigured placeholder, none) in isolation, plus a full regression run. --- services/crowdsec.sh | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/services/crowdsec.sh b/services/crowdsec.sh index ac99283..123d0dd 100644 --- a/services/crowdsec.sh +++ b/services/crowdsec.sh @@ -196,11 +196,31 @@ labels: echo " https://app.crowdsec.net/" # ── 7. Optional: push ban alerts to ntfy ───────────────────────────────── + echo "" local CS_NTFY="" prompt_yn "Send CrowdSec ban alerts to an ntfy topic? (y/n):" "n" CS_NTFY if [ "$CS_NTFY" = "y" ] || [ "$CS_NTFY" = "Y" ]; then + # Prefer a locally-installed ntfy's own base-url as the default, if one + # exists and actually looks configured (not still the placeholder + # domain ntfy.sh writes when no SITE_DOMAIN was set at its own install + # time). Otherwise, nudge toward a hosted instance elsewhere (e.g. a + # homelab) instead of silently defaulting to the public ntfy.sh. + local _ntfy_default="https://ntfy.sh/crowdsec-alerts" + if [ -f "$DOCKER_DIR/ntfy/config/server.yml" ]; then + local _local_base_url + _local_base_url="$(grep -oP '(?<=base-url: ")[^"]+' "$DOCKER_DIR/ntfy/config/server.yml" 2>/dev/null || true)" + if [ -n "$_local_base_url" ] && [ "$_local_base_url" != "https://ntfy.example.com" ]; then + _ntfy_default="${_local_base_url}/crowdsec-alerts" + echo " Detected a configured local ntfy instance at $_local_base_url — using it as the default." + fi + fi + if [ "$_ntfy_default" = "https://ntfy.sh/crowdsec-alerts" ]; then + echo " No configured ntfy instance detected on this box. If you have one hosted" + echo " elsewhere (e.g. a homelab), enter its topic URL below instead of the public" + echo " ntfy.sh default — e.g. https://ntfy.your-homelab.com/crowdsec-alerts" + fi local CS_NTFY_URL="" - prompt_text " ntfy topic URL (e.g. https://ntfy.sh/my-crowdsec):" "https://ntfy.sh/crowdsec-alerts" CS_NTFY_URL + prompt_text " ntfy topic URL:" "$_ntfy_default" CS_NTFY_URL sudo mkdir -p /etc/crowdsec/notifications local NTFY_FILE="/etc/crowdsec/notifications/ntfy.yaml" local NTFY_CONTENT="type: http From f00e19099eda64d054ffe022c6713542fcb4ba0e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 15:25:14 +0000 Subject: [PATCH 2/3] asterisk-do: switch extras prompt to a numbered, comma-separated menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing out keyword names (e.g. "netbird backup") was more friction than necessary. Now a numbered list (1-6), answered as comma-separated digits with an example shown ("Example: 5,6"), translated internally back to the same space-separated keyword string every existing dispatch check (authelia/ntfy/watchtower/wg-easy/netbird/backup) was already matching against — so none of those call sites needed to change. Handles spaces after commas and silently ignores invalid entries rather than erroring. Verified the number-to-keyword mapping in isolation across normal input, spacing variants, invalid digits, and blank, plus a full end-to-end regression run. --- services/asterisk-do.sh | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index 7675dca..8916193 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -206,7 +206,7 @@ install_asterisk-do() { if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would add a swapfile if RAM <= 2048MB and none exists" echo "[DRY-RUN] Would offer to install Caddy if not already present (full repo only)" - echo "[DRY-RUN] Would offer optional extras: authelia, ntfy, watchtower, wg-easy, netbird, backup" + echo "[DRY-RUN] Would offer optional extras as a numbered, comma-separated menu (1-6)" echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env" echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk" echo "[DRY-RUN] Would detect droplet public IP via DO metadata service" @@ -292,22 +292,32 @@ install_asterisk-do() { # wg-easy needs its own firewall rules alongside the others; backup makes # most sense last). Only offered in full-repo mode, same reasoning as Caddy. local EXTRAS="" + local _EXTRA_NAMES=(authelia ntfy watchtower wg-easy netbird backup) if declare -F install_authelia &>/dev/null; then echo "" - echo " Optional extras (space-separated, blank = skip all):" - echo " authelia SSO/2FA in front of the web admin (needs Caddy, above)" - echo " ntfy self-hosted push notifications (e.g. CrowdSec ban alerts)" - echo " watchtower auto-updates pulled images — only helps coturn here;" - echo " Asterisk itself is built locally, so OS security patches" - echo " still need a manual 'docker compose up -d --build'" - echo " wg-easy WireGuard VPN — lets you lock the web admin to VPN-only later" - echo " netbird Mesh VPN + optional built-in SSH server, so this droplet and a" - echo " home machine can reach each other without port-forwarding —" - echo " pairs with 'backup' below" - echo " backup Borg backup of ~/docker/* to a local machine or remote host —" - echo " config/data backup, NOT a full droplet image, instead of" - echo " DigitalOcean's paid Droplet Backups" - prompt_text "Install:" "" EXTRAS + echo " Optional extras — enter numbers, comma-separated (blank = skip all):" + echo " 1) authelia SSO/2FA in front of the web admin (needs Caddy, above)" + echo " 2) ntfy self-hosted push notifications (e.g. CrowdSec ban alerts)" + echo " 3) watchtower auto-updates pulled images — only helps coturn here;" + echo " Asterisk itself is built locally, so OS security patches" + echo " still need a manual 'docker compose up -d --build'" + echo " 4) wg-easy WireGuard VPN — lets you lock the web admin to VPN-only later" + echo " 5) netbird Mesh VPN + optional built-in SSH server, so this droplet and a" + echo " home machine can reach each other without port-forwarding —" + echo " pairs with 'backup' below (skipped automatically if already installed)" + echo " 6) backup Borg backup of ~/docker/* to a local machine or remote host —" + echo " config/data backup, NOT a full droplet image, instead of" + echo " DigitalOcean's paid Droplet Backups" + echo " Example: 5,6" + local EXTRAS_NUM="" + prompt_text "Install:" "" EXTRAS_NUM + local _IFS_OLD="$IFS" _n + IFS=',' + for _n in $EXTRAS_NUM; do + _n="${_n//[[:space:]]/}" + [[ "$_n" =~ ^[1-6]$ ]] && EXTRAS+=" ${_EXTRA_NAMES[$((_n-1))]}" + done + IFS="$_IFS_OLD" fi if [[ "$EXTRAS" == *authelia* ]] && declare -F install_authelia &>/dev/null; then From 57797c4ad74e5f3d39b0cfc36b7a0dbc0e915a55 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 15:30:37 +0000 Subject: [PATCH 3/3] asterisk-do: stop guessing the web-admin Caddy domain, hardcode it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-world failure: configure_caddy_for_service's own domain prompt defaults to ".${SITE_DOMAIN}", which only equals $DOMAIN_NAME if SITE_DOMAIN happens to be set to match. In practice SITE_DOMAIN is never set when this service is run by name (e.g. `sudo ./setup.sh asterisk-do`), since that path skips setup.sh's own site-defaults wizard — so the reconstructed default silently came out wrong/blank, and a user had to guess whether to type the SIP domain or something else at a bare "Domain [ ]:" prompt. There's exactly one correct domain for this site block — $DOMAIN_NAME, the same one already used for SIP — so it's no longer asked for at all. This inlines the same Caddyfile-writing logic configure_caddy_for_service uses (backup, dedup check, reload; local and remote-Caddy modes both preserved) but targets $DOMAIN_NAME directly. The only remaining question is a plain yes/no to proxy it. Verified the block-generation logic directly against a real Caddy directory + domain (produces the exact expected Caddyfile entry), plus a full end-to-end regression run. --- services/asterisk-do.sh | 79 +++++++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index 8916193..6c32088 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -637,18 +637,75 @@ ENV fi fi - # Reconstruct the subdomain-only fragment so configure_caddy_for_service's - # own ".${SITE_DOMAIN}" default lands exactly on $DOMAIN_NAME — - # pressing Enter at its domain prompt then just works. - local _CADDY_SUBDOMAIN="asterisk" - if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$DOMAIN_NAME" == *".${SITE_DOMAIN}" ]]; then - _CADDY_SUBDOMAIN="${DOMAIN_NAME%.${SITE_DOMAIN}}" - fi + # Deliberately NOT using configure_caddy_for_service here. That helper + # asks for its own domain, defaulting to ".${SITE_DOMAIN}" — + # which only lands on $DOMAIN_NAME if SITE_DOMAIN happens to be set to + # match, and silently shows a useless blank/wrong default otherwise + # (real-world confirmed: SITE_DOMAIN is never set when this service is + # run by name, e.g. `sudo ./setup.sh asterisk-do`, since that skips + # setup.sh's own site-defaults wizard entirely). There is exactly one + # correct domain for this site block — $DOMAIN_NAME — so it's written + # directly, with no domain prompt to get wrong. + echo "" + local WANT_CADDY_PROXY="" + prompt_yn "Reverse-proxy the web admin at https://${DOMAIN_NAME}/ via Caddy? (also gets Asterisk a trusted TLS cert for SIP instead of self-signed) (y/n):" "y" WANT_CADDY_PROXY + if [[ "$WANT_CADDY_PROXY" =~ ^[Yy]$ ]]; then + local _CADDY_MODE="local" + [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -n "${CADDY_REMOTE_HOST:-}" ]] && _CADDY_MODE="remote" - log_info "Reverse-proxying the web admin at https://${DOMAIN_NAME}/ — this is also what gets" - log_info "Asterisk a trusted TLS cert for SIP instead of a self-signed one." - log_info "When prompted for a domain next, use exactly: ${DOMAIN_NAME}" - configure_caddy_for_service "Asterisk Web Admin" "8080" "$_CADDY_SUBDOMAIN" "$EXTRA_BLOCK" + local _SITE_BLOCK + _SITE_BLOCK="$(cat << CADDY_BLOCK + +# Asterisk Web Admin +${DOMAIN_NAME} { + reverse_proxy localhost:8080 + + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + X-Frame-Options "SAMEORIGIN" + Referrer-Policy "strict-origin-when-cross-origin" + } + + log { + output file /var/log/caddy/${DOMAIN_NAME}.log + format json + } +${EXTRA_BLOCK} +} +CADDY_BLOCK +)" + + if [[ "$_CADDY_MODE" == "local" ]]; then + local _CADDYFILE="$DOCKER_DIR/caddy/Caddyfile" + local _CADDY_BACKUP="$_CADDYFILE.backup.$(date +%Y%m%d-%H%M%S)" + if [[ -f "$_CADDYFILE" ]]; then + cp "$_CADDYFILE" "$_CADDY_BACKUP" + else + touch "$_CADDYFILE" + fi + if grep -q "^${DOMAIN_NAME}" "$_CADDYFILE" 2>/dev/null; then + log_warning "${DOMAIN_NAME} already in Caddyfile — leaving the existing entry alone." + else + printf '%s\n' "$_SITE_BLOCK" >> "$_CADDYFILE" + log_success "Added ${DOMAIN_NAME} to Caddyfile (backup: $(basename "$_CADDY_BACKUP"))" + docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true + if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then + log_success "Web admin accessible at: https://${DOMAIN_NAME}" + else + log_warning "Reload failed — check: docker logs caddy" + log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile" + fi + fi + else + local _SNIPPET_DIR="$DOCKER_DIR/caddy-snippets" + mkdir -p "$_SNIPPET_DIR" + printf '%s\n' "$_SITE_BLOCK" > "$_SNIPPET_DIR/asterisk-do.caddy" + chown "$ACTUAL_USER:$ACTUAL_USER" "$_SNIPPET_DIR/asterisk-do.caddy" 2>/dev/null || true + log_success "Snippet saved: $_SNIPPET_DIR/asterisk-do.caddy" + log_info "Copy to your Caddy machine: scp $_SNIPPET_DIR/asterisk-do.caddy caddy-host:~/caddy-snippets/" + fi + fi fi # ── CrowdSec: SIP brute-force/enumeration protection ──────────────────────