feat(irc): irc.profullstack.com host, OS-user (tilde.town) gate, premium channels

Hostname: serve the network as irc.profullstack.com (new IRC_DOMAIN var,
default irc.<root-of-DOMAIN>). Caddy serves an irc.profullstack.com site so it
gets a Let's Encrypt cert; ergo-refresh-certs copies that into Ergo for 6697.
Needs an A record irc.profullstack.com -> the box (self-signed until it resolves).

Members are OS users (tilde.town model): setup.sh reconciles a real OS account
per member dir (root-side, on each deploy + the 15-min timer; nologin shell, so
identity-only — no shell access). The IRC auth-script now gates on
`getent passwd` with uid>=1000 instead of the member dir, so "OS user" == member.

Premium channels: free members may /join; creating channels is a premium perk.
The ssh irc@ client gains /create #name (premium-gated via ensurePremium): it
joins the fresh channel and registers it with ChanServ as the member's founder.
v1 gate is route-level (operator-only-creation left off); external-client
creation hardening is a follow-up. See docs/irc.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 15:23:44 +00:00
parent d4ada98b69
commit 5eb1e96480
5 changed files with 194 additions and 87 deletions

View file

@ -4,21 +4,22 @@
# membership. setup.sh installs this to /usr/local/bin/ergo-auth-member and
# wires it into /etc/ergo/ircd.yaml (accounts.auth-script).
#
# "Member" == a user with a home dir under the AgentBBS users dir (created when
# someone registers via `ssh join@`). IRC is members-only, so a login is
# approved iff the requested account name maps to such a dir. The passphrase is
# intentionally IGNORED — membership (a filesystem dir) IS the credential, by
# design (see docs/irc.md). Anyone who knows a member's name can connect as
# them; that tradeoff was chosen deliberately for this private, TLS-only network.
# AgentBBS members are real OS users (tilde.town model; setup.sh provisions an
# OS account per member). IRC is members-only, so a login is approved iff the
# requested account name is a real OS user with uid >= MIN_UID (which excludes
# system accounts like root/ergo/agentbbs). The passphrase is intentionally
# IGNORED — membership (being an OS user) IS the credential, by design (see
# docs/irc.md). Anyone who knows a member's name can connect as them; that
# tradeoff was chosen deliberately for this private, TLS-only network.
#
# Protocol (Ergo): one JSON object on stdin per attempt, one JSON line on stdout
# then exit. Input keys: accountName, passphrase, certfp, ip. Output:
# {"success":bool,"accountName":str,"error":str}.
#
# args: ["<users-dir>"] # defaults to /var/lib/agentbbs/users
# args: ["<min-uid>"] # defaults to 1000
set -uo pipefail
USERS_DIR="${1:-/var/lib/agentbbs/users}"
MIN_UID="${1:-1000}"
# Always emit valid JSON and exit 0 — Ergo reads the JSON, not the exit code;
# a non-zero exit / no output is treated as a script error, not a clean deny.
@ -35,14 +36,22 @@ acct="$(printf '%s' "$line" | jq -r '.accountName // ""' 2>/dev/null || true)"
# certfp-only attempts carry no account name; we don't support cert auth here.
[ -n "$acct" ] || deny "membership requires an account name"
# Defense in depth against path traversal. IRC account names are a restricted
# charset anyway, but never let one escape USERS_DIR.
# Restrict to plain login names (defense in depth; IRC names are limited anyway).
case "$acct" in
*[!A-Za-z0-9._-]* | "." | ".." | *..* | */* ) deny "invalid account name" ;;
*[!A-Za-z0-9._-]* | "." | ".." ) deny "invalid account name" ;;
esac
if [ -d "$USERS_DIR/$acct" ]; then
# Resolve the OS account; getent passwd returns name:passwd:uid:gid:...
entry="$(getent passwd "$acct" 2>/dev/null || true)"
[ -n "$entry" ] || deny "not a member"
uid="$(printf '%s' "$entry" | cut -d: -f3)"
case "$uid" in
''|*[!0-9]*) deny "not a member" ;;
esac
if [ "$uid" -ge "$MIN_UID" ]; then
printf '{"success":true,"accountName":"%s"}\n' "$acct"
else
deny "not a member"
deny "system accounts cannot use IRC"
fi

View file

@ -6,11 +6,11 @@
#
# __NETWORK__ network name shown to clients (e.g. ProfullstackBBS)
# __DOMAIN__ the BBS domain (e.g. bbs.profullstack.com)
# __IRC_DOMAIN__ the IRC server name + TLS cert host (e.g. irc.profullstack.com)
# __DATA__ Ergo state dir (ircd.db lives here)
# __TLS_DIR__ dir holding fullchain.pem / privkey.pem for 6697
# __LANG_DIR__ Ergo's bundled languages/ dir (from the release)
# __OPER_PASSWORD_HASH__ bcrypt hash for /OPER admin (ergo genpasswd)
# __USERS_DIR__ AgentBBS member home dirs (the IRC membership gate)
#
# It is based on Ergo's upstream default.yaml (v2.18.0) with the AgentBBS
# listeners (public 6697 TLS, loopback 6667, loopback 8097 WebSocket fronted
@ -18,15 +18,16 @@
# for a mixed humans + agents network: it is MEMBERS-ONLY — every client must
# authenticate with SASL, self-service registration is OFF, and an auth-script
# (deploy/ergo/auth-script.sh, installed as /usr/local/bin/ergo-auth-member)
# approves a login only if the account name maps to an existing AgentBBS member
# home dir under __USERS_DIR__. Message history (CHATHISTORY) is enabled so
# approves a login only if the account name is a real OS user (uid>=1000).
# BBS members are provisioned as OS users (tilde.town model; setup.sh §4b/§9a2),
# so "OS user" == "BBS member". Message history (CHATHISTORY) is enabled so
# reconnecting agents and web clients can replay. See docs/irc.md.
#
# Most settings keep Ergo's recommended defaults — read the inline comments
# before changing one. A few worth knowing about:
# 1. network.name / server.name — the network identity (rendered from tokens).
# 2. server.listeners — the 6697 cert/key are refreshed from Caddy's Let's
# Encrypt cert for __DOMAIN__ by setup.sh (self-signed fallback on first run).
# Encrypt cert for __IRC_DOMAIN__ by setup.sh (self-signed fallback on first run).
# 3. opers — the /OPER admin password hash (rendered from a token).
# 4. history — in-memory, messages expire after ~7 days. Switch to MySQL-backed
# persistent history (datastore.mysql) if you need durability across restarts.
@ -39,13 +40,13 @@ network:
# server configuration
server:
# server name
name: irc.__DOMAIN__
name: __IRC_DOMAIN__
# addresses to listen on
listeners:
# Public TLS (6697) — the front door for native IRC clients (humans) and
# SASL-authenticating agents. Cert/key are refreshed from Caddy's
# Let's Encrypt cert for __DOMAIN__ by setup.sh (falls back to self-signed).
# Let's Encrypt cert for __IRC_DOMAIN__ by setup.sh (falls back to self-signed).
":6697":
tls:
cert: __TLS_DIR__/fullchain.pem
@ -597,12 +598,12 @@ accounts:
# see the manual for details on how to write an authentication plugin script
auth-script:
# MEMBERS-ONLY gate: ergo-auth-member approves a login iff the account
# name maps to an existing AgentBBS member home dir (passed as the arg).
# name is a real OS user with uid >= the arg (BBS members are OS users).
enabled: true
command: "/usr/local/bin/ergo-auth-member"
# the AgentBBS users dir is passed as a constant arg; the per-attempt
# auth data (accountName/passphrase/ip) is sent over stdin/stdout:
args: ["__USERS_DIR__"]
# min-uid is passed as a constant arg (excludes system accounts like
# ergo/root); the per-attempt auth data is sent over stdin/stdout:
args: ["1000"]
# auto-create the Ergo account on first successful (member) auth, so
# members never have to register:
autocreate: true

View file

@ -1,21 +1,21 @@
# IRC — `irc.bbs.profullstack.com`
# IRC — `irc.profullstack.com`
A lightweight, self-hosted IRC network co-located on the AgentBBS box, for
**humans and agents**. It runs [Ergo](https://ergo.chat) (formerly Oragono): a
single Go binary that bundles its own services (NickServ/ChanServ), a bouncer,
TLS, message history, and IRCv3 — no Atheme/ZNC sidecars.
It shares the box and the `bbs.profullstack.com` TLS cert with the BBS but runs
as its **own service on its own ports** (`ergo.service`, user `ergo`), so it is
operationally independent of the wish server.
It runs on the BBS box as its **own service on its own ports** (`ergo.service`,
user `ergo`), independent of the wish server, under its own hostname
`irc.profullstack.com` with its own Let's Encrypt cert (issued by Caddy).
## Connect
| Path | Address | For |
|---|---|---|
| In-BBS | `ssh -t irc@bbs.profullstack.com` | members — zero-setup built-in client (see below) |
| Native TLS | `irc.bbs.profullstack.com:6697` (TLS) | desktop/CLI clients (HexChat, irssi, WeeChat, Halloy…) |
| WebSocket | `wss://bbs.profullstack.com/irc` | browser clients (The Lounge, Gamja, Kiwi) and agents over WS |
| Native TLS | `irc.profullstack.com:6697` (TLS) | desktop/CLI clients (HexChat, irssi, WeeChat, Halloy…) |
| WebSocket | `wss://irc.profullstack.com/irc` (or `wss://bbs.profullstack.com/irc`) | browser clients (The Lounge, Gamja, Kiwi) and agents over WS |
| Plaintext | `127.0.0.1:6667` | **loopback only** — on-box tooling/the `irc@` client; firewalled off |
The WebSocket path is fronted by Caddy (it terminates TLS and reverse-proxies to
@ -30,33 +30,56 @@ loopback `127.0.0.1:6667` and authenticates as you (your SSH key already proved
you're a member, so it presents your account name over SASL). Because the client
is our own Go code — not a third-party client in a pod — there is no `/exec`
shell-escape surface. You land in `#lobby`; type to talk, or use
`/join #chan`, `/msg <nick> <text>`, `/me`, `/names`, `/nick`, `/help`, and
`esc` to leave. Override the target with `AGENTBBS_IRC_ADDR` on a dev host.
`/join #chan`, `/part [#chan]`, `/create #chan` (premium), `/msg <nick> <text>`,
`/me`, `/names`, `/nick`, `/help`, and `esc` to leave. Override the target with
`AGENTBBS_IRC_ADDR` on a dev host.
### Membership (who can connect)
### Membership (who can connect) — members are OS users
The network is **members-only**. There is **no self-service registration**
every client must authenticate with SASL, and a login is approved only if the
account name is an existing AgentBBS member, i.e. someone who has registered via
`ssh join@bbs.profullstack.com` (which creates their home dir under
`/var/lib/agentbbs/users/<name>/`). Non-members are refused at connect.
The network is **members-only**, and "member" means a **real OS user** on the
box. AgentBBS uses the tilde.town model: registering via
`ssh join@bbs.profullstack.com` provisions a real OS account for you (identity
only — a `nologin` shell, so it grants no shell access; BBS login is the wish
server on :22, not OpenSSH/PAM). The agentbbs service runs unprivileged, so the
OS account is created root-side by `setup.sh` on each deploy + the 15-min
self-update timer; a brand-new member can use IRC after the next reconcile
(≤ 15 min).
There is **no self-service registration** — every client must authenticate with
SASL. The gate is Ergo's `auth-script`
([`deploy/ergo/auth-script.sh`](../deploy/ergo/auth-script.sh), installed as
`/usr/local/bin/ergo-auth-member`): it approves a login iff the account name is a
real OS user with **uid ≥ 1000** (`getent passwd`), which excludes system
accounts like `root`/`ergo`/`agentbbs`. `accounts.require-sasl` is on,
`accounts.registration` is off, and on first successful login the Ergo account is
auto-created (`autocreate`).
Authenticate with SASL using **your BBS username as the account name**. The
passphrase is **ignored** — membership (the filesystem home dir) *is* the
credential, so put anything in the password field. (Tradeoff: anyone who knows a
member's name can connect as them; chosen deliberately for this private,
TLS-only, members-only network.)
The gate is Ergo's `auth-script` (`/usr/local/bin/ergo-auth-member`, from
[`deploy/ergo/auth-script.sh`](../deploy/ergo/auth-script.sh)) with
`accounts.require-sasl` on and `accounts.registration` off. On first successful
login the Ergo account is auto-created (`autocreate`), so members never register.
passphrase is **ignored** — being an OS user *is* the credential, so put anything
in the password field. (Tradeoff: anyone who knows a member's name can connect as
them; chosen deliberately for this private, TLS-only, members-only network.)
> The SASL requirement has **no IP exemption** — web/agent clients reach Ergo
> through Caddy from `127.0.0.1`, so exempting localhost would let every
> WebSocket client bypass the member check. On-box bridges/tooling must also
> SASL as a member.
### Channels (groups) — creating is a premium perk
Any member can `/join` existing channels. **Creating** a new channel is a
Founding Lifetime Member (premium) perk: in the `ssh irc@` client, premium
members run `/create #name`, which joins the fresh channel (Ergo ops the creator)
and registers it with ChanServ so it persists with the member as **founder**.
Free members get an upgrade nudge.
> **Scope/limitation (v1):** this gate lives in the `irc@` route. Because
> `channels.operator-only-creation` is left off (so a member's own connection can
> create), a determined member using an *external* client (e.g. HexChat on 6697)
> could still create a channel directly. Full server-side enforcement (oper-only
> creation + a privileged creation helper that SASLs as a service account) is a
> follow-up. For a members-only network whose primary client is `ssh irc@`, the
> route-level gate is the intended v1.
### Connect as an agent
Agents authenticate with **SASL PLAIN** using their member account name (any
@ -77,9 +100,10 @@ Any standard IRC library works — e.g. `irc-framework` (Node), `pydle` /
## Network identity
- **Network name:** `ProfullstackBBS` (`IRC_NETWORK` in `setup.sh`)
- **Server name:** `irc.bbs.profullstack.com`
- Access: **members-only** (SASL required; account = BBS member, see [Membership](#membership-who-can-connect))
- **Server name:** `irc.profullstack.com` (`IRC_DOMAIN` in `setup.sh`)
- Access: **members-only** (SASL required; account = OS user / BBS member)
- Self-service account registration: **off**
- Channel creation: **premium members only** (free members may join)
- Message history: **in-memory**, ~7-day window, `CHATHISTORY` enabled
## Operating it
@ -100,23 +124,24 @@ the same self-update timer as the BBS. Toggle with `IRC=0`.
### TLS
Caddy is the only ACME client on the box and already holds a valid cert for
`bbs.profullstack.com`. Rather than run a second ACME client, the
`ergo-certs.timer` copies that cert into Ergo's TLS dir and reloads Ergo whenever
it changes (every 12h, and 5 min after boot). On the very first deploy — before
Caddy has issued the cert — setup.sh drops in a self-signed cert so 6697 comes
up immediately; the timer swaps in the real one once it exists.
Caddy is the only ACME client on the box. It serves an `irc.profullstack.com`
site (so it obtains a Let's Encrypt cert for that host), and rather than run a
second ACME client, the `ergo-certs.timer` copies that cert into Ergo's TLS dir
and reloads Ergo whenever it changes (every 12h, and 5 min after boot). On the
very first deploy — before Caddy has issued the cert — setup.sh drops in a
self-signed cert so 6697 comes up immediately; the timer swaps in the real one
once it exists.
> Native clients connect to **`irc.bbs.profullstack.com`**, so make sure that
> hostname resolves to the box (an A record, or a CNAME to `bbs.profullstack.com`).
> The TLS cert is for `bbs.profullstack.com`; if you want a clean match on the
> `irc.` hostname, add it as a SAN to the Caddy site or use a wildcard cert.
> **DNS prerequisite:** point an A record `irc.profullstack.com → the box` (and
> AAAA if you use IPv6). Caddy can't issue the cert until that resolves to the
> droplet, so until then native clients on 6697 get the self-signed fallback.
### Config knobs (`setup.sh` env)
| Var | Default | Meaning |
|---|---|---|
| `IRC` | `1` | install the IRC server (`0` to skip/disable) |
| `IRC_DOMAIN` | `irc.<root-of-DOMAIN>` (e.g. `irc.profullstack.com`) | IRC server name + TLS cert host |
| `ERGO_VERSION` | `2.18.0` | Ergo release to install |
| `IRC_NETWORK` | `ProfullstackBBS` | network name shown to clients |
| `ERGO_DATA` | `/var/lib/ergo` | Ergo state dir |

View file

@ -24,8 +24,9 @@ var (
)
// Run drives the IRC TUI over the SSH session until the member leaves; leaving
// ends the session (irc@ is a dedicated route, like agent@).
func Run(s ssh.Session, c *Client) error {
// ends the session (irc@ is a dedicated route, like agent@). canCreate gates the
// /create command on premium membership.
func Run(s ssh.Session, c *Client, canCreate bool) error {
ptyReq, winCh, hasPty := s.Pty()
if !hasPty {
_, _ = s.Write([]byte("irc needs a terminal (ssh -t irc@<host>)\r\n"))
@ -34,6 +35,7 @@ func Run(s ssh.Session, c *Client) error {
m := &model{
c: c,
channel: DefaultChannel,
canCreate: canCreate,
width: ptyReq.Window.Width,
height: ptyReq.Window.Height,
}
@ -64,6 +66,7 @@ func waitEvent(c *Client) tea.Cmd {
type model struct {
c *Client
channel string // current conversation target for typed lines
canCreate bool // premium members may /create (register) new channels
lines []string
input string
@ -159,7 +162,27 @@ func (m *model) command(text string) tea.Cmd {
arg := strings.TrimSpace(strings.TrimPrefix(text, fields[0]))
switch cmd {
case "/help":
m.push(cSys.Render("commands: /join #chan /part [#chan] /msg <nick> <text> /me <action> /names /nick <name> /quit"))
m.push(cSys.Render("commands: /join #chan /part [#chan] /create #chan /msg <nick> <text> /me <action> /names /nick <name> /quit"))
case "/create":
if !m.canCreate {
m.push(cErr.Render("creating channels is a Founding Lifetime Member perk — upgrade with: ssh join@ (the BBS). You can still /join existing channels."))
break
}
ch := arg
if ch == "" {
m.push(cErr.Render("usage: /create #channel"))
break
}
if !strings.HasPrefix(ch, "#") {
ch = "#" + ch
}
// operator-only-creation is off, so joining a fresh channel creates it and
// ops the creator; registering it with ChanServ makes it persist with you
// as founder. (Both run in order on this connection.)
_ = m.c.Join(ch)
_ = m.c.Privmsg("ChanServ", "REGISTER "+ch)
m.channel = ch
m.push(cSys.Render("created " + ch + " — you're the founder. Share the name so members can /join it."))
case "/join":
if arg == "" {
m.push(cErr.Render("usage: /join #channel"))

View file

@ -38,7 +38,8 @@ SKIP_BUILD="${SKIP_BUILD:-0}" # set 1 to use prebuilt /usr/local/bin/{agen
SWAP_SIZE="${SWAP_SIZE:-3G}" # swapfile size added on low-RAM hosts (set 0 to skip)
SELF_UPDATE="${SELF_UPDATE:-1}" # set 0 to skip the autonomous self-update systemd timer
SELF_UPDATE_INTERVAL="${SELF_UPDATE_INTERVAL:-15min}" # how often the box polls origin for new commits
IRC="${IRC:-1}" # set 0 to skip the co-located Ergo IRC server (irc.${DOMAIN})
IRC="${IRC:-1}" # set 0 to skip the co-located Ergo IRC server (${IRC_DOMAIN})
IRC_DOMAIN="${IRC_DOMAIN:-irc.${DOMAIN#*.}}" # IRC host (default: irc.<root-of-DOMAIN>, e.g. irc.profullstack.com)
NEWS="${NEWS:-1}" # set 0 to skip the co-located Usenet/NNTP server (news.${DOMAIN})
ERGO_VERSION="${ERGO_VERSION:-2.18.0}" # Ergo IRCd release to install
IRC_NETWORK="${IRC_NETWORK:-ProfullstackBBS}" # IRC network name shown to clients
@ -481,6 +482,26 @@ news.${DOMAIN} {
"
fi
# IRC site (${IRC_DOMAIN}). Caddy serving this host means it obtains a Let's
# Encrypt cert for it — which ergo-refresh-certs copies into Ergo for 6697 TLS.
# It also fronts the same loopback WebSocket, so wss://${IRC_DOMAIN}/irc works
# (alongside wss://${DOMAIN}/irc). Needs an A record ${IRC_DOMAIN} -> this host.
IRC_SITE=""
if [ "$IRC" = "1" ]; then
IRC_SITE="
${IRC_DOMAIN} {
encode zstd gzip
handle /irc {
reverse_proxy 127.0.0.1:8097
}
handle {
header Content-Type \"text/plain; charset=utf-8\"
respond \"AgentBBS IRC (members-only). Native: ${IRC_DOMAIN}:6697 (TLS), SASL as your BBS name. Web/agents: wss://${IRC_DOMAIN}/irc. Or from the BBS: ssh -t irc@${DOMAIN}\"
}
}
"
fi
cat > /etc/caddy/Caddyfile <<CADDY
{
email ${ACME_EMAIL}
@ -524,7 +545,7 @@ ${DOMAIN} {
file_server
}
}
${GIT_SITE}${NEWS_SITE}
${GIT_SITE}${NEWS_SITE}${IRC_SITE}
# Free per-user homepages at <name>.${DOMAIN} (needs wildcard DNS
# *.${DOMAIN} -> this host). On-demand TLS mints a cert only when agentbbs's
# ask endpoint confirms <name> is a registered member, so random subdomains
@ -558,13 +579,39 @@ ufw allow 80/tcp >/dev/null
ufw allow 443/tcp >/dev/null
systemctl reload caddy 2>/dev/null || systemctl restart caddy
# ---- 9b. Ergo IRC server (co-located irc.${DOMAIN}; humans + agents) --------
# A lightweight single-binary IRC network on its own ports, sharing this box and
# this hostname's TLS cert. Native clients hit irc.${DOMAIN}:6697 (TLS); web
# clients and agents hit wss://${DOMAIN}/irc (Caddy fronts Ergo's loopback
# WebSocket). See docs/irc.md. Disable with IRC=0.
# ---- 9a2. members are OS users (tilde.town model) --------------------------
# Every BBS member gets a real OS account. It is IDENTITY ONLY — a nologin shell,
# so it grants no shell access (BBS login is the wish server on :22, authenticated
# against the sqlite store, not OpenSSH/PAM). This matches the tilde.town shape
# and is what lets the IRC network gate on `getent passwd`. The agentbbs service
# runs unprivileged and can't useradd, so we reconcile here (root) on every deploy
# + the 15-min self-update timer: create an account for any member home dir that
# lacks one. Existing members are migrated on the first run; new members get their
# OS account within one reconcile (<= ${SELF_UPDATE_INTERVAL}).
log "reconciling member OS users from ${DATA_DIR}/users"
getent group members >/dev/null 2>&1 || groupadd --system members
if [ -d "${DATA_DIR}/users" ]; then
for d in "${DATA_DIR}"/users/*/; do
[ -d "$d" ] || continue
name="$(basename "$d")"
# Only valid member/login names; skip anything already taken (incl. system users).
case "$name" in *[!a-z0-9._-]* | "" ) continue ;; esac
id "$name" >/dev/null 2>&1 && continue
# Non-system account (uid auto-assigned >=1000, matching the IRC auth-script
# gate), home = the member's existing data dir, no shell.
useradd --no-create-home --home-dir "$d" --shell /usr/sbin/nologin --gid members "$name" 2>/dev/null \
&& log " + OS user ${name}" \
|| warn " could not create OS user ${name}"
done
fi
# ---- 9b. Ergo IRC server (co-located ${IRC_DOMAIN}; humans + agents) --------
# A lightweight single-binary IRC network on its own ports. Native clients hit
# ${IRC_DOMAIN}:6697 (TLS, using Caddy's Let's Encrypt cert for ${IRC_DOMAIN});
# web clients and agents hit wss://${DOMAIN}/irc or wss://${IRC_DOMAIN}/irc
# (Caddy fronts Ergo's loopback WebSocket). See docs/irc.md. Disable with IRC=0.
if [ "$IRC" = "1" ]; then
log "installing Ergo IRC server v${ERGO_VERSION} (irc.${DOMAIN})"
log "installing Ergo IRC server v${ERGO_VERSION} (${IRC_DOMAIN})"
case "$GOARCH" in
amd64) ERGO_ARCH=x86_64 ;;
arm64) ERGO_ARCH=arm64 ;;
@ -597,28 +644,29 @@ if [ "$IRC" = "1" ]; then
# Render the config template from the repo (the __TOKENS__ become real values).
sed -e "s|__NETWORK__|${IRC_NETWORK}|g" \
-e "s|__DOMAIN__|${DOMAIN}|g" \
-e "s|__IRC_DOMAIN__|${IRC_DOMAIN}|g" \
-e "s|__DATA__|${ERGO_DATA}|g" \
-e "s|__TLS_DIR__|${ERGO_DATA}/tls|g" \
-e "s|__LANG_DIR__|/opt/ergo/languages|g" \
-e "s|__OPER_PASSWORD_HASH__|${OPER_HASH}|g" \
-e "s|__USERS_DIR__|${DATA_DIR}/users|g" \
"${SRC_DIR}/deploy/ergo/ircd.yaml" > /etc/ergo/ircd.yaml
chmod 640 /etc/ergo/ircd.yaml
# IRC is members-only: this auth-script approves a SASL login only if the
# account name maps to an AgentBBS member home dir under ${DATA_DIR}/users.
# account name is a real OS user (uid>=1000) — i.e. a BBS member, since
# members are provisioned as OS users in §4b (tilde.town model).
install -m 0755 "${SRC_DIR}/deploy/ergo/auth-script.sh" /usr/local/bin/ergo-auth-member
# TLS for 6697: reuse Caddy's Let's Encrypt cert for ${DOMAIN}; self-signed
# TLS for 6697: reuse Caddy's Let's Encrypt cert for ${IRC_DOMAIN}; self-signed
# fallback on first run before Caddy has issued it (the timer swaps it in).
install -m 0755 "${SRC_DIR}/deploy/ergo/refresh-certs.sh" /usr/local/bin/ergo-refresh-certs
DOMAIN="$DOMAIN" ERGO_DATA="$ERGO_DATA" /usr/local/bin/ergo-refresh-certs || true
DOMAIN="$IRC_DOMAIN" ERGO_DATA="$ERGO_DATA" /usr/local/bin/ergo-refresh-certs || true
if [ ! -s "$ERGO_DATA/tls/fullchain.pem" ]; then
warn "no Caddy cert for ${DOMAIN} yet — using a self-signed cert on 6697 until the ergo-certs timer swaps in the real one"
warn "no Caddy cert for ${IRC_DOMAIN} yet (is its A record pointed here?) — using a self-signed cert on 6697 until the ergo-certs timer swaps in the real one"
( cd /etc/ergo && /opt/ergo/ergo mkcerts --conf /etc/ergo/ircd.yaml --quiet 2>/dev/null ) \
|| openssl req -newkey rsa:2048 -nodes -days 90 -x509 \
-keyout "$ERGO_DATA/tls/privkey.pem" -out "$ERGO_DATA/tls/fullchain.pem" \
-subj "/CN=irc.${DOMAIN}" 2>/dev/null
-subj "/CN=${IRC_DOMAIN}" 2>/dev/null
fi
chown -R ergo:ergo "$ERGO_DATA" /etc/ergo
@ -628,7 +676,7 @@ if [ "$IRC" = "1" ]; then
log "installing ergo.service"
cat > /etc/systemd/system/ergo.service <<UNIT
[Unit]
Description=Ergo IRC server (AgentBBS — irc.${DOMAIN})
Description=Ergo IRC server (AgentBBS — ${IRC_DOMAIN})
After=network-online.target
Wants=network-online.target
@ -654,11 +702,11 @@ UNIT
# Daily cert refresh from Caddy (tracks auto-renewals).
cat > /etc/systemd/system/ergo-certs.service <<UNIT
[Unit]
Description=Refresh Ergo TLS cert from Caddy for ${DOMAIN}
Description=Refresh Ergo TLS cert from Caddy for ${IRC_DOMAIN}
[Service]
Type=oneshot
Environment=DOMAIN=${DOMAIN}
Environment=DOMAIN=${IRC_DOMAIN}
Environment=ERGO_DATA=${ERGO_DATA}
ExecStart=/usr/local/bin/ergo-refresh-certs
UNIT
@ -901,8 +949,9 @@ cat <<DONE
Web https://${DOMAIN}/ site root
https://${DOMAIN}/~<name> a member's homepage
https://<your-domain> a member's homepage on a custom domain (auto-HTTPS)
IRC irc.${DOMAIN}:6697 (TLS) native clients ${IRC:+(set IRC=0 to disable)}
IRC ${IRC_DOMAIN}:6697 (TLS) native clients (DNS: ${IRC_DOMAIN} A -> host) ${IRC:+(set IRC=0 to disable)}
wss://${DOMAIN}/irc web clients + agents over WebSocket
ssh -t irc@${DOMAIN} the in-BBS client (premium: /create #chan)
/OPER admin <pw> oper password in ${ENV_DIR}/ergo-oper.txt
News news.${DOMAIN}:563 (NNTPS) newsreaders + agents ${NEWS:+(set NEWS=0 to disable)}
ssh -t news@${DOMAIN} the in-BBS newsreader (DNS: news.${DOMAIN} A -> host)