Generalizes services/pstn-trunk.sh (renamed from voipms-trunk.sh in the
prior commit) away from VoIP.ms specifics - any IP-authenticated SIP
provider works, VoIP.ms is just the suggested default. Adds:
- Role-based outbound permission: a configurable allow-list of extensions
that may dial PSTN numbers (regex-gated on CHANNEL(peername)), separate
from internal extension-to-extension dialing which stays open to everyone
regardless. Blank list preserves the original "everyone can dial out"
behavior.
- Inbound ring-group: rings a configurable list of extensions instead of a
single hardcoded one.
- ntfy alerts: immediate on denied (unauthorized extension) or rejected
(concurrency cap hit) calls, plus an hourly cron-driven check that alerts
once per month when estimated spend crosses a threshold and every hour
call volume looks like a burst. Uses a self-contained pipe-delimited call
log rather than Asterisk's CDR, to avoid depending on CDR module
availability and CSV comma-quoting.
- Settings persisted to .pstn-trunk.env so "update in place" reapplies
everything from that file instead of fragile re-parsing out of generated
Asterisk config (which had a real bug: update mode was extracting the
wrong Dial(PJSIP/...) line).
Tested end-to-end against a sandboxed copy of the real vendor files:
permission-gate regex, ring-group dial-string construction, ntfy line
injection/removal, and the usage-alert script's threshold/burst/monthly-
dedup logic all verified with synthetic data. Caught and fixed a sed `&`
escaping bug in the ring-group substitution before it shipped (RING_DIAL
contains literal `&` join characters, which sed's replacement syntax
otherwise treats as "insert the match").
Adds a VoIP.ms SIP trunk on top of asterisk-digital-ocean: IP-authenticated
trunk (no password stored), NANP-only outbound dialplan, a global 3-call
concurrent cap via GROUP()/GROUP_COUNT(), and inbound routing to one
extension. Config lives in its own include files rather than being
appended directly to pjsip.conf/extensions.conf, since Easy Asterisk fully
regenerates both from its own internal state — the includes are patched
into the vendor's generator functions so they survive that regeneration.
Wires the new service into setup.sh's is_installed() and README's services
table, and updates docs/pstn-calling-voipms-plan.md to reflect what's now
implemented vs. still open (spend/volume alerting, live-account
verification).
New service, native on the host (not Docker) so it can call cscli and
read Asterisk's security log directly without bridging the
container/host boundary or exposing CrowdSec LAPI credentials to a
containerized frontend.
- Security Log tab: parses ~/docker/asterisk-digital-ocean/logs/full
for SIP auth failures (wrong password, unknown extension, etc.) with
timestamp/account/remote IP, classified by severity.
- CrowdSec tab: current bans via cscli, a delete/unban button per
entry, and ASN-exempt management for the Asterisk brute-force
scenarios (services/crowdsec.sh) without SSHing in.
- Link out to the existing Asterisk web admin (reads its domain from
asterisk-digital-ocean's own .env, doesn't hardcode or embed it).
Runs as a dedicated unprivileged system user (secdash), with sudo
scoped to exactly three commands via /etc/sudoers.d/security-dashboard
(cscli decisions delete --id <digits>, cscli decisions list -o json,
systemctl restart crowdsec) — validated with visudo -c. Listens on
127.0.0.1 only, reachable through Caddy, and refuses to proceed without
explicit confirmation if no Authelia (local or remote) is configured,
since this page can delete active security bans.
Stdlib-only Python (no framework), matching the RAM-conscious pattern
already used for Easy Asterisk's own web admin. All embedded code
(bash, Python, JS) syntax-checked; the generated sudoers rule
validated with visudo -c -f.
Lets 'ssh <alias>' connect directly to user@host instead of retyping it —
especially useful once machines are reachable over NetBird/VPN and have
IPs that aren't worth memorizing.
- lib/common.sh: ssh_config_path/add_ssh_host_alias/list_ssh_host_aliases/
remove_ssh_host_alias helpers, operating on the invoking user's own
~/.ssh/config (not root's) with correct 700/600 permissions and ownership
- base.sh: after SSH key import, optionally add one or more Host aliases
interactively as part of the base install
- services/ssh-config.sh: new standalone service (sudo ./setup.sh ssh-config)
to list/add/remove aliases any time, independent of base install; follows
the existing non-Docker standalone-bootstrap pattern (see crowdsec.sh)
- setup.sh: ssh-config never shows [installed] since it's a repeatable
management tool, not a one-time install
- README: new 'SSH Host aliases' section, base row and wizard-flow step 1
updated, ssh-config added to the extras group and copiable service list
Verified end-to-end with a test harness: add with defaults, add with a
custom user/port, list (correct numbering), and remove-by-name preserving
the other entry and file permissions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Duplicates services/sky-cam.sh into a Frigate-backed variant that pulls
recordings via Frigate's export API instead of a JPEG image folder.
Includes a frigate-retime.sh helper that exports a coarse timelapse,
measures its actual duration with ffprobe, and re-encodes once with a
computed setpts factor to hit an exact target length (e.g. a Four
Seasons movement's runtime).
Group membership added by 'usermod -aG docker' (in require_docker) doesn't
apply to the shell that invoked sudo — only to new logins. Users had to
manually run 'newgrp docker' or reconnect SSH after every install. Since a
child process can't change its parent shell's group list directly, the
practical fix is to exec a fresh 'su - ' login shell at the end
of the guided flow, which re-reads /etc/group and lands the user back in
the same terminal with docker access already active.
Gated on: running via sudo (SUDO_USER set), interactive (not --unattended),
docker group exists and the user is actually a member, and stdin is a real
tty — so this never fires for scripted/explicit-service/piped invocations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
The 'Install Caddy now?' prompt ran regardless of the just-answered
Caddy location question, so choosing 'remote' still asked whether to
install Caddy locally — contradicting the choice made one prompt earlier.
Gate it on CADDY_MODE being local (or unset, for configs predating the
wizard split).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Previously the Caddy-location question lived inside run_site_configure,
gated behind 'Configure site defaults now? (y/n)'. Answering 'n' (e.g.
because Caddy is on a different box and you don't care about domain/tz
autofill) meant CADDY_MODE never got set, which silently disabled Caddy
prompts for every service for the life of the install (configure_caddy_for_service
falls through to mode 'none' and returns immediately).
Split into two steps:
1. ask_caddy_location() — always runs on first setup.sh invocation,
independent of any other prompt, and persists CADDY_MODE immediately.
2. run_site_configure() — now only asks timezone/domain/Caddy-network,
and is only offered when CADDY_MODE=local (those defaults are only
useful for FQDN autofill tied to a locally-managed Caddyfile).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Reorders the site defaults wizard so 'Where does Caddy run?' comes before
timezone/domain, since it's the more fundamental choice and the answer
context matters when explaining the other prompts. Also skips the Caddy
Docker network prompt entirely when Caddy isn't running locally — that
setting is only relevant to services joining a local Caddy container's
bridge network; remote/none mode proxies via localhost:PORT + snippet
files instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
The Docker check+install was inside the else branch that only runs when
base has never been installed. On re-runs (base already present) Docker
was silently skipped and only warned about. Move the check outside the
if/else so Docker is always installed if missing, regardless of whether
base was skipped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
whiptail fix:
- bootstrap.sh: redirect stdout and stderr to /dev/tty alongside stdin so
whiptail has full terminal control for raw mode (arrow keys, highlighting)
- setup.sh: run 'stty sane' on /dev/tty before the menu loop to reset any
stale terminal state from SSH reconnections or prior sessions
Installed-service summary:
- Print a grouped list of all currently-installed services before every
menu session so the operator knows the current state at a glance
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Two fixes:
1. Export TERM (default xterm-256color) early — whiptail needs a valid
TERM to enter raw mode; when bash is started via pipe TERM may be
unset, causing keypresses to leak to the shell instead of the menu
2. Add </dev/tty to both whiptail calls so keyboard input always comes
from the controlling terminal regardless of how stdin was redirected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
Three fixes:
1. configure_caddy_for_service: remove the '!= example.com' filter that
silently dropped any valid domain matching that string; now any non-empty
SITE_DOMAIN is used as the default subdomain suggestion
2. load_site_config: trim leading/trailing whitespace from key and val so
hand-edited .config files with extra spaces still parse correctly
3. setup.sh: call load_site_config after the site wizard saves so the
in-memory values are guaranteed fresh for all subsequent service installs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
The old CADDY_REMOTE_HOST variable was confusingly named — it sounded like
the Caddy server's address but actually stored this machine's IP (so Caddy
knew how to reach services here). Services don't need to know where Caddy
is; they only need to know whether to write a Caddyfile or create a snippet.
Changes in lib/common.sh:
- Add CADDY_MODE=local|remote|none as the authoritative setting
- load_site_config: parse CADDY_MODE; if old CADDY_REMOTE_HOST present and
CADDY_MODE unset, infer CADDY_MODE=remote (backward compat)
- save_site_config: write CADDY_MODE, drop CADDY_REMOTE_HOST output
- configure_caddy_for_service: use CADDY_MODE for mode detection; for remote
snippets auto-detect this machine's primary IP via hostname -I instead of
requiring a stored value (still falls back to CADDY_REMOTE_HOST if present
in an old .config)
Changes in setup.sh (run_site_configure wizard):
- Replace free-text "Caddy remote host" prompt with a 3-choice menu:
[1] This machine [2] Remote machine [3] None/skip
- Existing installs with CADDY_REMOTE_HOST pre-select option 2 automatically
https://claude.ai/code/session_01S7UecmQRG6CKTYPoBqbVLj
New site config key: CADDY_REMOTE_HOST (set via 'sudo ./setup.sh configure').
When set, configure_caddy_for_service operates in "remote" mode instead of
writing to a local Caddyfile:
- Upstream uses CADDY_REMOTE_HOST:PORT (host IP, not container name)
- Snippet saved to ~/docker/caddy-snippets/<subdomain>.caddy
- User is shown scp/rsync commands to copy it to the Caddy machine
Three modes in configure_caddy_for_service (lib/common.sh and inline stubs):
local: ~/docker/caddy/ exists → write Caddyfile + reload (existing behavior)
remote: CADDY_REMOTE_HOST set → save snippet, print copy instructions
none: neither configured → silent return (unchanged)
All 31 service standalone bootstrap stubs updated with the new logic.
CADDY_REMOTE_HOST global added to all 42 standalone bootstrap sections.
setup.sh configure now prompts for CADDY_REMOTE_HOST with a clear explanation.
wolf.sh: add missing stubs (configure_caddy_for_service, write_readme,
prompt_yn, ensure_docker_dir_ownership) and the Authelia/Caddy/start calls
that were missing from the install function.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
Apt-based service for Android/iPhone ↔ Linux integration: shared
clipboard, notifications, file transfer, remote input. Works on Ubuntu
(GNOME) and Linux Mint Cinnamon. Opens UFW ports 1714-1764 automatically.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
lib/common.sh:
- detect_os(): reads /etc/os-release into OS_DISTRO, OS_VERSION,
OS_CODENAME globals (exported, auto-called on source)
- ubuntu_version_ge(): numeric version comparison helper
- pip_user_install(): central wrapper for pip3 install --user so any
future version-specific flags are in one place
setup.sh:
- Both header banners now show detected OS line (e.g., "Ubuntu 24.04 (noble)")
- First-run path warns if not Ubuntu or < 24.04
services/sky-cam.sh, services/sync-cc.sh:
- Replace inline pip3 invocations with pip_user_install helper
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
On second run, is_installed base (command -v ncdu) detects that base
packages are already present and jumps straight to the service menu,
skipping the required-setup banner, confirm prompt, and apt-get install.
The first-run path is unchanged; `sudo ./setup.sh base` forces reinstall.
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
Introduces a one-time configuration wizard (sudo ./setup.sh configure)
that stores SITE_TZ, SITE_DOMAIN, and SITE_CADDY_NET in ~/docker/.config.
Every service now uses these as prompt defaults so the user types common
values once instead of re-answering the same questions for each service.
- lib/common.sh: load_site_config / save_site_config; auto-loads on source;
backward-compat BASE_DOMAIN alias kept for old .config files
- setup.sh: run_site_configure wizard; first-run offer after base install;
`sudo ./setup.sh configure` command to update defaults at any time
- 14 services: TZ_VAL now honours SITE_TZ, falling back to /etc/timezone
- 3 inline-heredoc services (filebrowser, homeassistant, ntfy): same fix
- authelia: SITE_TZ/SITE_DOMAIN as prompt defaults; SITE_CADDY_NET replaces
hardcoded caddy_net throughout (env, compose patch, network creation)
- minecraft, frigate-audio: simplify BASE_DOMAIN read to use SITE_DOMAIN
- sky-cam: SITE_TZ as default for timezone prompt
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
sky-cam (cameras/non-docker):
Clones outis1one/sky-cam via bootstrap.sh to ~/sky-cam. Prompts for
latitude, longitude, timezone, camera names, BASE_DIR, and optional
Mattermost webhook. Patches sky-cam.conf and installs systemd user
timers via the repo's install.sh. Produces sunrise clips, Four Seasons
timelapse, moon-track, and monthly moon-phase images.
frigate-audio (cameras/docker):
Full stack from outis1one/frigate_w_audio: Frigate 0.17 NVR +
Mosquitto MQTT broker + frigate-notify → ntfy push alerts. Audio-ready
config template with face recognition and LPR pre-configured. Prompts
for camera credentials, media storage path (supports drive detection),
MQTT password (auto-generated), and ntfy server. Bootstraps the
Mosquitto passwd file. Detector choice: CPU / USB Coral / PCIe Coral.
Hardcoded media path from upstream replaced with a configurable prompt.
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
Adds sync_cc as an extras service module:
- extras/sync_cc.py: the Python tool (3196 lines) — 8 modes: SYNC,
GENERATE, BATCH, RENAME (TMDB), EXTRACT, REMUX, EMBED, BURNSUBS
- services/sync-cc.sh: installs system deps (python3, ffmpeg, mkvtoolnix,
ccextractor), pip installs openai-whisper + ffsubsync, copies the script
to ~/sync-cc/, prompts for TMDB API key → .env, creates /usr/local/bin/sync-cc
wrapper so users run it from any directory containing video/SRT files
Heavy optional deps (easyocr, pgsreader) are installed on first use by the
script itself. GPU (CUDA/MPS) is used automatically if detected.
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
Every service from ubuntu-post-install-24.04-crowdsec.sh is now a module.
35 services across 8 categories; setup.sh is the primary install path.
- services/linux-to-sync.sh (extras): clone private repo via SSH or PAT
- setup.sh: is_installed case for linux-to-sync (~/.git marker)
- MODULAR.md: migration table updated to show full inventory
- VERSION: 0.9.11 → 1.0.0 (parity achieved)
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
Adds services/silent-send.sh — installs the Silent Send browser extension
(client-side PII redaction for AI chat). Non-docker module: installs git +
Node.js >=18 (NodeSource) + npm, clones outis1one/silent-send to ~/silent-send,
runs npm install (readies web-ext for Firefox build/sign), optionally builds a
signed Firefox .xpi, and prints per-browser load/build instructions. README
written to the checkout.
Introduces a new 'extras' category for non-docker add-ons pulled from other
repos, wired into setup.sh CATEGORY_ORDER (gaming -> extras -> backup) with an
is_installed marker. MODULAR.md groups list updated.
Bumps version to 0.9.9.
Introduce the modular post-install structure chosen for reconciling 'one
source of truth' with 'run just the service I want':
- lib/common.sh: shared helpers (logging, prompts, ownership, Caddy wiring) and
a service registry. Single implementation of each helper.
- setup.sh: dispatcher — interactive menu, run-one (./setup.sh <name>), --list,
--dry-run, --unattended. Sources lib + services/*.sh (self-registering).
- services/base.sh: essential CLI packages incl. glow (Charm apt repo).
- services/homeassistant.sh: first migrated service (bridge/host networking,
trusted_proxies, Caddy integration).
- MODULAR.md: architecture, how to add a module, migration status.
- Groups: base/homelab/gaming/backup. Gaming group makes this a base for
homelab OR gaming boxes.
Also add glow as a default app to the live -crowdsec scripts' essential
packages so it's installed today regardless of entry point.
Verified: bash -n on all new files; ./setup.sh --list groups services;
dry-run run-one routes correctly.
https://claude.ai/code/session_017eA2qqq9jfF2tNtpUYL8vK