mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
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:
parent
d4ada98b69
commit
5eb1e96480
5 changed files with 194 additions and 87 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
95
docs/irc.md
95
docs/irc.md
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -24,18 +24,20 @@ 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"))
|
||||
return nil
|
||||
}
|
||||
m := &model{
|
||||
c: c,
|
||||
channel: DefaultChannel,
|
||||
width: ptyReq.Window.Width,
|
||||
height: ptyReq.Window.Height,
|
||||
c: c,
|
||||
channel: DefaultChannel,
|
||||
canCreate: canCreate,
|
||||
width: ptyReq.Window.Width,
|
||||
height: ptyReq.Window.Height,
|
||||
}
|
||||
m.lines = append(m.lines,
|
||||
cSys.Render(fmt.Sprintf("connected as %s — joined %s. /help for commands, esc to leave.", c.Nick(), DefaultChannel)))
|
||||
|
|
@ -62,10 +64,11 @@ func waitEvent(c *Client) tea.Cmd {
|
|||
}
|
||||
|
||||
type model struct {
|
||||
c *Client
|
||||
channel string // current conversation target for typed lines
|
||||
lines []string
|
||||
input string
|
||||
c *Client
|
||||
channel string // current conversation target for typed lines
|
||||
canCreate bool // premium members may /create (register) new channels
|
||||
lines []string
|
||||
input string
|
||||
|
||||
width, height int
|
||||
}
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
85
setup.sh
85
setup.sh
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue