28 Commits

Author SHA1 Message Date
57_Wolve c6b0f7d418 Merge pull request 'feat(copyparty): add file-server deployment with SFTP/FTPS + security-notices updater' (#5) from claude/agitated-bose-f427e5 into main
Reviewed-on: #5
2026-06-29 20:57:39 +00:00
57_Wolve c00ca055f2 feat(copyparty): add file-server deployment with SFTP/FTPS + security-notices updater
New deployments/copyparty/: copyparty (copyparty/ac) behind Caddy/LE for the
web UI/WebDAV, plus its own SFTP (password auth) and FTPS listeners published
directly. Ships update.sh, which drives container updates off copyparty's
security-advisories API (api.copyparty.eu/advisories) -- policies latest|security|off.

- Real client IP end-to-end: Caddy XFF/X-Real-IP + copyparty xff-src: lan.
- SFTP host key + self-signed FTPS cert generated/persisted in /cfg; admin
  password generated on first deploy; conf auto-included via the image's % /cfg.
- Firewall opens 80/443 + SFTP/FTPS + passive range (colon form for ports.d).
- Wired into automations.sh, README, .gitignore; cloud-init for fresh VMs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:56:24 -05:00
57_Wolve cd402ba79e Merge pull request 'feat(squid): add SSL-bump caching forward-proxy deployment' (#4) from feat/squid-proxy into main
Reviewed-on: #4
2026-06-22 21:33:34 +00:00
57_Wolve e7ba68790b Done — the Squid deployment is fully built, wired in, and statically verified on the feat/squid-proxy branch. ## What landed deployments/squid/ (13 files) plus wiring into [automations.sh](automations.sh:51), [README.md](README.md), and [.gitignore](.gitignore). All six approved design decisions are implemented: - **Self-built minimal Alpine image** — apk add squid (ssl-bump compiled in), openssl/tini. [entrypoint.sh](deployments/squid/entrypoint.sh) renders squid.conf and generates the cache policy from the lists at start. - **SSL-bump** peek→splice→bump with [splice-domains.txt](deployments/squid/splice-domains.txt) passthrough. - **Wildcard hostname caching** — leading-dot [cache-domains.txt](deployments/squid/cache-domains.txt) + optional regex; CACHE_ONLY_LISTED boost/allowlist toggle. - **HTML/dynamic storage gate**, with query strings exempt on boosted domains. - **CA lifecycle** — generated on first deploy via the built image, key 0600 on the host, staged to tmpfs for the container, never embedded, git-ignored. - **Deny-by-default http_access + BIND_ADDR pinning** as the real access gate (Docker bypasses INPUT). ## One bug caught & fixed The test of the entrypoint's policy generation showed it emitting ubuntu.com with an **unescaped dot** — the s/\./\\./g double-backslash is sed-implementation-dependent and silently failed. Switched to a [.] character class (ubuntu[.]com), portable across busybox/git-bash, re-embedded, re-audited. ## Verified vs. pending **Verified locally:** bash -n on all scripts, LF endings, launcher parses, embedded archive byte-matches the loose files, and the cache-policy generation produces correct Squid directives in both modes. **Pending a Docker host** (this Windows box has none, as the plan anticipated): the container functional smoke test — build, CA mint, a bumped HTTPS fetch, a TCP_HIT, and a splice check. A few version-sensitive specifics get confirmed there: tls-cert=/tls-key= option names, the security_file_certgen path, busybox install -o/-g, ssl::server_name subdomain matching, and the squid -z flag. (All isolated and easy to adjust if any differ.) ## Commit (for you to GPG-sign) Staged and ready. Per your convention I haven't committed — here's the message: `` feat(squid): add SSL-bump caching forward-proxy deployment New deployments/squid/: an explicit forward proxy with SSL-bump TLS interception (local CA, generated on first deploy) and hostname-targeted static-content caching. Unlike the other stacks it is a forward proxy, not a Caddy/Let's-Encrypt inbound site. - Self-built minimal Alpine image (apk squid ships ssl-bump); entrypoint renders squid.conf and generates the cache policy from the domain lists. - Wildcard hostname caching (cache-domains.txt leading-dot + optional cache-domains.regex); boost vs strict-allowlist toggle (CACHE_ONLY_LISTED). - Storage gate never caches HTML or dynamic content; query strings exempt on boosted domains so versioned static assets still cache. - splice-domains.txt passthrough for pinned/banking domains. - Deny-by-default http_access (TRUSTED_CIDR) + BIND_ADDR pinning; CA key 0600 on host, never embedded, git-ignored. - Wired into automations.sh, README, .gitignore; cloud-init for fresh VMs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ` `bash git commit -S -F- <<'EOF' <paste message above> EOF ` Want me to **push feat/squid-proxy** once you've committed (you'd open the PR in Gitea), or hold? And if you can point me at a Docker host (or run docker compose build in /srv/squid` after a test deploy), I'll drive the functional smoke test and fix anything the live Squid version flags.
New deployments/squid/: an explicit forward proxy with SSL-bump TLS
interception (local CA, generated on first deploy) and hostname-targeted
static-content caching. Unlike the other stacks it is a forward proxy, not
a Caddy/Let's-Encrypt inbound site.
- Self-built minimal Alpine image (apk squid ships ssl-bump); entrypoint
  renders squid.conf and generates the cache policy from the domain lists.
- Wildcard hostname caching (cache-domains.txt leading-dot + optional
  cache-domains.regex); boost vs strict-allowlist toggle (CACHE_ONLY_LISTED).
- Storage gate never caches HTML or dynamic content; query strings exempt on
  boosted domains so versioned static assets still cache.
- splice-domains.txt passthrough for pinned/banking domains.
- Deny-by-default http_access (TRUSTED_CIDR) + BIND_ADDR pinning; CA key 0600
  on host, never embedded, git-ignored.
- Wired into automations.sh, README, .gitignore; cloud-init for fresh VMs.
2026-06-22 16:32:25 -05:00
57_Wolve 8555f7cbe4 Merge pull request 'feat(firewall): drive firewalld on Alma/RHEL with full CLI parity' (#3) from fix/alma-harden-resilience into main
Reviewed-on: #3
2026-06-14 22:42:55 +00:00
57_Wolve fe25f35305 feat(firewall): drive firewalld on Alma/RHEL with full CLI parity
A fresh Alma box has firewalld active, and the iptables-based harden-firewall.sh
refused to run there (caught by harden-ssh's '|| warn', so the host firewall was
silently skipped). Use firewalld natively on the rhel family instead of fighting it.

- harden-firewall.sh: family-aware backend. On rhel, apply/allow/deny/list/disable
  drive firewall-cmd (deny-by-default zone, SSH + registered ports, ping policy,
  source-restricted rich rules); Alpine/Debian keep the iptables engine unchanged.
  FW_BACKEND=iptables|firewalld overrides.
- oslib: install_firewalld(); sshguard_backend() prefers sshg-fw-firewalld on rhel
  so brute-force blocks land in firewalld (no INPUT->sshguard jump needed).
- Deployments already fall through to a firewall-cmd branch when the iptables
  engine is absent, so they need no changes.
- README + script header document the per-family backend.

harden-ssh / harden-jumphost are unchanged -- they call harden-firewall.sh apply
and read sshguard_backend(), so the switch happens underneath them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:16:24 -05:00
57_Wolve 4b02cac919 Merge pull request 'fix(oslib): install_openssh must not report failure on non-Alpine' (#2) from fix/alma-harden-resilience into main
Reviewed-on: #2
2026-06-14 22:00:53 +00:00
57_Wolve c3e2e9c52b fix(oslib): install_openssh must not report failure on non-Alpine
install_openssh ended with '[[ "$OS_FAMILY" == alpine ]] && pkg_install ...'.
As the function's LAST statement, that trailing test returns 1 on every
non-Alpine OS (a false '[[ ]]' exits 1), so the function reported failure even
when the packages installed fine. Harmless while the call was bare under set -e
(a short-circuited && is exempt), but the new 'install_openssh || die' guard
read it as a real failure and aborted harden-ssh on Alma right after
'Installing OpenSSH server...'.

Fix: convert the Alpine-only linux-pam step to an if-block, and add '|| return 1'
to the main install so a genuine package failure still propagates honestly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:59:39 -05:00
57_Wolve a844037cb0 Merge pull request 'fix(launcher): install git across all distros in the curl-pipe bootstrap' (#1) from fix/alma-harden-resilience into main
Reviewed-on: #1
2026-06-14 21:54:47 +00:00
57_Wolve 60433e4c8d fix(harden): keep hardening and the ntfy notifier alive when sshguard can't install
On a fresh AlmaLinux 9.8 box, install_bruteforce_protection ran unguarded under
'set -euo pipefail'. When sshguard (from EPEL) wasn't installable at that moment,
the single failed dnf aborted the ENTIRE harden run before it wrote sshd_config
or installed the pam_exec login notifier -- leaving a stock, unhardened box and a
silently-missing ntfy hook.

- oslib: install the iptables backend best-effort first, then sshguard, and
  return sshguard's status so callers can treat it as non-fatal.
- harden-ssh/harden-jumphost: install_openssh now dies with a clear message on
  failure; sshguard is '|| warn' so sshd hardening and the notifier still apply.
- install_login_notifier verifies the script + pam hook landed and logs
  'Login notifier ACTIVE' (or a loud warning) instead of failing silently.
- ntfy-ssh-login.sh: NTFY_DEBUG=1 logs delivery attempts + curl errors to
  /var/log/ssh-notify.log so the next silent failure leaves a trace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:53:39 -05:00
57_Wolve 3c02574dd0 fix(launcher): install git across all distros in the curl-pipe bootstrap
The one-liner bootstrap only auto-installed git on Alpine (apk), so piping it
onto a fresh Debian/Alma host with no git fell straight into 'git clone' and
died with 'git: command not found'. oslib's pkg_install can't help here -- the
repo isn't on disk yet. Install git inline via apk/apt-get/dnf/yum, and fail
with a clear message if it still can't.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:53:39 -05:00
57_Wolve 7e6c1ce7d1 Update globals/authorized_keys 2026-06-14 21:07:45 +00:00
57_Wolve 13535da3d8 fix(base): drop dead AUTO_UPDATE knob from base.yml
base.yml set AUTO_UPDATE=1 but never forwarded it to harden-ssh.sh (which
defaults to 0), so base hosts never actually scheduled daily updates despite
the config implying they did. Auto-update is intentionally bastion-only for
now — jumphost.yml / harden-jumphost.sh enable it by default, and base
(container) hosts will get a dedicated docker-image updater later — so remove
the knob entirely rather than wire it through. No behavior change.
Also drop cloud-init/base.yml from the README "Daily updates" line that
claimed it takes AUTO_UPDATE=1.
2026-06-12 17:23:55 -05:00
57_Wolve e23557b4fb feat(firewall): add deny-by-default host firewall (harden-firewall.sh)
Add a reusable iptables baseline that hardens hosts with ICMP + SSH
defaults and lets deployments register the ports they need. INPUT is
deny-by-default (loopback, established, ICMP, SSH on the configured port,
plus registered ports); OUTPUT stays open and FORWARD is left untouched so
Docker container networking is unaffected.
Persistence is native -- no boot hook. Rules are saved and restored by the
distro's own package (iptables/ip6tables on Alpine, iptables-persistent on
Debian, iptables-services on Alma) via the new oslib helpers
install_iptables / fw_save_cmd / fw_enable_restore. The saved ruleset
carries the INPUT->sshguard jump, so brute-force protection survives reboot
without the old sshguard-iptables hook.
A self-contained /usr/local/sbin/firewall-apply rebuilds INPUT from
declarative drop-ins under /etc/firewall/ports.d and runs the native save,
so deployments add a port without needing the repo present:
  printf '80/tcp\n443/tcp\n' > /etc/firewall/ports.d/mystack.rule
  /usr/local/sbin/firewall-apply
- SSH port read live from sshd_config (custom bastion ports just work);
  FW_SSH_SOURCE restricts the source CIDR; FW_ALLOW_PING gates echo
- harden-ssh.sh / harden-jumphost.sh install it when ENABLE_FIREWALL=1
  (default) and skip the sshguard-only hook; ENABLE_FIREWALL=0 keeps it
- cloud-init base.yml / jumphost.yml forward the toggle
- the four stack deploy.sh open_web_ports() register 80/443 via the
  firewall (ufw/firewalld kept as fallback); Docker-published ports bypass
  INPUT, so this is belt-and-braces and self-documenting
- README + cloud-init/README document the mechanism, Docker caveat, and the
  `disable` recovery path
2026-06-12 17:06:25 -05:00
57_Wolve 73cf299417 feat(headscale): allow-all default ACL with lockdown template
Ship allow-everything as the active default (first acl rule), with the
Tailscale default policy translated to headscale's acls format included but
commented for when you lock down. Pre-fix the headscale gotchas: tag:shared
owned by group:admins (not an autogroup), autogroup:self/ssh-check flagged
experimental. Rebuild embedded archive.
2026-06-12 16:32:41 -05:00
57_Wolve 6a3fc68b75 fix(headscale): valid default ACL + document /admin gating
headscale rejects autogroups as tagOwners (only user/group:/tag:), which made
the shipped policy fatal. Replace with a valid allow-all default plus correct
commented examples for tightening. Document gating /admin to a pocket-id
superuser group via the headplane client's Allowed User Groups. Rebuild archive.
2026-06-12 16:17:26 -05:00
57_Wolve 1ca79938cd feat(headscale): add headplane web UI at /admin
Integrate headplane (ghcr.io/tale/headplane) into the headscale stack, served
by Caddy at /admin. API-only (no Docker socket); deploy.sh mints a headscale
API key on first run, generates headplane.yaml, and wires optional OIDC login
via pocket-id (second client, /admin/oidc/callback). Adds HEADPLANE_* env,
compose service, Caddy routing; rebuild embedded archive.
2026-06-12 16:15:34 -05:00
57_Wolve 573785f2cc feat(headscale): add file-based ACL policy
Ship policy.hujson (mounted + installed on first deploy, edits preserved) and
wire policy.mode=file / policy.path in config.yaml. Translate the Tailscale
"grants" default into headscale's legacy "acls" format (self-access, tag:shared,
Tailscale SSH), since headscale 0.28 doesn't support grants. Embed in deploy.sh
and document `headscale policy check`.
2026-06-12 16:04:24 -05:00
57_Wolve 25f20037e9 feat(headscale): pin OIDC PKCE to S256 explicitly
Make pkce.method: S256 explicit in config.yaml (alongside pkce.enabled: true)
and note the pocket-id client must have PKCE enabled too. Rebuild embedded
archive.
2026-06-12 15:55:06 -05:00
57_Wolve 74b34550bc feat(headscale): install host headscale CLI wrapper
Add /usr/local/bin/headscale (generated by deploy.sh) that runs the headscale
CLI inside the container with the compose file path baked in, so `headscale
users list` etc. work from any directory instead of erroring with
"no configuration file provided". Update the post-deploy hints accordingly.
2026-06-12 15:52:35 -05:00
57_Wolve ecad11c416 feat(headscale): print OIDC callback URL after deploy
Surface https://${HEADSCALE_DOMAIN}/oidc/callback in the final DEPLOYED
summary so it's easy to register in the pocket-id OIDC client.
2026-06-12 15:51:11 -05:00
57_Wolve c5f5be6f3f docs: add "About this repo" intro to README
Explain the project's origin — years of personal infra notes and scripts,
polished with Claude into a consistent, multi-distro toolkit — and note that
more is still being added from the collection. Framed as a starting point for
friends to use and extend. PRs and ideas welcome.
2026-06-12 15:29:22 -05:00
57_Wolve 8fbeb8f6b0 feat: unified launcher, multi-OS hardening, login alerts & auto-updates
Restructure around a single entry point (automations.sh) with a Gum wizard and
a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the
provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on
Alpine/Debian/Alma; seed root keys from globals/.
- ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec
- daily auto-updates: AUTO_REBOOT=idle reboots only when no SSH active; opt-in
  Alpine stable-branch upgrades (ALLOW_RELEASE_UPGRADE)
- cloud-init: generic base/jumphost + per-deployment, which harden SSH by
  default on fresh VMs
- pocket-id: optional WebFinger block (BASE_DOMAIN), tag v2.8.0
- headscale: fix oidc.expiry schema for 0.28 so the container starts
- Gitea release workflow on tag (TOKEN_GITEA); repo URLs -> Gitea
- README/LICENSE/.gitignore/.gitattributes (force LF)
2026-06-12 15:24:30 -05:00
57_Wolve 7faa9098de feat: unified launcher, multi-OS hardening, login alerts & auto-updates
Restructure around a single entry point (automations.sh) with a Gum wizard and
a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the
provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on
Alpine/Debian/Alma; seed root keys from globals/.

- ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec
- daily auto-updates with AUTO_REBOOT=idle (reboots only when no SSH active) and
  opt-in Alpine stable-branch upgrades
- generic + per-deployment cloud-init; Gitea release workflow on tag
- README/LICENSE/.gitignore/.gitattributes (force LF); repo URLs -> Gitea
2026-06-12 14:56:02 -05:00
57_Wolve 85eeb79971 Update scripts/harden-ssh.sh 2026-06-12 18:16:27 +00:00
57_Wolve 991758de4b Update deployments/pocket-id/deploy.sh 2026-05-04 17:30:22 +00:00
57_Wolve d0977bcbe8 Upload files to "deployments/pocket-id" 2026-05-04 17:15:05 +00:00
57_Wolve aef47c835a Upload files to "scripts" 2026-05-04 17:14:17 +00:00