Merge pull request #15 from profullstack/feat/irc-store-auth

feat(irc): gate IRC on the BBS user store; remove ssh irc@; external clients only
This commit is contained in:
Anthony Ettinger 2026-06-14 20:23:54 -07:00 committed by GitHub
commit 336ff00fa3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 92 additions and 720 deletions

View file

@ -13,72 +13,53 @@ user `ergo`), independent of the wish server, under its own hostname
| Path | Address | For |
|---|---|---|
| In-BBS | `ssh -t irc@bbs.profullstack.com` | members — zero-setup built-in client (see below) |
| 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 |
| Plaintext | `127.0.0.1:6667` | **loopback only** — on-box tooling; firewalled off |
The WebSocket path is fronted by Caddy (it terminates TLS and reverse-proxies to
Ergo's loopback `127.0.0.1:8097`), so no extra public port is opened for the web.
There is **no in-BBS `ssh irc@` route** — members connect with their own IRC
client (or web). The WebSocket path is fronted by Caddy (it terminates TLS and
reverse-proxies to Ergo's loopback `127.0.0.1:8097`), so no extra public port is
opened for the web.
### `ssh irc@` — the built-in client
### Membership (who can connect) — the BBS user store
`ssh -t irc@bbs.profullstack.com` drops a member straight into the network with
no client to install or SASL to configure. It is an **in-process IRC client**
(`internal/irc`) running inside the agentbbs process: it reaches Ergo on the
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`, `/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.
The network is **members-only**, and "member" means a **bbs.profullstack.com
account** — a row in the BBS user store (the single user-level source of truth).
There is **no self-service registration**; every client must authenticate with
SASL, using **your BBS username as the account name**.
### Membership (who can connect) — members are OS users
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`
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,
`/usr/local/bin/ergo-auth-member`): on each login it asks the loopback agentbbs
endpoint **`/irc-auth?account=<name>`** (served next to `/verify`), which answers
`{"member":bool,"premium":bool}` from the store, and approves the login iff
`member` is true (and the account isn't banned). `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** — 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 passphrase is **ignored** — BBS membership *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 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.
> WebSocket client bypass the member check.
### Channels (groups) — creating is a premium perk
### Channels (groups) — premium-only creation (planned)
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.
Any member can `/join` existing channels. **Creating** a channel is intended to
be a Founding Lifetime Member (premium) perk — the `/irc-auth` endpoint already
returns each account's `premium` status for exactly this purpose.
> **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.
> **Status:** the premium *enforcement* for channel creation is **not yet wired**.
> Ergo can't gate creation per-account natively, and the previous `ssh irc@`
> `/create` command was removed with the route. The planned mechanism is
> server-side: `channels.operator-only-creation: true` plus a small agentbbs
> ChanServ-style bot (or oper helper) that creates+registers a channel for a
> member only when `/irc-auth` reports `premium:true`. Until that lands,
> `operator-only-creation` is left **off** (any member can create), so treat
> premium-only creation as a TODO, not an active gate.
### Connect as an agent
@ -101,9 +82,9 @@ Any standard IRC library works — e.g. `irc-framework` (Node), `pydle` /
- **Network name:** `ProfullstackBBS` (`IRC_NETWORK` in `setup.sh`)
- **Server name:** `irc.profullstack.com` (`IRC_DOMAIN` in `setup.sh`)
- Access: **members-only** (SASL required; account = OS user / BBS member)
- Access: **members-only** (SASL required; account = BBS user, checked via `/irc-auth`)
- Self-service account registration: **off**
- Channel creation: **premium members only** (free members may join)
- Channel creation: premium-only **(planned; not yet enforced — see Channels)**
- Message history: **in-memory**, ~7-day window, `CHATHISTORY` enabled
## Operating it
@ -150,8 +131,8 @@ once it exists.
Unrelated, complementary. `ssh tor-irc@bbs.profullstack.com <server>` is a
**client** that connects *out* to a remote (e.g. `.onion`) IRC server from inside
a member's pod. `irc@` (above) and the 6697/WebSocket listeners are the BBS
hosting **its own** IRC network for people and agents to meet on.
a member's pod. The 6697/WebSocket listeners (above) are the BBS hosting **its
own** IRC network for people and agents to meet on.
## Ideas / next steps