Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Locks the Swarm ports (2377 control, 7946 gossip, 4789 overlay VXLAN) to the
|
|
|
|
|
# WireGuard interface only — they must never be reachable from the public
|
2026-07-26 06:56:38 +00:00
|
|
|
# internet. Run on ALL THREE nodes (vps2, vps1, desktop) after joining the
|
|
|
|
|
# swarm. Existing app-facing rules (80/443, SSH, etc.) are untouched.
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
WG_IFACE="${WG_IFACE:-wg0}"
|
|
|
|
|
|
|
|
|
|
if ! command -v ufw >/dev/null 2>&1; then
|
|
|
|
|
echo "ufw not found — apply the equivalent iptables/nftables rules by hand:" >&2
|
|
|
|
|
echo " allow 2377/tcp, 7946/tcp, 7946/udp, 4789/udp only on interface ${WG_IFACE}" >&2
|
|
|
|
|
echo " deny 2377/tcp, 7946/tcp, 7946/udp, 4789/udp on every other interface" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "==> Allowing Swarm ports on ${WG_IFACE} only"
|
|
|
|
|
ufw allow in on "$WG_IFACE" to any port 2377 proto tcp
|
|
|
|
|
ufw allow in on "$WG_IFACE" to any port 7946 proto tcp
|
|
|
|
|
ufw allow in on "$WG_IFACE" to any port 7946 proto udp
|
|
|
|
|
ufw allow in on "$WG_IFACE" to any port 4789 proto udp
|
|
|
|
|
|
|
|
|
|
echo "==> Explicitly denying the same ports on every other interface"
|
|
|
|
|
ufw deny 2377/tcp
|
|
|
|
|
ufw deny 7946/tcp
|
|
|
|
|
ufw deny 7946/udp
|
|
|
|
|
ufw deny 4789/udp
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "Rules added (not yet necessarily active — check ufw status):"
|
|
|
|
|
ufw status numbered | grep -E "2377|7946|4789|Status" || true
|
|
|
|
|
echo
|
2026-07-21 03:57:20 +00:00
|
|
|
if ! ufw status | grep -q "^Status: active"; then
|
|
|
|
|
echo "ufw is currently INACTIVE on this node — 'ufw enable' switches its"
|
|
|
|
|
echo "default policy to deny-incoming for EVERYTHING, not just the Swarm"
|
|
|
|
|
echo "ports above. Before enabling, explicitly allow every port this node"
|
2026-07-26 06:56:38 +00:00
|
|
|
echo "already serves publicly (SSH at minimum; on vps2 specifically, also"
|
|
|
|
|
echo "80/tcp and 443/tcp for the live thermograph.org/beta.thermograph.org"
|
registry: move the registry host to dev.jinemi.com, MCP to mcp.jinemi.com
Completes the Forgejo domain migration. ROOT_URL moved to dev.jinemi.com
earlier; the registry half was deliberately deferred. Every image name,
REGISTRY_HOST default, runner label and --add-host pin now names
dev.jinemi.com, so the registry host and the bearer-token realm agree again.
Both names address the same Forgejo, so no image needs re-pushing and a
rollback to a tag pushed under the old prefix still resolves.
git.thermograph.org therefore stays served off the same Caddy site block --
one block, so the /v2/* mesh-only matcher keeps covering both names -- for
pre-migration tags and for runners holding it as their registered instance
URL.
Mesh clients now pin both names in /etc/hosts: the new one as registry host
and token realm, the old one for pre-migration tags. runner-vps2/config.yaml
carries both --add-host entries for the same reason.
Also renames Centralis' endpoint to mcp.jinemi.com in the two places this
repo names it; Centralis itself is provisioned outside this repo.
Host-side steps this cannot do (documented in deploy/forgejo/README.md,
"Host-side steps"): the Forgejo Actions variable REGISTRY_HOST, docker login
against the new host, and the /etc/hosts pins.
2026-08-01 23:06:22 +00:00
|
|
|
echo "Caddy; on vps1, 80/tcp and 443/tcp for dev.jinemi.com,"
|
2026-07-26 06:56:38 +00:00
|
|
|
echo "dashboard.thermograph.org and emigriffith.dev) — check 'ss -tlnp' for"
|
|
|
|
|
echo "what's actually listening first. Enabling ufw without doing this WILL"
|
|
|
|
|
echo "drop live traffic the moment it activates."
|
2026-07-21 03:57:20 +00:00
|
|
|
fi
|