- Python 79.9%
- HTML 9.1%
- Jinja 7.2%
- Shell 3.7%
| host_vars | ||
| roles | ||
| .gitignore | ||
| ansible.cfg | ||
| hosts | ||
| README.md | ||
| site.yml | ||
runv.club
Server config for the runv.club pubnix. The box is set up from this repo with Ansible, run on the server itself:
cp host_vars/srv1.yml.example host_vars/srv1.yml # and edit it
sudo ansible-playbook site.yml
Two secrets live outside the repo and must exist before the first run:
/etc/runv/s3.env, S3 credentials for the backups/etc/runv/restic.pass, the restic repository password
Mail (welcome messages, the weekly report and failure alerts) stays off
until /etc/runv/mail.env exists. There is an example next to it.
Members
sudo runv-user add NAME --email ADDR --key-file key.pub
sudo runv-user suspend NAME --reason '...'
sudo runv-user unsuspend NAME
sudo runv-user delete NAME
sudo runv-user list
Each member gets 1G of RAM, 200 processes, one CPU, 2G of disk and
200k files. runv-user quota-sync brings existing members in line when
the limits change (ansible runs it).
Deleted homes sit in /var/lib/runv/deleted for 14 days before they
are gone for good.
What they left in /srv/square goes to the quarantine with them, and
what's theirs in /tmp, /var/tmp and /dev/shm is removed, since uids
get reused. A uid still in the quarantine isn't handed out again.
/srv/square is the shared directory: mode 1777, so anyone writes and
nobody touches other people's files (no setgid: with umask 002, files in
a members-group dir would be writable by everyone), and the kernel's
protected_* settings cover links and fifos left there. It counts on
quota and isn't backed up.
Web, Gemini and Gopher
nginx serves https://runv.club/~NAME/ from ~/public_html, molly-brown
serves gemini://runv.club/~NAME/ and gophernicus serves
gopher://runv.club/1/~NAME. Links to files a member doesn't own are not
followed, and a suspended member's pages go dark on all three.
molly-brown only reads under /var/gemini, so each active member's
public_gemini is bind mounted read-only into /var/gemini/users.
runv-user keeps those mounts in sync, don't touch them by hand.
The Gemini certificate is self-signed and made once. Clients pin it, so don't regenerate it.
I2P
Every member gets an eepsite with an address of their own, served from
~/public_i2p. runv-user writes one i2pd tunnel per member in
/etc/i2pd/tunnels.d and leaves the address in ~/.i2p-address. The key
in /var/lib/i2pd/runv is the address, so it's backed up as soon as it
exists and only goes away with the account. i2pd runs with no console and
no proxies.
On the box
Members get help (topics live in /usr/local/share/runv/help, and
runv-help-web puts them at /help too), a
login banner that's different every time (motd), and the usual tools:
editors, git, tmux, irssi, weechat, senpai, compilers and interpreters for
Python, C, Go, Rust, Node, Ruby, Lua, Haskell, Scheme, Common Lisp and
Racket, and games (help games, botany included). mosh works on UDP 60000-61000.
runv is the member dashboard, in the terminal: keys, tokens, pages,
blog, pastes, access logs, stats and the state of every service. Every
screen also prints as plain text (runv keys, runv analytics). The few
things that need root go through runv-priv, which members can run with
sudo. It only ever acts on whoever called it and takes no free arguments.
blog: markdown in~/blogbecomes a blog with an Atom feed on the web, Gemini and Gopher. A timer rebuilds it as the member, every 3 minutes, when something changed.runv-planetcollects everyone's posts at/planetand on the home page, and writes/tilde.json, the member directory at/~(with who changed their page in the last day, also as a feed at/updated.xml) and the webring at/ring. Members join the ring withtouch ~/.ring.pb: text from stdin to~/public_html/p, with an expiry.tw: twtxt in~/public_html/twtxt.txt, with a timeline of all members.board: a forum in the terminal. Posts are written byrunv-privinto/var/lib/runv-board, so nobody can touch other people's posts.- finger on port 79 shows
~/.planand who is on. write NAMEandmesg: Debian 13 dropped both, and utmp with them. These are ours.writegoes throughrunv-priv, which finds the ptys the target owns and writes to the most recently used one that has the group write bit (mesg y), with control characters stripped and at most five messages a minute.
Guestbooks
runv-guestbook (flask on 127.0.0.1:8002, user runvgb) serves
/guestbook/NAME, an iframe version at /guestbook/NAME/embed and the
approved notes as json. A book is off until its owner runs
guestbook on, and notes show up only after guestbook ok N.
Moderation goes through runv-priv guestbook, which runs gbctl.py as
runvgb with the caller's name, so nobody touches someone else's book and
the database is never root's. Against spam: a signed one-use token
(runvform.py, shared with the signup form), a honeypot, 3 notes per book
and 10 overall per address a day (kept as a salted hash for 30 days), and
nginx limits. Suspending a member closes their book, purging deletes it.
IRC bouncer
soju at irc.runv.club:6697, gamja at https://chat.runv.club. Every
member gets a bouncer account on tilde.chat in #runvclub, with the password
in ~/.config/runv/irc-token. chat opens senpai on it. Members can add
up to 3 networks. Accounts idle for 30 days get disconnected until they
log in again, and suspending a member locks theirs.
Tunnels and apps
Whatever listens on /run/runv-apps/NAME/app.sock is served at
https://NAME.tun.runv.club: a tunnel from the member's machine
(ssh -R /run/runv-apps/NAME/app.sock:localhost:8000 NAME@runv.club -N)
or an app under systemd --user (lingering is on for members). Members
may only forward to sockets: no -L, -D or TCP -R, so the box can't
be used as a proxy. runv-tun-cert keeps one certificate with every
active member's name on it.
Stats
nginx logs ~member requests to their own file. runv-stats runs every
hour and gives each member an access log with addresses cut down, kept
14 days, and, if they turned analytics on, counts of unique visitors
(hashed with a salt that's thrown away every day), pages and referrers,
kept a year. No address is stored.
ntfy and feeds
ntfy.runv.club is closed by default. runv token ntfy gives a member
a user whose only topics start with their name. feeds.runv.club is
miniflux on postgres, with login through Forgejo only.
From outside, the box only forwards. admin@, postmaster@, abuse@,
dmarc@ and root@ go to mail_forward_to in host_vars, with SRS so
SPF still passes on the other end. Members can forward their own address
with runv mail forward set ADDRESS: a code goes to that address through
Mailgun, and only after runv mail forward confirm CODE does runv-user mail-sync add it to runv-forward (virtual), runv-forward-transport
(the only other addresses mail may leave for) and runv-inbound.regexp
(who outside may write to). The data lives in /var/lib/runv/forwards.json.
Anything else is refused at RCPT and nothing is relayed.
rspamd sits in front as a milter: spam is refused during the SMTP
conversation (score 12), nothing is greylisted, each address takes at
most 100 messages a day from outside (valkey keeps the counts), mail that
comes in gets an ARC seal and mail from runv.club addresses a DKIM
signature (selector s2026, key in /var/lib/rspamd/dkim). unbound on
127.0.0.1:8053 answers rspamd's DNS blocklist lookups. Mail goes out over
IPv4 only, one message every 3 seconds per destination. A forward whose
every message bounces for three days is switched off by runv-user forward-check, and the member is told.
Inside, members mail each other: runv.club is a local domain, delivered
to /var/mail (50MB each). Nothing leaves: default_transport is an
error, and transport_maps lets out only the exact mail_forward_to
address. Only root and active members may use sendmail, each under their
own name (local_login_sender_maps). runv-user keeps the list of
senders and the suspended members (whose mail bounces) in step with the
member list, and builds the mailing lists (mail_lists, sent to
list+NAME) from each member's ~/.lists. A deleted member's mailbox
goes to the quarantine with their home.
Bounces for forwarded mail don't go back to the original sender outside: SRS addresses are refused at RCPT, since nothing could take the bounce out anyway. A destination that refuses mail is caught by forward-check.
git.runv.club and links.runv.club
Forgejo, closed to the outside: no signups, nothing visible without
logging in. Git over SSH is on port 2222 with the same key as the shell,
and each member has 1GiB. The binary comes from Codeberg and is checked
against its GPG signature, so updating means changing forgejo_version
and the checksum in the role defaults.
linkding runs from a venv and has no login form of its own. People log in through Forgejo (OIDC), so only members get in.
runv-user creates the Forgejo account along with the shell account and
leaves the first password in ~/.forgejo-password. Suspending a member
locks them out of both, and the purge 14 days after a delete removes the
accounts and the repos.
Signups
The form at /join, and ssh join@runv.club (a user with no password
whose only command asks the same questions and posts to the same form),
only drop requests into /var/lib/runv-join/queue.
Nothing gets created until someone runs:
sudo runv-queue list
sudo runv-queue approve ID
sudo runv-queue reject ID --reason '...'
At most 5 approvals a week and 100 people in line. Every 60 days in line
people are asked if they still want in, and they're dropped if they don't
answer within a week. Set signup_state: open in host_vars to open the
form.
Backups
restic to S3. A full backup runs on the 1st and the 16th and the last 6
are kept. The member list, the signup queue and the I2P keys are also
sent right after they change. The Forgejo and linkding databases are
copied with SQLite's backup API into /var/backups/runv-db before each
full run, along with the soju and ntfy databases and a pg_dump of
miniflux, and those copies are what gets backed up.
sudo runv-backup status