- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| clients | ||
| docs | ||
| examples | ||
| install | ||
| lib | ||
| templates | ||
| .gitignore | ||
| install.sh | ||
| README.md | ||
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.conffor 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
ufwalready allows. Both layers must permit a port for it to be reachable — this is exactly what blocked WireGuard on the first hecate run (ufwwas correct; the provider firewall was not). - Hostname vs. FQDN.
hostnamectl set-hostnameonly sets the short name;hostname -fresolving correctly may still need an/etc/hostsentry or working DNS. - Cloud-init SSH drop-ins can silently win.
sshd_config.d/*.confloads 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 thoughsshd -tpassed.25-openssh-harden.shre-checks the effective config after reload and warns if this happens — seedocs/operations.md. - Locale warnings on a fresh VPS (
setlocale: LC_ALL: cannot change locale) are cosmetic, not a functional problem — seedocs/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. Seedocs/client-macos-wireguard.mdandexamples/macos/.
Roadmap
- ntfy
- backup hooks