No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-16 19:26:18 +02:00
clients Add operator-visible install flow 2026-07-19 18:40:31 +02:00
docs Document SSH key exchange and NOPASSWD risk 2026-08-16 19:26:18 +02:00
examples Document SSH key exchange and NOPASSWD risk 2026-08-16 19:26:18 +02:00
install Add optional Forgejo publication host setup 2026-08-16 18:43:46 +02:00
lib Add optional Forgejo publication host setup 2026-08-16 18:43:46 +02:00
templates Initial base-relay MVP 2026-06-24 12:22:54 +00:00
.gitignore Initial base-relay MVP 2026-06-24 12:22:54 +00:00
install.sh Add optional Forgejo publication host setup 2026-08-16 18:43:46 +02:00
README.md Document SSH key exchange and NOPASSWD risk 2026-08-16 19:26:18 +02:00

base-relay

What is this?

A small, conservative base installer for a minimal public relay/utility VPS: OpenSSH, time sync, firewall, fail2ban, and an optional WireGuard server. Plain shell, plain systemd, no Docker.

The relay is also the natural place for a small Forgejo instance when a project needs Git-based coordination between a laptop, a public VPS, local developer machines, AI/Hydra workers, and outside collaborators. In that role, Forgejo is not treated as a general application platform. It is the basic communication layer for repositories, issues, pull/merge review, releases, and readable project state.

Built and refined against a real installation (hecate.intr.rpdo.net, IONOS VPS S+, Ubuntu 24.04) — the lessons from that run are baked into the modules and into "Known provider pitfalls" below.

What it does not do

  • No Docker requirement.
  • No root-login provisioning.
  • No automatic secret/password handling.
  • No automatic passwordless-sudo provisioning.
  • No aggressive SSH hardening without a working lockout check.
  • No silent changes that risk your SSH access.
  • Not a general app platform. Caddy and Forgejo are available as explicit opt-in modules for small publication hosts; the minimal profile does not enable them.
  • Not a CI farm, chat server, project-management suite, or replacement for a development workstation.

What it does

  • OpenSSH service check/enablement
  • apt package installation on Debian/Ubuntu-style systems
  • chrony time sync with optional trusted NTP relay networks
  • UFW firewall setup that allows SSH before activation
  • optional HTTP/HTTPS firewall openings for later proxy use
  • optional WireGuard server setup
  • optional Caddy reverse proxy with automatic ACME/TLS
  • optional public Forgejo Git service as a native systemd service for source, issue/review, release, and collaborator coordination
  • fail2ban jail for sshd
  • /etc/base-relay/base-relay.conf for repeatable runs
  • installation summary under /var/log/base-relay/
  • reboot-required visibility after install runs
  • --dry-run, --config, --yes, --commands-only, --explain --more
  • client-side tmux operator session helper with terminal logging

Quickstart

./install.sh --explain --more
./install.sh --commands-only
./install.sh --dry-run
./install.sh

Repeatable, config-driven run:

cp examples/base-relay.conf.example base-relay.conf
vi base-relay.conf
./install.sh --config ./base-relay.conf --dry-run
./install.sh --config ./base-relay.conf --yes

Run as a normal sudo-capable user, never as root. The installer calls sudo -v once, then sudo only for individual privileged steps. It never reads, stores, or pipes your sudo password.

Operator-visible sessions

When the operator wants to watch and keep a plain-text record of an install, start a tmux session from the checkout:

./clients/operator-tmux.sh --command './install.sh --dry-run'
./clients/operator-tmux.sh --command './install.sh'

The left pane is the operator command pane. The right pane tails the log under ~/.local/state/base-relay/operator-logs/. If sudo asks for permission, the operator types into the tmux pane directly. base-relay still never handles the sudo password itself.

For operators who want to do the work manually, --commands-only prints the commands the installer would run without changing the system:

./install.sh --commands-only

Profiles / options

Only one profile exists today: minimal (OpenSSH, chrony, ufw, fail2ban).

Everything else is an opt-in toggle in the config file (see examples/base-relay.conf.example):

Variable Default Effect
BASE_RELAY_HARDEN_OPENSSH no Disable root login / password auth (lockout-checked)
BASE_RELAY_NTP_ALLOW empty Networks allowed to use this host as an NTP source
BASE_RELAY_FIREWALL_ALLOW_WEB no Open 80/tcp + 443/tcp ahead of a future proxy
BASE_RELAY_ENABLE_WIREGUARD no Stand up a wg0 server interface
BASE_RELAY_PROXY none caddy enables Caddy reverse-proxy/TLS support
BASE_RELAY_CADDY_ACME_EMAIL empty Optional ACME account email for Caddy
BASE_RELAY_ENABLE_FORGEJO no Install Forgejo + PostgreSQL + Caddy site
BASE_RELAY_FORGEJO_DOMAIN empty Required public hostname when Forgejo is enabled
BASE_RELAY_FORGEJO_SHA256 empty Required for real Forgejo binary downloads unless explicitly waived
BASE_RELAY_FORGEJO_MAILER_ENABLED no Enable Forgejo SMTP via explicit mailer config

