Commit Graph
220 Commits
Author SHA1 Message Date
Claude 685099e9d8 Refine crowdsec geo-allowlist: drop core Eastern Europe, add Russia back
Excludes Bulgaria, Czechia, Hungary, Moldova, Poland, Romania, Slovakia,
and Ukraine per user request, while keeping the Balkans and Baltics
(several of which, e.g. Estonia, don't fit the same risk profile despite
the old Cold-War grouping). Russia added back to the allowed list per
explicit user request.
2026-07-20 23:39:58 +00:00
Claude 8132f882e1 Add optional geo-allowlist to crowdsec.sh (NA + Europe, web traffic only)
Opt-in prompt that bans any Caddy-fronted web request from outside an
editable North America + Europe country list, via a local CrowdSec
trigger scenario scoped to the existing "type: caddy" acquisition label.
Uses CrowdSec's bundled GeoLite2 enrichment data (already active with no
extra setup) rather than the firewall bouncer's separate MaxMind-key
country-CIDR feature, so no account signup is needed. SSH is untouched
so a bad edit can't lock out the session running the installer.
2026-07-20 22:42:57 +00:00
Claude 9aacb17a4d Pin X-Forwarded-Host on the Authelia portal's own Caddy block
The generated auth.<domain> block's bare "reverse_proxy authelia:9091"
let Caddy recompute X-Forwarded-Host from its own incoming request
(always auth.<domain> itself) on every hop through it, overwriting
whatever a forward_auth caller elsewhere had already set for its own
domain. Confirmed live: a remote site's forward_auth check always
evaluated as if it were for the Authelia portal itself (bypass policy),
so 2FA silently never triggered for any domain going through it.
2026-07-20 22:31:07 +00:00
Claude 2ebbe5e529 Hardcode X-Forwarded-Host in remote-Authelia forward_auth, not {host}
The previous fix used the {host} Caddy placeholder for X-Forwarded-Host,
but confirmed live it still evaluated to the upstream Authelia's own
hostname rather than the original site's — Caddy appears to rewrite the
outgoing request's Host to the upstream target before header_up
placeholders resolve for a scheme-qualified remote upstream, so {host}
echoed back the already-rewritten value. Since this site block only ever
serves one domain, hardcode it instead of depending on placeholder timing.
2026-07-20 21:06:06 +00:00
Claude 0764be44da Fix Authelia bypass on remote-Authelia forward_auth (asterisk-digital-ocean)
The remote-Authelia forward_auth block dialed a scheme-qualified upstream
(https://auth.example.com), which is a second Caddy hop. Caddy rewrites the
outgoing Host header to the upstream host for routing, and without an
explicit override X-Forwarded-Host picked up that rewritten value instead
of the original site's host. Authelia was evaluating every protected
domain as auth.example.com itself (bypass policy), so 2FA never triggered
for any domain behind the remote instance. Pin the forwarded headers to
the original request explicitly to fix it.
2026-07-20 20:09:37 +00:00
Claude 861b4478a8 Fix Authelia protection being dead code — reverse_proxy ran before the auth check
Both Caddy site block generators (the shared configure_caddy_for_service
helper, and asterisk-digital-ocean.sh's own inline template) wrote
reverse_proxy before the forward_auth/import authelia block. Caddy
doesn't reorder repeats of the same directive within a block — forward_auth
and reverse_proxy are the same directive family internally, so they run in
the order written. With reverse_proxy first, it handled and terminated
every request immediately; the auth check written after it never ran at
all. Full bypass on every domain using either generator with Authelia
protection, regardless of how correct the Authelia access_control rules
themselves were — confirmed live against a config that was otherwise
completely correct (default_policy: deny, explicit wildcard rule covering
the affected domain).

Affects every service that's ever passed `import authelia` or a
forward_auth block through configure_caddy_for_service (asterisk.sh,
wolf-pair.sh, and any future caller), plus asterisk-digital-ocean.sh's
own site block.

Moved the auth block before reverse_proxy in both generators.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 19:29:26 +00:00
Claude 6b2aaee756 wolf-pair: don't expose the pairing UI publicly when Caddy fronts it
wolf-pair has no login of its own — Authelia via Caddy is the only
protection option offered for it — but UFW opened its port to the
whole internet unconditionally, before the Caddy/Authelia prompt even
ran. Same gap just fixed for the Asterisk web admin: reachable
straight over the bare port regardless of Authelia.

Reordered so the Caddy decision happens first, and scope the port to
caddy_net's subnet via ufw_allow_from_caddy_net() instead of leaving
it open to 0.0.0.0/0 when Caddy fronts it locally. Also enables UFW
via ensure_ufw_enabled() like the Asterisk services.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 18:32:40 +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 67ab1737bf asterisk-digital-ocean.sh: enable UFW after adding its rules
Calls the new ensure_ufw_enabled() so the UFW rules this script adds
actually get enforced instead of sitting queued but inactive.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 18:20:34 +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 fe7129c974 Stop exposing the web admin port publicly when Caddy fronts it locally
UFW and the DO Cloud Firewall both opened the web admin port to
0.0.0.0/0 unconditionally, even when Caddy+Authelia was configured to
protect it on the actual domain. Caddy reaches the container over the
host's internal network (host.docker.internal), not the public
internet, so that direct port was pure attack surface: anyone could
hit http://<droplet-ip>:<port>/clients directly, fully bypassing
Authelia and the built-in web admin auth (which gets disabled
whenever Authelia is handling it instead).

Reordered the install flow so the Caddy reverse-proxy decision is
made before the firewall rules are built, and only open the web
admin port publicly when there's no local Caddy actually fronting
it (no domain, Caddy not installed, proxy declined, or a remote
Caddy machine that needs to reach it over the public IP instead).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 18:13:46 +00:00
Claude 45609c2cb1 Rename asterisk-do service to asterisk-digital-ocean
Renamed services/asterisk-do.sh -> services/asterisk-digital-ocean.sh
(register_service name, install function, install dir, and all prose/
comments) so the whiptail menu shows a clearer, more discoverable name.

Updated the functional cross-references that depend on the old name:
crowdsec.sh's SIP-log auto-detection path and acquisition filename,
caddy.sh's host.docker.internal comment, and the CLAUDE.md/README.md
docs (services table, directory listing, network-wiring example).

Container names, the Docker Compose project name, and the internal
_asterisk_do_* helper function identifiers are left unchanged since
they aren't user-facing and renaming them would add risk for no
benefit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
2026-07-20 13:57:35 +00:00
Claude 69b0e054aa Offer Caddy and CrowdSec as recommended installs in base.sh
Same pattern already used for NetBird: a simple prompt (default yes,
since these are the two explicitly called out as recommended) right
after the mandatory package/Docker/SSH setup, before the whiptail
menu. Both stay fully optional and available later from the menu
either way — this just surfaces them earlier as a nudge, matching
how most other services in this repo end up wanting a reverse proxy
and something watching for brute-force/scan traffic.
2026-07-20 13:45:24 +00:00
Claude 52d46c5552 Remove asterisk-do's auto-install of Caddy/CrowdSec/base/extras
asterisk-do previously offered to auto-install base, Caddy, CrowdSec,
and a numbered extras menu (authelia/ntfy/watchtower/wg-easy/netbird/
backup) on top of its own setup, layering a second install flow on
top of the whiptail menu setup.sh already provides. Strips all of
that back out — asterisk-do now only installs Asterisk + coturn, same
scope as any other service. Caddy/Authelia integration (reverse
proxy, cert sync, SSO) is kept, since it only activates when those
are already installed — no auto-install behind it. CrowdSec SIP
protection still wires up automatically via crowdsec.sh's own
asterisk-do detection, regardless of which one installs first.

Also fixes a real regression from ensure_caddy_network (added
earlier): it created caddy_net via a bare `docker network create`,
which doesn't carry Compose's ownership labels, so Caddy's own
non-external network declaration conflicted with it and failed to
start ("network exists but was not created by compose"). Caddy's
compose file now declares caddy_net as external: true like every
other service, since ensure_caddy_network is the single creator for
all of them, Caddy included.
2026-07-20 13:39:23 +00:00
Claude b6046b3ca1 Ensure caddy_net exists via require_docker instead of per-service
43 services declare caddy_net as "external: true" in their compose
file, meaning they require it to already exist — but only Caddy's own
compose file actually creates it (authelia.sh was the sole exception,
with its own inline check-and-create). Installing any of the other 42
before Caddy fails outright with "network caddy_net declared as
external, but could not be found."

Adds ensure_caddy_network to lib/common.sh, called from require_docker
(which every install_* function already calls first), so the network
exists regardless of install order without touching each service file.
Removes authelia.sh's now-redundant duplicate of the same check.

Also documents in CLAUDE.md that network_mode: host services (asterisk/
asterisk-do) need host.docker.internal, not localhost, when Caddy
reverse-proxies to them — the fix from the previous commit.
2026-07-20 04:14:56 +00:00
Claude 6c48bdc707 Fix Caddy reverse-proxy target for host-network services
Caddy runs in its own container on the caddy_net bridge network, so
"localhost" in a Caddyfile site block resolves to Caddy's own
container — never the host, and never a sibling container. That
broke every reverse proxy pointed at a network_mode: host service
(confirmed live with asterisk-do's web admin): once nothing else
(like a forward_auth redirect) intercepted the request first, Caddy
couldn't actually reach the upstream.

- services/caddy.sh: add extra_hosts so host.docker.internal resolves
  inside the Caddy container (Linux Docker needs this explicitly —
  it's automatic only on Docker Desktop).
- lib/common.sh's configure_caddy_for_service: bare-port upstreams
  (its documented "host-network service" case) now target
  host.docker.internal instead of localhost.
- services/asterisk-do.sh: its self-contained Caddy block (doesn't go
  through configure_caddy_for_service) gets the same fix for local
  Caddy, and now correctly targets the droplet's public IP instead of
  localhost for the remote-Caddy snippet case, which had the same bug.

services/asterisk.sh needs no direct change — it already goes through
configure_caddy_for_service, so it inherits the fix.
2026-07-20 04:11:24 +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 57797c4ad7 asterisk-do: stop guessing the web-admin Caddy domain, hardcode it
Real-world failure: configure_caddy_for_service's own domain prompt
defaults to "<subdomain>.${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.
2026-07-19 15:30:37 +00:00
Claude f00e19099e asterisk-do: switch extras prompt to a numbered, comma-separated menu
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.
2026-07-19 15:25:14 +00:00
Claude 79b27a1594 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 <base-url>/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.
2026-07-19 15:19:19 +00:00
Claude 16a4985e04 asterisk-do: offer to run base setup first if not already done
Naming a service directly (sudo ./setup.sh asterisk-do) bypasses
setup.sh's own first-run base step entirely — essential packages, SSH
key import, disabling password auth. Docker still gets installed
either way (asterisk-do's own require_docker handles that), but the
SSH-hardening part of this setup's security story was silently
skipped on a genuinely fresh droplet unless the user knew to run
`base` separately first.

Checks the same marker setup.sh itself uses for "is base installed"
(command -v ncdu) and offers to run install_base directly if not —
same cross-service-call pattern already used for Caddy/CrowdSec/etc.
Verified end-to-end in a real sandbox run: base actually installed
packages, and execution correctly continued through the rest of the
asterisk-do flow afterward.
2026-07-19 13:58:10 +00:00
Claude 2e6f14f17d Support remote Authelia and remote/central CrowdSec LAPI
Both let the DO droplet lean on services already running on a
homelab instead of duplicating them locally, per the RAM-budget
discussion (Authelia+Redis and a second CrowdSec LAPI+DB add up).

crowdsec.sh: new step lets this agent register against a remote LAPI
(cscli lapi register -u <url>) and disables its own local API server
by removing the api.server block from config.yaml (backed up first;
verified the exact block boundaries against CrowdSec's actual default
config.yaml from upstream before writing the awk removal). Parsers,
scenarios, and the firewall bouncer still run locally regardless —
only banning decisions centralize, and only after the registration is
approved with `cscli machines validate` on the central machine, which
this script can't do since that's a different box. The final restart
step is skipped with an explanation when registration is pending,
instead of showing a misleading "failed to restart" for an expected
state.

asterisk-do.sh: when no local Authelia is installed, the web-admin
Caddy step now offers a remote Authelia option instead, building the
same forward_auth block inline (authelia.sh's shared Caddy snippet
only exists for local installs) targeting either a bare host:port
(e.g. a NetBird mesh IP) or a full https:// URL. Documents that this
couples web-admin availability to the remote instance's reachability,
while SIP/calling on the droplet stays unaffected either way.

Both changes verified: the config.yaml block-removal awk logic tested
against CrowdSec's real upstream default file structure, the remote
Authelia forward_auth block construction tested in isolation, and
full regression runs confirm the default (declined) path through both
new prompts is unchanged.
2026-07-19 11:42:40 +00:00
Claude 36e56c343b asterisk-do: add netbird to the optional extras
Adds a 'netbird' keyword to the existing extras prompt, dispatching
services/base.sh's _base_setup_netbird helper — a plain function like
any other once setup.sh sources every services/*.sh file, despite its
underscore-prefixed, not-independently-registered naming. Its own
prompt already defaults to enabling NetBird's built-in SSH server
(--allow-server-ssh), which is what makes the 'backup' extra usable
against a home machine without port-forwarding a router: install
NetBird here and on that machine, join both to the same network, and
Borg's SSH remote target becomes the home machine's mesh IP instead of
a public address. Skips cleanly if NetBird's already installed.
README's Optional extras section documents the pairing.
2026-07-19 05:04:02 +00:00
Claude af7fe36283 asterisk-do: wire in authelia/ntfy/watchtower/wg-easy, borg-backup
Extends the self-contained pattern from Caddy/CrowdSec to five more
services, offered through one consolidated "Install:" prompt instead
of five separate interruptions:

- authelia: only offered if Caddy is present (it's useless without
  Caddy's forward-auth snippet); dispatched right where Caddy's state
  is already known.
- wg-easy: installed alongside the other firewall rules so its port
  lands with them. Only 51820/udp (the VPN handshake) goes on the
  public firewall — the web UI (51821) is deliberately left closed,
  documented as reachable via SSH tunnel instead, since exposing a
  VPN's own admin panel publicly is a real foot-gun.
- ntfy, watchtower: independent, dispatched after CrowdSec. Watchtower
  section is explicit that it only benefits coturn (a pulled image) —
  Asterisk is a local Dockerfile build with no registry tag to check.
- backup (borg-backup): dispatched last. Documented clearly as a
  config/data backup to a local machine or SSH remote, not a full
  droplet image — the alternative to DO's paid Droplet Backups.

Every sub-install this calls does its own `cd` into ~/docker/<name>;
each call site restores `cd "$EA_DIR"` afterward so the later bare
`docker compose up -d --build` still targets the right directory.
Verified in isolation (mocked cd side effects) since driving five
real interactive sub-installs through piped stdin isn't practical.

README updated with an "Optional extras" section covering all five.
2026-07-19 04:02:09 +00:00
Claude 5eddab9f9a asterisk-do: auto-install Caddy/CrowdSec, fix confusing domain prompt
Self-contained by default now: if Caddy or CrowdSec aren't already on
the box, asterisk-do offers to install them itself (calling their
install_ functions directly — setup.sh sources every services/*.sh up
front, so they're already in-process during a wizard run). Standalone
single-file runs get a manual pointer instead, since those functions
don't exist outside the full repo checkout.

Also fixes the confusing "Configure Caddy reverse proxy for Asterisk
Web Admin" domain prompt: it used to ask for a second, independent
domain, which silently breaks the TLS cert sync if it doesn't match
the SIP FQDN exactly (Caddy only holds a cert for the domain it's
actually serving). It now always reuses the SIP FQDN automatically —
reconstructing configure_caddy_for_service's subdomain default so the
common case (SIP domain is a subdomain of SITE_DOMAIN) needs zero
extra input, with clear wording either way. FQDN prompt, README, and
final summary updated to match.
2026-07-19 03:48:34 +00:00
Claude dfc298f6ab Wire crowdsecurity/asterisk into crowdsec.sh for asterisk-do
Vendor's logger.conf only sent Asterisk's security-level log lines
(auth failures, SIP registration scanning) to the console, i.e.
Docker's stdout — not a file CrowdSec could tail. asterisk-do.sh now
patches its copy of entrypoint.sh (vendor/ untouched) to also write
those events to /var/log/asterisk/full, which is bind-mounted to
~/docker/asterisk-do/logs/full on the host.

crowdsec.sh now detects that directory and, if present, installs the
crowdsecurity/asterisk collection (asterisk_bf + asterisk_user_enum
scenarios) with a matching log acquisition — mirroring the existing
Caddy detection pattern. Order-independent: asterisk-do's install
summary tells the user to rerun crowdsec if it's already installed,
since detection only runs during crowdsec's own install step.
2026-07-19 03:20:36 +00:00
Claude 4f5f264a96 asterisk-do: auto-add swap on low-RAM droplets, document the $4/mo tier
DigitalOcean doesn't provision swap by default and the $4/mo (512MB)
droplet has little headroom once Docker + Asterisk + coturn are
running. The installer now detects RAM <=2GB with no existing swap and
offers to add a persistent 2GB swapfile before doing anything else, so
that tier is safe to use instead of risking an OOM kill under load.
README updated with the corrected sizing table.
2026-07-19 02:57:09 +00:00
Claude d8072b61f4 Add asterisk-do: Easy Asterisk PBX tuned for a DigitalOcean droplet
Duplicates services/asterisk.sh (left untouched) into a DO-specific
variant: auto-detects the droplet's public IP/ID via the DO metadata
service, always assumes a public FQDN (no LAN/VLAN prompts), offers to
provision a matching DigitalOcean Cloud Firewall via doctl (never
touching one that's already attached), and documents droplet sizing,
firewall rules, and Sipnetic client setup in the generated README.
2026-07-19 02:47:24 +00:00
Outis 6224fe10b8 Merge pull request #175 from outis1one/claude/frigate-sky-cam-timelapse-t19akr
sky-cam: add retry-on-failure to sunrise/seasons/moon jobs
2026-07-09 09:02:54 -04:00
Claude 9bf2172786 sky-cam: add retry-on-failure to sunrise/seasons/moon jobs
install.sh generates the sunrise, sunrise-upload, seasons, and moon jobs
as Type=oneshot with only OnFailure=notify - a transient ffmpeg/network
blip fails the whole day's job with just an alert, no retry.

Add systemd drop-in overrides (Restart=on-failure, RestartSec=60,
StartLimitBurst=3 within a 10 min window) for each of these units after
install.sh runs. Drop-ins live outside the files install.sh generates,
so they survive re-running install.sh (e.g. after editing
sky-cam.conf), unlike a direct edit to the generated unit which would
be silently overwritten next time. systemd only fires OnFailure once
retries are exhausted, so this doesn't add notification spam - just
one alert after 3 tries, 60s apart.

capture.sh/capture-watchdog.sh already have Restart=on-failure baked
into install.sh's own generation (Type=simple, long-running) and don't
need this.
2026-07-08 15:02:36 +00:00
Claude c1c6d732df Merge origin/main into claude/bootstrap-script-404-d8dc7h 2026-07-02 18:11:08 +00:00
Claude 702c184abf base.sh: detect NVIDIA GPU, install driver + Container Toolkit
Nothing in the repo actually installed the NVIDIA driver or
nvidia-container-toolkit — ai-gpu.sh, wolf.sh, etc. all assumed both were
already present. Adds _base_setup_nvidia_gpu, called during base install
right after Docker:

- No-ops silently on boxes without an NVIDIA GPU (lspci VGA/3D controller
  check) so non-GPU installs are unaffected
- If a GPU is present but nvidia-smi isn't working, offers to run
  'ubuntu-drivers devices' (shown to the operator) then
  'ubuntu-drivers autoinstall', and warns a reboot is required
- If Docker is present and nvidia-container-cli is missing, offers to
  install NVIDIA Container Toolkit and run
  'nvidia-ctk runtime configure --runtime=docker' so GPU-accelerated
  Docker services (ai-gpu, wolf, paintplus, iopaint) can request the GPU
- Offers to reboot immediately if a driver install requires it

Verified with a mocked-lspci/nvidia-smi/ubuntu-drivers test harness across
three scenarios: no GPU (silent no-op), GPU with no driver (full install +
toolkit + reboot prompt flow), and GPU with driver already active (skips
driver prompt, still offers toolkit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
2026-07-02 16:46:27 +00:00
Claude d374f4983a Add SSH Host alias management (base wizard, standalone service, docs)
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
2026-07-02 16:20:22 +00:00
Claude dc162b8f8d sky-cam: retire motionEye via direct RTSP capture, drop Frigate detour
capture.sh already replaces motionEye/any NVR itself - it just needs
each camera's RTSP URL. services/sky-cam.sh never actually prompted for
CAM_RTSP_<cam>, so capture/audio never had anything to connect to.

- Prompt per camera for its RTSP URL -> CAM_RTSP_<cam> in .env
- Prompt for sunrise mic / optional ambient audio library
- Fix Mattermost integration: sunrise2mm.py reads mattermost_url/
  access_token/channel_id (bot-token REST upload), not the
  MM_WEBHOOK_URL/MM_CHANNEL incoming-webhook scheme the installer used
  to write - uploads never worked before this
- Add optional ntfy push notifications
- Auto-generate SCHEDULE_SEASONS_<cam> (staggered 30 min apart) for
  every configured camera, not just the stock east/north/south, so
  install.sh wires up every applicable systemd timer for any camera set

Removes services/sky-cam-frigate.sh entirely - routing sky-cam's frames
through Frigate (via export API or restream) turned out to be solving a
problem that doesn't exist; sky-cam's own capture.sh talking directly to
each camera is simpler and has no quality/resolution tradeoffs. Frigate
continues to run fully independently for NVR/detection.
2026-07-02 05:26:24 +00:00
Claude 3c3183bbda sky-cam-frigate: source frames from Frigate's restream, not an export API
The real sky-cam repo's capture.sh already replaces MotionEye/any NVR
itself (plain ffmpeg RTSP frame-grab) - it and daily_sunrise_video.sh's
optional audio capture are the only places that touch a camera's RTSP
URL directly. Every other script (4-seasons, montage-mvt, year-end-join,
moon-track, moon-phase-monthly) only reads JPEGs/audio already on disk.

So the entire motionEye->Frigate transition is pointing CAM_RTSP_<cam>
at Frigate's go2rtc restream (rtsp://<frigate-host>:8554/<cam>) instead
of the camera directly - no upstream script changes needed. Replaces
the previous frigate-retime.sh/export-API approach, which solved a
problem (matching an arbitrary recording length to music duration) that
sky-cam's own 4-seasons.sh/montage-mvt.sh already handle via JPEG frame
counts.
2026-07-02 05:03:29 +00:00
Claude ce0a14904f Add sky-cam-frigate service using Frigate exports for timelapse source
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).
2026-07-02 04:17:53 +00:00
Outis 78f6365bbe Merge pull request #170 from outis1one/claude/asterisk-dns-filtering-vlan-5pmz12
asterisk: default to FQDN mode, auto-detect VLANs, auto-sync Caddy certs
2026-07-01 23:52:47 -04:00
Claude 42a38c9397 frigate.sh: detect existing camera config, offer keep/backup/add/remove
Previously re-running the installer always overwrote config.yml and .env
from scratch, silently discarding any real camera credentials already on
disk. Now install_frigate parses an existing config.yml + .env (best-effort,
matching this installer's own output shape) and presents a numbered list
of detected cameras with a menu:

  [1] Keep everything as-is (no changes at all)
  [2] Backup existing config and start fresh
  [3] Add more cameras (keep these)
  [4] Remove cameras (choose numbers, or 'all'), then optionally add more

Implementation switches from building config.yml/.env as concatenated text
blocks inline in the collection loop to parallel CAM_* bash arrays
(name/ip/port/var-names/enabled/notify/substream-suffixes), so cameras can
be parsed, listed, removed, and re-rendered independently:

- _frigate_parse_existing: reads go2rtc streams + cameras: enabled/notifications
  from config.yml, and credential values from .env, into the CAM_* arrays
- _frigate_review_existing: numbered menu, mutates arrays per choice
- _frigate_next_suffix_int: kept cameras retain their existing FRIGATE_RTSP_USER[N]
  var names unchanged; new cameras get the next unused numeric suffix so
  credentials never collide after removals
- _frigate_camera_wizard / _frigate_render_config: same prompts and output
  shape as before, now array-driven so kept + new cameras render uniformly
- _frigate_backup_existing: copies config.yml/.env/docker-compose.yml to a
  timestamped backup-YYYYMMDD-HHMMSS/ dir before any destructive rewrite

Verified with a 5-scenario test harness (fresh install, keep-as-is producing
byte-identical output, add-camera preserving existing credentials, remove-
by-number without var collisions, backup-and-fresh) before committing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
2026-07-01 20:25:55 +00:00
Claude 7dc9a762a0 frigate.sh: interactive camera wizard generates go2rtc + cameras config
Previously Frigate always wrote a placeholder config.yml the operator had
to hand-edit to add cameras. Now install_frigate prompts to add cameras
one at a time (name, RTSP IP/port/user/password/path, optional sub-stream,
enabled, notifications), matching the go2rtc + cameras structure used in
production frigate configs:

- Each camera gets a go2rtc stream entry (+ optional _sub for detection)
  and a cameras: block with ffmpeg inputs/roles, detect, notifications
- RTSP credentials/IPs are written to .env as FRIGATE_* variables (first
  camera gets FRIGATE_RTSP_USER/PASSWORD, later cameras get numbered
  suffixes _1, _2, ... to avoid collisions) and referenced in config.yml
  via Frigate's {FRIGATE_VAR} substitution syntax — secrets never appear
  in the YAML directly
- docker-compose.yml now includes env_file: .env so those vars actually
  reach the container for substitution to work
- Skipping all camera prompts falls back to the original starter
  config.yml for manual editing, preserving existing behavior
- README and DRY-RUN summary updated to reflect the new flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
2026-07-01 19:49:26 +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 3d976b84e3 base.sh: surface Docker install failures instead of swallowing them
require_docker returning non-zero was silently ignored (no set -e).
Add explicit warning so the operator sees the failure; setup.sh already
has an unconditional Docker check after base that will retry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
2026-07-01 16:08:00 +00:00
Claude c09dd775ea base.sh: add Docker, SSH server/hardening, and NetBird to base install
- require_docker now runs as part of base so Docker is present on every box
- Install openssh-server, offer GitHub (gh:) and Launchpad (lp:) key import
  via ssh-import-id; disable password auth only after keys are confirmed imported
- Handle Ubuntu cloud-init drop-in that re-enables PasswordAuthentication
- Offer NetBird install with optional --allow-server-ssh flag and setup key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq
2026-06-28 15:00:51 +00:00
Claude 71ef582873 kyber-server: fix Wine crash + map rotation format
Three root-cause fixes found by reading the Docker image source:

1. Wine/Proton page fault: Docker's default seccomp profile blocks
   system calls that Wine Proton GE requires. Fix: security_opt:
   seccomp=unconfined + shm_size: 256m (Xvfb needs /dev/shm for
   MIT-SHM extension; 64 MB default is too small).
   Added network_mode: host for game traffic (dynamic UDP ports).

2. KYBER_MAP_ROTATION exit 64: the Kyber CLI decodes base64 and
   parses newline-separated "MODE;MAP_PATH" lines, not JSON objects.
   Our JSON [{map:...,mode:...}] format split on semicolons into one
   field → ExitCode.usage (64). Fixed builder to emit MODE;MAP_PATH\n
   lines and updated .env comment + README example.

3. GPU passthrough removed: Proton GE includes DXVK which crashes
   headlessly when a GPU is passed through (no Vulkan display). The
   server needs no GPU; removing passthrough is the correct fix.

Also: install libgamemode0:i386 on the host (Wine/Proton dep),
add alphanumeric-password warning (special chars → INVALID_PASSWORD).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014be1aK9G8CY2msho5LjxR4
2026-06-26 15:26:55 +00:00
Outis f97a4266b6 Merge pull request #149 from outis1one/claude/gifted-shannon-5ti8vx
Claude/gifted shannon 5ti8vx
2026-06-26 09:34:38 -04:00