Useful CLI flags: --dry-run, --yes, --config FILE, --profile NAME, --commands-only, --explain, --more, --confirm-ssh-lockout-risk, --no-sudo-keepalive. See ./install.sh --help.

Security principles

  • Respect the admin — no surprises, no silent config rewrites.
  • Run as a normal sudo-capable user, never as root.
  • Never handle passwords directly.
  • Treat SSH keys as login authority: exchange and verify them deliberately, keep one key per device/operator, and remove stale keys.
  • Treat passwordless sudo (NOPASSWD) as dangerous. It may be useful for a temporary automation/operator window, but a stolen SSH key or compromised client can then become remote root without a second password prompt.
  • Dry-run and config-file paths are first-class, not afterthoughts.
  • Explain and commands-only paths are first-class, not afterthoughts.
  • Operator-visible tmux/logging paths are first-class, not afterthoughts.
  • Risky changes (SSH hardening) require an explicit, checked opt-in.
  • KISS over cleverness.

Why Forgejo belongs here

A relay host exists to connect otherwise separate work locations safely. In a small development setup those locations are often:

  • an operator laptop,
  • a public VPS,
  • one or more local developer machines,
  • a local AI/Hydra development head,
  • worker machines that need read access to project state,
  • outside developers who should not receive direct shell access.

Forgejo provides the shared Git and project-state surface between them. It lets the relay publish selected repositories, accept issues/reviews, expose releases, and grant read/write access without turning SSH accounts, ad-hoc file copies, or private local checkouts into the collaboration mechanism.

This is why Forgejo is an opt-in base-relay module: for a relay that coordinates developers, it is part of the basic communication substrate. For a host that only needs SSH, firewall, time sync, fail2ban, and WireGuard, it remains off.

Modules

Run in this order by install.sh:

Module Does
00-detect.sh Logs detected user/host/OS/package backend
05-hostname.sh Applies BASE_RELAY_HOSTNAME via hostnamectl if it differs
10-packages.sh Installs the apt packages needed by enabled modules
20-openssh.sh Enables sshd, detects its effective listening port(s)
25-openssh-harden.sh Opt-in, lockout-checked SSH hardening drop-in
30-chrony.sh chrony time sync, optional NTP allow for trusted networks
40-firewall.sh ufw setup; SSH port detected, not assumed; WireGuard opt-in
45-wireguard.sh Opt-in wg0 server interface, key generated only once
50-fail2ban.sh fail2ban jail for sshd
55-caddy.sh Opt-in Caddy base config and service
56-forgejo.sh Opt-in Forgejo/PostgreSQL/systemd setup
57-forgejo-caddy.sh Opt-in public Caddy site for Forgejo
60-reboot-status.sh Reports reboot-required marker and packages when present
99-summary.sh Writes a summary + next manual checks to /var/log/base-relay/

Checking operation

Quick look:

systemctl status ssh chrony ufw fail2ban
sudo ufw status verbose
chronyc tracking

Full runbooks (reboot check, SSH/hardening check, WireGuard troubleshooting, Forgejo-over-tunnel check, locale, ...) are in docs/operations.md.

Known provider pitfalls

  • ufw vs. provider firewall are two separate layers. A cloud provider's network/security-group firewall (e.g. an IONOS Cloud Panel policy) can silently drop traffic that ufw already allows. Both layers must permit a port for it to be reachable — this is exactly what blocked WireGuard on the first hecate run (ufw was correct; the provider firewall was not).
  • Hostname vs. FQDN. hostnamectl set-hostname only sets the short name; hostname -f resolving correctly may still need an /etc/hosts entry or working DNS.
  • Cloud-init SSH drop-ins can silently win. sshd_config.d/*.conf loads in filename order with first-value-wins per keyword; a provider's own drop-in (often sorted before ours) can leave a hardening directive ineffective even though sshd -t passed. 25-openssh-harden.sh re-checks the effective config after reload and warns if this happens — see docs/operations.md.
  • Locale warnings on a fresh VPS (setlocale: LC_ALL: cannot change locale) are cosmetic, not a functional problem — see docs/operations.md.
  • A live server-side tunnel does not prove a client peer is reachable. On macOS, prefer the official WireGuard.app for human-operated Macs (macos-app-human). Use CLI/launchd/watchdog tooling (macos-cli-worker) only where automation is truly required, and never run competing tunnel managers for the same interface/profile. A hybrid setup may observe health but must not fight WireGuard.app unless explicitly configured. See docs/client-macos-wireguard.md and examples/macos/.

Roadmap

  • ntfy
  • backup hooks