diff --git a/README.md b/README.md
index 4a187fd..33977b1 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,64 @@
-tilde.club
-==========
+# tilde.club
-This is the set of code and utilities for running and managing the
-tilde.club server.
+This repo documents how to build and run a shared Unix machine like
+[tilde.club](https://tilde.club). It also keeps the older site utilities at their
+existing paths.
-# What is tilde.club?
-- [https://tilde.club](https://tilde.club) is one unmodified unix server on the Internet
- - Several thousand people have accounts
- - It was launched at the end of September 2014
-- Tilde.Club is *also* an emerging, mutually supportive community of dozens hundreds thousands of people launching similar servers.
- - These people enjoy working in the tool-rich, very social Unix programming environment.
- - Many of these people want to help new users use these open, free tools to empower themselves.
- - Even new users who are scared of command-lines.
- - Especially new users who want to learn but don't like being laughed at.
- - _We won't laugh at you because we know how hard it is to learn about technology._
- - _If this is confusing we will try to help._
-- [Here is the story of how tilde.club started](https://medium.com/message/tilde-club-i-had-a-couple-drinks-and-woke-up-with-1-000-nerds-a8904f0a2ebf). Basically, one person had a few too many drinks and launched a cheap computer in the cloud. Github user @ftrain is the initiator and recently Github user @mbuchholz81 has taken over tilde.club from @ftrain.
-- [Here is the FAQ](https://tilde.club/wiki/faq.html) which is being developed here at this GitHub repository.
-- No particular entity controls tilde.club. It's emerging as sort of an open, basically free mini-Internet for people who need some refuge from the regular Internet. It was founded with the rule, "no drama," a rule directly inspired by the Mary J. Blige song "No More Drama." So far there's only been a little drama. It's only been three weeks, though. If we're honest with ourselves there is going to be drama.
+Members get a shell account, space for their own pages, and tools for writing,
+programming, chatting, and making whatever interests them. You do not need to
+know all of Unix before joining. Helping people learn is part of running the
+place.
-It's all moving very fast.
+## Build your own
-# Getting started: run a tilde and help users build on it
+Start with [Build a tilde-style machine](docs/how-to-set-up-a-tilde.md).
+It follows a Fedora/nginx setup from a new host through account creation,
+publishing, limits, backups, and launch checks.
-If your goal is to launch your own tilde-style host (or improve an existing one), start here:
+The [current system record](docs/current-system.md) describes what was confirmed
+on tilde.club on October 22, 2026. The [example configs](examples/README.md) are
+separate starting points for a new host. Applying this repo patch does not
+install them or change a running server.
-- [Build your own tilde-style server](docs/how-to-set-up-a-tilde.md)
-- [Shell server setup notes](docs/shellserver.md)
-- [SSH key onboarding guide](docs/ssh.md)
-- [Current `/etc/skel` permissions reference](docs/etc-skel-permissions.md)
-- [User and session accounting quick reference](docs/accounting.md)
+| Topic | Guide |
+| --- | --- |
+| Base installation | [Build a tilde-style machine](docs/how-to-set-up-a-tilde.md) |
+| Accounts and onboarding | [Account creation and first login](docs/accounts.md) |
+| Login and recovery | [SSH keys and MFA](docs/ssh.md) |
+| Home defaults | [Skeleton and permissions](docs/etc-skel-permissions.md) |
+| Publishing | [nginx, PHP-FPM, and CGI](docs/web.md) |
+| Mail | [Postfix, Dovecot 2.4, submission, and filtering](docs/mail.md) |
+| Other publishing | [Gopher, Gemini, and finger](docs/publishing.md) |
+| Chat | [IRC operations](docs/ircserver.md) and [connection details](docs/ircserver-connections.md) |
+| News | [INN and local newsgroups](docs/usenet.md) |
+| Daily operation | [Quotas, resource limits, firewall, and maintenance](docs/shellserver.md) |
+| Recovery | [Borg/borgmatic backups and restore](docs/backups.md) |
+| Usage records | [Login and process accounting](docs/accounting.md) |
+| Website and apps | [Sources, signups, wiki, and community tools](docs/community.md) |
-These documents are focused on practical operations, onboarding, and adding community functionality in safe increments.
+The older [server.org](docs/server.org) path remains as a pointer. The
+[API notes](api/README.org), [recent-page utility notes](api/recently-updated/README.md),
+and [connection-list notes](pages/list-html-connections/README.md) explain the
+retained programs and their limitations. They are not an unattended installer.
-# Help Wanted
-Tilde.club is moving very quickly and is 100% volunteer led.
+## About tilde.club
-## 1. optimal: pull requests and bug reports
-- There is a lot of great work going on around config, FAQ, basic docs.
-- Jump in if you want. Literally everyone here said "I am ready to help, what can I do."
-- We need non-coders (writers, editors, communicators, artists, project managers) as much as we need tech folks.
-- Most of the coding work is done for us by the Unix operating system. We're celebrating that.
-- A lot of the basic docs are in the [wiki](https://github.com/tildeclub/tilde.club/wiki) which you (yes, you) can edit.
+Tilde.club started in September 2014. Paul Ford wrote about
+[how it began](https://medium.com/message/tilde-club-i-had-a-couple-drinks-and-woke-up-with-1-000-nerds-a8904f0a2ebf).
+The [website](https://tilde.club) and [user wiki](https://tilde.club/wiki/) cover
+joining and using the server. This repo covers operating one.
-## 2. suboptimal: open ended feature requests and broad discussion
-- We love philosophy, just not in our issue tracker!
+[Preguntas frecuentes en español](faq-esp.md).
+
+## Contributing
+
+Corrections, tested configs, and missing instructions are welcome. Include the
+OS and software versions you used, what you changed, and how you checked the
+result. Documentation fixes are useful contributions too.
+
+Use example domains and accounts. Leave passwords, private keys, MFA secrets,
+and members' personal information out of commits and issue attachments. Keep
+production-specific settings distinct from advice for a new installation.
+
+Existing utilities are licensed under the terms retained in [LICENSE](LICENSE)
+and their source notices. This update preserves those files and attributions.
diff --git a/api/README.org b/api/README.org
index 7b523cb..29167b6 100644
--- a/api/README.org
+++ b/api/README.org
@@ -1,7 +1,24 @@
-Gather up the code that people are writing and put it here
-- chat tools
-- user listing
-- user #
-- list of sites
-- river
--
+#+TITLE: Site utilities
+
+* Scope
+
+The files under this directory are retained utility implementations, not the
+current server configuration or an automated installation system. Start with
+[[file:../docs/how-to-set-up-a-tilde.md][the installation guide]] and [[file:../docs/community.md][community applications]].
+
+* Recently updated pages
+
+See [[file:recently-updated/README.md][the implementation notes]] before using these scripts.
+A separate public repo also exists at https://github.com/tildeclub/recently-updated.
+Inspect the chosen version; do not infer production deployment from a repo name.
+
+Run indexing as an unprivileged service identity with only intended public-read
+access, stage output outside the served tree, validate it, and publish atomically.
+Do not run a broad scan of homes as root from a web request.
+
+* Adding an endpoint
+
+Document its input, output schema, account visibility, schedule, data retention,
+error behavior, and owner. Escape HTML and serialize JSON with a real serializer.
+A local account name or filesystem filename is data, not trusted markup.
+Keep private account fields and credentials out of generated public output.
diff --git a/api/recently-updated/README.md b/api/recently-updated/README.md
index 11a1e9e..f8d15e4 100644
--- a/api/recently-updated/README.md
+++ b/api/recently-updated/README.md
@@ -1,40 +1,46 @@
-# Recently-Updated Page Shell Scripts
+# Recently updated pages: retained implementations
-As soon as [~ford](http://tilde.club/~ford) granted me entrance to tilde.Club, I knew that I wanted to keep tabs on what cool things folks were doing there. And thinking back to the mid-90s, what was the best way to know what was new on a site? A recently-updated page! So I set about making one for tilde.club.
+These scripts are kept so existing users do not lose the programs or old paths.
+They are historical implementations, not a tested production installation
+recipe. The maintained deployment source must be chosen and reviewed separately;
+there is also [tildeclub/recently-updated](https://github.com/tildeclub/recently-updated).
-There's no question at all that this is a rudimentary hack, but it's the *exact* sort of hack that was The Way To Do Things on the mid-90s Internet. (Note that that's the mid-90s-capital-I-Internet, not the [21st-century-lowercase-i-internet](http://www.nytimes.com/2002/12/29/weekinreview/29SCHW.html).) And I'm using tried-and-true command-line Unix filesystem commands to grab, parse, and sort the list of pages, so I feel like I'm treading in the footsteps of thousands before me. The result is [the list of all tilde.club pages updated in the last 24 hours](http://tilde.club/~delfuego/tilde.24h.html), which is generated from the ```findmod.sh``` shell script once a minute from a crontab job.
+## Files and interfaces
-Of course, while the HTML version of the recently-updated list is firmly mid-90s (well, if you view source you'll see that it's 21st-century HTML5, but still), enabling cooler 21st-century functionality required making the same list available in a more easily-parsed format. Enter JSON, and the [API version of the recently-updated list](http://tilde.club/~delfuego/tilde.24h.json), generated by the ```findmod-json.sh``` shell script once a minute via crontab as well.
+| File | Behavior |
+| --- | --- |
+| `findmod.sh` | Writes HTML to stdout for recently changed top-level `index.html` files. |
+| `findmod-json.sh` | Writes a JSON-like `pagelist` response to stdout for the same index scan. |
+| `findmod2.pl` | Scans each public subtree and writes both HTML and JSON output files. |
+| `header.tpl`, `footer.tpl` | HTML fragments used by the original shell implementation's external template paths. |
-Hope these are useful to someone!
+The original shell script hard-codes template paths in another user's home and
+an HTTP domain. Configure a reviewed copy for your own domain and deployment
+path rather than creating that person's home layout on a new host.
-Jason Levine, aka [~delfuego](http://tilde.club/~delfuego)
+The Perl program accepts `--domain`, `--root`, `--since-hours`, `--destdir`,
+`--update-every`, `--verbose`, and `--help`. Its default window is 24 hours and
+its output names are `tilde.24h.html` and `tilde.24h.json` (with the chosen hour
+count substituted). A positive update period makes it keep rescanning.
+## Limitations to fix before deployment
-# Recently-Updated Page Perl Script
+The retained implementations concatenate strings instead of safely serializing
+all output. The Perl JSON writer includes a trailing comma, so its nonempty JSON
+output is invalid. HTML escaping and URL encoding also need review. Filesystem
+names must not be trusted as markup, and timestamps labeled UTC must actually
+be generated in UTC. The shell regex also restricts which usernames it finds.
-`findmod2.pl` is a pure-Perl5 reimplementation of ~delfuego's scripts.
-It's self-contained, readable, well-documented and will search a
-user's entire `public_html` subtree for the most recent file rather
-than just using the date of the `index.html` file.
+The tree scanner can traverse large user-controlled public trees. Define bounds,
+symlink handling, public-file eligibility, error handling, and exclusion policy.
+Run with read access only to intended public content; running it as root can
+publish information about files the web server itself cannot read.
-It accepts the following command-lines options:
+For a deployment, validate JSON with `python3 -m json.tool`, check HTML and URLs,
+write to a private temporary file on the destination filesystem, and rename the
+validated output into place. Do not truncate the live page at the start of a
+scan or repeatedly publish partial files from overlapping jobs.
-* --domain *name* -- Domain name for the site. Guesses if omitted.
-* --root *url* -- Root URL to use in links. Derived from domain by default.
-* --since-hours *hours* -- Number of hours considered recent. Default = 24.
-* --destdir *path* -- Destination directory for output files.
-* --update-every *seconds* -- Regenerate the lists every *seconds*.
-* --verbose -- Print more information as the program runs.
-* --help -- Print a simple help message and exit.
-
-If `--update-every` is given with a positive number, `findmod2.pl`
-will loop forever, waiting the given number of seconds before
-rescanning and regenerating the list. Otherwise, it will scan once
-and exit.
-
-The output files are named `tilde.XXh.json` and `tilde.XXh.html`,
-where *XX* is replaced by number of hours considered recent (24 or the
-argument given with `--since-hours`).
-
-Chris Reuter, [~suetanvil](http://totallynuclear.club/~suetanvil).
+The original scripts were contributed by Jason Levine (~delfuego) and Chris
+Reuter (~suetanvil). Their interfaces and code are preserved by this docs update;
+no claim is made that they have been repaired or replaced here.
diff --git a/docs/accounting.md b/docs/accounting.md
index 9a7381b..c3e4221 100644
--- a/docs/accounting.md
+++ b/docs/accounting.md
@@ -1,49 +1,57 @@
-# Accounting
+# Login and process accounting
-This page collects simple command-line checks for understanding shell usage on a multi-user host.
+Accounting is useful for understanding use and investigating a problem. It is
+not a reliable count of people and it is not proof of what someone intended.
+Tell members what you retain and restrict access to logs containing addresses
+and commands.
-## Longest cumulative login time (per user)
-
-The historical one-liner below is still useful, but this version is easier to read and less dependent on shell backticks:
-
-```bash
-users | tr ' ' '\n' | sort -u | while read -r user; do
- ac "$user" | awk -v u="$user" '{print $1, u}'
-done | sort -n
-```
-
-### Notes
-
-- `ac` reads connection accounting data (typically from `/var/log/wtmp`).
-- Results are cumulative and depend on log retention.
-- On some systems, log rotation or reboots can make totals look lower than expected.
-
-## Who is currently online
+## Current sessions
```bash
who
+w
+loginctl list-sessions
```
-If you only need usernames:
+These commands answer different questions. A persistent tmux session, a user
+service, and a current terminal login are not the same thing. Some terminal
+programs also update traditional login records differently.
+
+## Login history
+
+On systems with traditional wtmp records:
```bash
-who | awk '{print $1}' | sort -u
+last
+ac -p
```
-## Most recent login for each user
-
-```bash
-lastlog
-```
-
-This is useful for finding dormant accounts and checking recent activity.
-
-## Total command usage summary (process accounting)
-
-If process accounting is enabled, you can summarize command usage:
+`ac -p` summarizes the retained connection records for all recorded users.
+Do not start with `users` if the goal is to count everybody: that limits the
+input to people logged in at the time you run the command. Rotated or missing
+records change the totals, and concurrent sessions are not unique human time.
+
+Check which tools and storage the OS actually uses. Newer Fedora installations
+may use lastlog2 and different login-record implementations. Use the installed
+`lastlog2 --help` and service documentation rather than assuming the old
+`lastlog` file is populated. A missing record does not establish that an account
+has never been used.
+
+## Process accounting
+
+The collected host has `psacct.service` active. On a new Fedora host, install
+and enable it only after deciding the logging and retention policy:
```bash
+dnf install psacct
+systemctl enable --now psacct
sa
```
-Not all systems enable this by default; if `sa` has no data, verify process accounting configuration first.
+`sa` summarizes completed-process accounting records. It does not replace
+systemd resource limits, live process inspection, or application logs. Check
+package log rotation so accounting data does not consume the filesystem.
+
+For a service problem, start with that service's journal and resource counters.
+For a user resource problem, inspect the relevant user slice as described in
+[shell operations](shellserver.md#process-memory-and-cpu-limits).
diff --git a/docs/accounts.md b/docs/accounts.md
new file mode 100644
index 0000000..21e3f64
--- /dev/null
+++ b/docs/accounts.md
@@ -0,0 +1,101 @@
+# Accounts and first login
+
+These notes describe the scripts collected from tilde.club on October 22, 2026.
+They explain the dependencies and side effects that a replacement installation
+must account for. Do not run the production scripts on a new machine before
+changing the domains, paths, and external service integrations.
+
+## What account creation currently does
+
+`/usr/local/bin/makeuser` accepts a username, contact email, and SSH public key.
+For a new username, it performs these operations in order:
+
+1. Generate a 20-character password with `pwgen`.
+2. Create a matching primary group if needed.
+3. Create a Bash account and home, adding supplementary group ID 100.
+4. Set the account password with `chpasswd`.
+5. Render `/usr/local/bin/welcome-email.tmpl` and send it through `sendmail`.
+ The message is also copied to the new local user and the root mailbox.
+6. Write the supplied public key to the new home's `.ssh/authorized_keys`.
+7. Announce the account through the local `toot` command.
+8. Remove matching entries from `/var/signups_current`.
+9. Remove the skeleton's `.git` directory and README from the new home.
+10. Sort the password database with `pwck -s`.
+11. Request an XFS block quota of 1 GiB soft and 3 GiB hard.
+12. Create a ZNC account with the generated password and a three-network limit.
+13. Add the contact address to the local helpdesk.
+
+The mailing-list subscription function is present but its call is commented
+out. PHP pool creation is not called by this copy of `makeuser`.
+
+If the username already exists, the script exits successfully without doing
+the remaining work. That is important after an interrupted run: invoking it
+again does not complete a partially provisioned account. Check each dependent
+service before treating that account as ready.
+
+## Dependencies to resolve on another host
+
+The common group must exist before account creation. Group ID 100 is a local
+choice; check its meaning on the target instead of assuming that ID names the
+same group everywhere.
+
+The skeleton must supply `.ssh` with mode 0700 and `authorized_keys` with mode
+0600. The script writes into that file; it does not create the parent directory
+or independently repair its ownership and permissions.
+
+The production script also needs Python 3, `pwgen`, working outbound mail, its
+welcome template, XFS quota support, and these local integrations:
+
+- `/usr/local/bin/toot`
+- `/usr/local/bin/znccreate.py`
+- `/usr/local/bin/helpdesk_admin.sh`
+
+These are separate components. Merely copying `makeuser` does not install them.
+The production command changes external services as well as the Unix account,
+so an account test must use a test configuration for those services too.
+
+## First-login wizard
+
+`/usr/local/bin/firstlogin` is a Ruby program using `tty-prompt` and `tty-screen`.
+It also uses `figlet`, `passwd`, `chsh`, `/etc/shells`, `/etc/editors`, `tzselect`,
+and optionally Byobu. Its sequence is:
+
+1. Change the account password using the temporary password from the welcome
+ message.
+2. Choose a login shell from an allowlist of names in `/etc/shells`.
+3. Choose an editor and append `EDITOR` to `.bash_profile`.
+4. Offer to enable Byobu on login.
+5. Choose a timezone; write it to `.zshrc` for Zsh or `.bash_profile` otherwise.
+6. Offer mail forwarding through `.forward`.
+7. Offer to run `setup-2fa`.
+8. Save pronouns in `.pronouns`.
+9. Remove `.new_user`; launch Byobu if selected.
+
+The wizard makes enrollment optional. The collected `setup-2fa` simply invokes
+Google Authenticator, and the main SSH config accepts key-only login separately
+from its PAM password/TOTP path. See [SSH and MFA](ssh.md) for the observed policy
+and an explicit new-host enforcement example. The `.new_user` marker and shell
+startup hook do not prevent someone from using another login path.
+
+The collected wizard writes the editor setting to `.bash_profile` regardless
+of the selected shell, and handles timezone setup specially only for Zsh. Test
+those settings after selecting Fish or another shell before documenting them
+as working for every choice.
+
+## Checks for a newly created account
+
+On a test installation, verify all of the following before sending an account
+ready message:
+
+- Primary and supplementary groups match the intended policy.
+- The home, `.ssh`, and key file have the intended owner and permissions.
+- A supplied key can log in, and an unrelated key cannot.
+- First-login setup runs from each supported login path.
+- Enrollment and subsequent SSH authentication behave as documented.
+- The user's starter page works at the published URL.
+- Mail delivery and any forwarding work.
+- Quotas are enforced on the filesystem containing the home.
+- ZNC and helpdesk provisioning completed, if those integrations are enabled.
+
+Use a separate connection for login tests. Keep the existing admin connection
+and provider console available while changing authentication.
diff --git a/docs/backups.md b/docs/backups.md
new file mode 100644
index 0000000..9c0b66b
--- /dev/null
+++ b/docs/backups.md
@@ -0,0 +1,169 @@
+# Backup and recovery
+
+The collected host runs Borg 1.4.5. Root's crontab invokes
+`/usr/bin/borgmatic` at 03:00 daily; that executable reports 2.1.9. A separate
+`/usr/local/bin/borgmatic` reports 1.5.21 and appears earlier in the collector's
+PATH. Use the absolute path from the actual job when checking versions,
+validating configs, or reproducing a run.
+
+The production backup configuration and credentials were not collected. The
+example here is a new-host plan, not a claim about its repository, retention,
+or current source coverage. Historical backup logs are also kept on the
+operator's NAS; missing local journal history is not proof a backup did not run.
+
+## Decide what must be recoverable
+
+Include these in the plan:
+
+| Data | What to capture |
+| --- | --- |
+| Accounts | `/etc`, homes, UIDs/GIDs, keys, shell defaults, quotas, and group memberships. |
+| Member publishing and mail | Public directories, private mailbox data, forwarding and Sieve files. |
+| Local code | `/usr/local`, source revisions, application lockfiles, service definitions, and build notes. |
+| Operator web apps | Code, uploaded content, config, and a consistent database backup. |
+| Service state | ZNC, INN spool/history, Gemini/Gopher content, and other app-specific state. |
+| Secrets | Restricted encrypted copies of signing keys, service credentials, and recovery material. |
+| Backup recovery | Repository key, passphrase, SSH credentials, destination, and restore instructions held off the host. |
+
+Do not assume all state lives in `/home`. Inspect service units and application
+configs. A Postfix queue restore needs its own policy because re-delivering
+queued mail can duplicate messages. Reinstalling a database package and copying
+its live data directory is not a substitute for a supported database restore.
+
+## Prepare a remote repository
+
+Install the maintained `borgbackup` and `borgmatic` packages. Configure a
+dedicated backup account/key on the destination and verify the host key through
+a trusted channel. Restrict the remote key to the intended repository and
+operations using the destination's supported Borg setup.
+
+Keep the repository passphrase in a root-only file outside the repo. Create
+`/root/.config/borg` at 0700 and the passphrase file at 0600 using a root editor
+or password-manager procedure; do not put the passphrase in a shell command,
+git commit, or public config archive.
+
+For the Borg **1.x** example, initialize a new repository from a root Bash shell:
+
+```bash
+export BORG_PASSCOMMAND='cat /root/.config/borg/passphrase'
+repository='ssh://backup@backup.example.net/./tilde'
+/usr/bin/borg init --encryption=repokey-blake2 "$repository"
+/usr/bin/borg key export "$repository" /root/tilde-borg-key-export
+```
+
+Store the export and the passphrase off the source host through a protected
+channel. Test access from a recovery machine. Do not initialize a repository
+that is already in use, and do not substitute Borg 2 commands or formats into a
+Borg 1 backup without planning that migration.
+
+## Configure borgmatic
+
+Review [config.yaml](../examples/borgmatic/config.yaml). Put the adjusted file
+in `/etc/borgmatic/config.yaml` with root ownership and mode 0600. It uses the
+flat borgmatic 2.x schema, not the old 1.5 `location/storage/retention` sections.
+Create the database-dump directory at mode 0700 before validation:
+
+```bash
+install -d -o root -g root -m 0700 /var/backups/tilde-databases
+/usr/bin/borgmatic config validate
+```
+
+Add existing service-state directories to `source_directories`. Keep the
+missing-source check enabled so a typo or missing mount fails visibly. If a
+required data mount can disappear while leaving an empty mountpoint behind,
+add a mount-identity check too: merely testing that a directory exists is not
+sufficient.
+
+The example keeps daily, weekly, and monthly restore points. Choose retention
+based on available storage and the longest period a bad change might go
+unnoticed. A live filesystem backup is not a simultaneous snapshot of every
+file. Quiesce services or use consistent snapshots where that matters.
+
+## Consistent databases
+
+Use borgmatic's supported database hooks for your installed version, or prepare
+logical backups before running it. Here is a small wrapper for a host with a
+local MariaDB instance. Save it as `/usr/local/sbin/backup-tilde`, root-owned and
+mode 0700, and adjust the database section if MariaDB is not part of your host:
+
+```bash
+#!/usr/bin/env bash
+set -euo pipefail
+export PATH=/usr/sbin:/usr/bin:/sbin:/bin
+umask 077
+
+dump_dir=/var/backups/tilde-databases
+install -d -m 0700 "$dump_dir"
+dump_file=$(mktemp "$dump_dir/.mariadb.XXXXXXXX.sql")
+trap 'rm -f -- "$dump_file"' EXIT
+
+mariadb-dump --all-databases --single-transaction --routines --events \
+ --triggers --hex-blob > "$dump_file"
+mv -f -- "$dump_file" "$dump_dir/mariadb.sql"
+
+/usr/bin/borgmatic create prune compact check --verbosity 1
+```
+
+This assumes a local root socket login or a separately protected client config;
+it does not embed a database password. `--single-transaction` gives a consistent
+view of transactional tables, not arbitrary nontransactional tables or concurrent
+schema changes. Check engines and use a suitable lock, snapshot, or maintenance
+window where needed. A failed dump must stop the backup rather than silently
+reuse yesterday's SQL.
+
+For SQLite, use its backup API or the CLI `.backup` operation while the app is
+running. Do not copy a database file while ignoring an active WAL. For other
+databases, use their supported consistent export/snapshot method.
+
+## Schedule and monitor
+
+Run the wrapper manually once and inspect the resulting archive. For a
+cron-based setup, a root crontab entry can be:
+
+```cron
+0 3 * * * /usr/bin/flock -n /run/tilde-backup.lock /usr/local/sbin/backup-tilde >> /var/log/tilde-backup.log 2>&1
+```
+
+Create the log at mode 0600 and configure root-only log rotation. This example
+runs at 03:00 in cron's configured timezone; record that timezone. Do not enable
+a second timer for the same job without intending two schedules. A skipped
+lock acquisition is not a successful backup and needs to be visible in monitoring.
+
+Alert when there is no successful archive within the expected interval, not
+only when the scheduler launches a command. Check off-host capacity, archive
+age, consistency checks, repository access, database exports, and restore tests.
+Store monitoring/notification credentials separately from the public docs.
+
+## Restore an account in isolation
+
+On a recovery machine or isolated directory, use the matching Borg version and
+repository key. With `BORG_PASSCOMMAND` set as above:
+
+```bash
+repository='ssh://backup@backup.example.net/./tilde'
+/usr/bin/borg list "$repository"
+install -d -m 0700 /root/tilde-restore-test
+cd /root/tilde-restore-test
+/usr/bin/borg extract --dry-run "$repository::ARCHIVE" home/member
+/usr/bin/borg extract "$repository::ARCHIVE" home/member
+```
+
+Replace `ARCHIVE` with the exact archive name returned by the list command.
+Check the stored path names first. Extract relative paths into the test
+directory; do not start a restore by extracting over `/home` or `/etc`.
+
+Verify file contents, private modes, ownership, symlinks, mail, public pages,
+and database exports. Preserve numeric UIDs/GIDs or deliberately map them before
+returning restored data to service. Reapply SELinux labels and re-create quota
+limits on the restored filesystem. Test login on an isolated host, not by
+announcing a restored production account before it has been checked.
+
+For a whole-host restore, reinstall a supported OS, restore account identity
+and data deliberately, restore databases using their matching tools, restore
+service secrets, then enable services in dependency order. Keep outbound mail,
+IRC bots, signup announcements, and scheduled jobs disabled until duplicate
+external actions have been ruled out. Change DNS or reopen public access only
+after the replacement has passed its service checks.
+
+Record the last successful restore date, what was restored, and what failed.
+A recovery key kept only inside the encrypted backup cannot recover that backup.
diff --git a/docs/community.md b/docs/community.md
new file mode 100644
index 0000000..0d7c20d
--- /dev/null
+++ b/docs/community.md
@@ -0,0 +1,119 @@
+# Website, signups, and community tools
+
+A working shell and a place to publish are enough to start. Add community tools
+when someone will maintain them. Each operator application needs its own code,
+service identity, writable data, update procedure, and backup entry.
+
+## Source repositories
+
+These public repos provide parts of the tilde.club setup. Review the code and
+its revision before deployment; a public checkout is not necessarily identical
+to a local production copy.
+
+| Component | Source |
+| --- | --- |
+| Website and wiki | [tildeclub/site](https://github.com/tildeclub/site) |
+| Home skeleton | [tildeclub/tilde.etcskel](https://github.com/tildeclub/tilde.etcskel) |
+| Account creation | [tildeclub/makeuser](https://github.com/tildeclub/makeuser); the collected live script has local integrations. |
+| First-login wizard | [tildeclub/welcome](https://github.com/tildeclub/welcome) |
+| Gopher daemon | [tildeclub/Fossor](https://github.com/tildeclub/Fossor) |
+| Gopher root content | [tildeclub/gopherhole](https://github.com/tildeclub/gopherhole) |
+| Gemini root content | [tildeclub/geminipage](https://github.com/tildeclub/geminipage) |
+| MOTD | [tildeclub/motd](https://github.com/tildeclub/motd) |
+| Terminal wiki helper | [tildeclub/wiki-cli](https://github.com/tildeclub/wiki-cli) |
+| Recent-page listing | [tildeclub/recently-updated](https://github.com/tildeclub/recently-updated) |
+| Botany | [tildeclub/botany](https://github.com/tildeclub/botany) |
+| Terminal blogging | [tildeclub/ttbp](https://github.com/tildeclub/ttbp) |
+| Forum client | [tildeclub/ttrv](https://github.com/tildeclub/ttrv) |
+| Helpdesk | [tildeclub/helpdesk](https://github.com/tildeclub/helpdesk) |
+| File sharing | [tildeclub/share](https://github.com/tildeclub/share) |
+
+## Deploy the website deliberately
+
+Stage `tildeclub/site` outside the web root, select and record a revision, then
+review domains, contact addresses, signup behavior, JSON data, helper scripts,
+and web dependencies. The collected host's main root is
+`/usr/share/nginx/html`; configure its own PHP handler before deploying a PHP
+front page. The static bootstrap example deliberately does not execute PHP.
+
+Keep `.git`, credentials, local environment files, Composer's development
+artifacts, and diagnostics out of the served copy. In particular, the reviewed
+site checkout has a signup `_probe.php` mail diagnostic; do not deploy that as
+a public endpoint. Install each application's declared dependencies using its
+lockfile in a build/staging location and check its supported PHP version.
+Do not infer a database requirement merely because MariaDB runs on the machine.
+
+Make code root-owned and grant the backend write access only to the files it
+must update. Avoid making the entire document root writable by nginx or giving
+an operator application the same writable identity as every member's PHP.
+Test error pages, forms, mail submission, and file ownership on the target host.
+
+The wiki has its own `source/` Markdown and Makefile in the site repo. Review
+that build file, install its declared tools, and build in staging. Check that
+generated pages, local links, feeds, and navigation are present before syncing
+the output. A successful checkout is not a completed wiki deployment.
+
+## Signup requests are data
+
+The reviewed website sends a request to the operator and records pending
+usernames and a suggested `makeuser` command in files beneath `/var`. The
+collected account script later removes the processed pending entry. Neither
+step is a transaction across the Unix account, mail, ZNC, helpdesk, and signup
+queue.
+
+For a new host, reserve service/admin names, validate the username and contact
+address, parse the supplied SSH public key with OpenSSH, and review the request.
+A useful key-file check is:
+
+```bash
+ssh-keygen -lf /root/pending/member.pub
+```
+
+Keep that pending directory private. Construct privileged invocations from
+validated arguments or structured records. Do not paste an emailed shell
+command into a root terminal without inspecting its quoting and values, and do
+not execute a web-writable queue as a shell script.
+
+The public form should handle duplicate requests, concurrent writes, invalid
+keys, rate limiting, CSRF where applicable, and mail failures. Trust forwarded
+client-address headers only from a configured trusted proxy. Keep request data
+and contact addresses out of the document root and define their retention.
+Running a signup form is not a reason to give the web process passwordless sudo.
+
+## First contact and support
+
+Give the member a short welcome with:
+
+- The hostname, username, SSH instructions, and host-key verification path.
+- Where their pages live and which publishing services are enabled.
+- First-login/MFA instructions and a recovery contact.
+- Chat, mail, quotas, rules, and how to request help or packages.
+- What account data is public and how to request removal.
+
+The existing script emails a generated password and copies it to local mailboxes.
+That is its recorded behavior, not a requirement for a new installation. Choose
+an enrollment process consistent with your credential and mailbox-retention
+policy, and ensure temporary credentials are changed before normal use.
+
+Keep MOTD concise. Put longer help in the wiki or a local `help` command, and
+publish an independent status/contact path for when the host is unavailable.
+Respect the difference between a maintenance notice and a public announcement
+of a new member's account; make the announcement policy clear.
+
+## Optional apps
+
+Install apps under separate operator-managed paths and identities. Record the
+revision and dependency lockfile, local socket or port, reverse-proxy route,
+backup data, and a restart/restore test. If an app requires secrets, load them
+from protected config or the service environment, never from a public checkout.
+
+For cgit, only expose repositories deliberately marked public, validate the user
+and repository mapping, and test path traversal. For Botany and similar shared
+apps, test whose files the backend can read and write. Keep IRC bot rate limits
+and reconnection behavior within the network's policy. Treat browser terminals
+and relay sockets as authenticated services, not harmless static content.
+
+The older utilities retained under `api/` and `pages/` are not used as an
+automatic install system. Their current limitations and safer deployment rules
+are documented in [the API notes](../api/README.org) and
+[connection-list notes](../pages/list-html-connections/README.md).
diff --git a/docs/current-system.md b/docs/current-system.md
new file mode 100644
index 0000000..c95619c
--- /dev/null
+++ b/docs/current-system.md
@@ -0,0 +1,76 @@
+# Current system
+
+This records the operator's inventories and selected configs collected on
+October 22, 2026. It describes the existing shell host. The
+[installation guide](how-to-set-up-a-tilde.md) and [examples](../examples/README.md)
+make separate choices for a new host; they do not silently claim those choices
+are already deployed here.
+
+## Machine
+
+- Fedora Linux 43, x86-64, 8 logical CPUs, about 29 GiB usable RAM.
+- About 1 TiB on the root XFS filesystem; `/home` and `/var` are on that mount.
+- Root mount options include user and group quotas.
+- SELinux disabled on the recorded host; CSF is the active firewall manager.
+
+These are the resources on the existing machine, not minimum requirements.
+Exact user inventories, addresses, private keys, credentials, and raw config
+dumps are deliberately not part of this public record.
+
+## Services
+
+| Purpose | Confirmed configuration |
+| --- | --- |
+| Shell access | OpenSSH main service; separate restricted legacy service on 2222. |
+| Authentication | Key login, or PAM password/TOTP path; the collected main config does not require both paths together. |
+| Onboarding | Local `makeuser`, Ruby `firstlogin`, skeleton marker, Google Authenticator wrapper. |
+| Web | nginx, root `/usr/share/nginx/html`, `/~USER/` and `/u/USER/` publishing. |
+| PHP | Shared `php-user:club` pool for user pages; `nginx:nginx` pool for main site applications. |
+| CGI | fcgiwrap instance with a local override running as `cgiwrap`. |
+| Mail | Postfix 3.10.14, Dovecot/Pigeonhole 2.4.5, Maildir at `~/.mail`. |
+| Mail filtering | OpenDKIM and Rspamd on configured local milter endpoints; pyspf-milter also running. |
+| Webmail | nginx vhost points to SnappyMail. |
+| Usenet | INN 2.7.3 running on the shell host. |
+| Gopher | Fossor socket activation; dedicated `gopher` service identity. |
+| Gemini | gemserv, running from `/srv/gemini/gemserv`, as `gemini`. |
+| Finger | Local Python `fingerd.py`, service identity `efingerd`. |
+| IRC support | oidentd and ZNC running. Network-server location/daemon was not established by this collection. |
+| Database/cache | MariaDB and Valkey running. |
+| Accounting | psacct active. |
+| Certificates | Custom twice-daily standalone Certbot timer that stops/starts nginx. |
+| Backups | Root cron at 03:00, explicit `/usr/bin/borgmatic` 2.1.9 with Borg 1.4.5. |
+
+The compiled services and private application integration code need their own
+version/update records. Installed RPMs are not an installation manifest: the
+host has accumulated optional tools and old builds over time.
+
+## Details worth keeping straight
+
+`/usr/local/bin/borgmatic` is a different, older installation (1.5.21). Manual
+invocation through PATH is not necessarily the program run by the backup job.
+
+`/home` is not an independent mount here. Quota commands need the containing
+mountpoint `/`; the collector's `/home` quota query printed an error.
+
+The per-user FPM pool generator exists, but the collected main nginx route uses
+the shared socket. A generated per-user pool alone does not route a member's
+requests to it.
+
+The supplied main SSH config does not include `sshd_config.d`; collecting those
+files does not establish that the daemon reads them. The effective global
+`AuthenticationMethods` was `any`. The SSH chapter explains the key and PAM
+paths, with an explicit MFA example for a new installation.
+
+The aggregate `user.slice` reported no finite memory or task limit. Individual
+user slices and PAM limits were not comprehensively collected, so no complete
+production resource policy is claimed here.
+
+The Gopher service reads options from `/etc/default/fossor` and
+`/etc/sysconfig/fossor`; those files were not supplied. Gemini's service uses
+`/srv/gemini/gemserv/config.toml`, which was also not supplied. The publishing
+chapter identifies the runtime layout and points to version-matched upstream
+configuration rather than inventing live option values.
+
+The backup repository, complete source list, retention, and restore history
+were not supplied. The backup chapter's example is explicitly separate from the
+confirmed scheduler and executable versions.
diff --git a/docs/etc-skel-permissions.md b/docs/etc-skel-permissions.md
index 93d52e4..0c08a51 100644
--- a/docs/etc-skel-permissions.md
+++ b/docs/etc-skel-permissions.md
@@ -1,99 +1,90 @@
-The [tilde.etcskel repository](https://github.com/tildeclub/tilde.etcskel) contains the contents of the `/etc/skel/` directory, but since git repositories can't reproduce permissions on the files, here are the current permissions for those contents:
+# Home skeleton and permissions
+`useradd --create-home` copies `/etc/skel` into a new home. Get it right before
+creating accounts. Changes to the skeleton do not update existing homes.
+
+The reference source is [tilde.etcskel](https://github.com/tildeclub/tilde.etcskel).
+Review its startup commands, domains, IRC settings, and first-login dependencies
+before deploying it. Stage changes outside `/etc/skel`; do not clone over a live
+skeleton or copy a repository's `.git` directory into every new account.
+
+## Minimal new-host skeleton
+
+Keep the distribution's shell files, then add the public page and an empty
+SSH key file. On a new skeleton only:
+
+```bash
+install -d -o root -g root -m 0700 /etc/skel/.ssh
+install -o root -g root -m 0600 /dev/null /etc/skel/.ssh/authorized_keys
+install -d -o root -g root -m 0755 /etc/skel/public_html
```
-root@tilde /e/skel (master)# ls -alR
-.:
-total 63
-drwxr-xr-x. 10 root root 4096 Jul 17 2024 ./
-drwxr-xr-x. 164 root root 12288 Jan 27 19:40 ../
--rw-r--r--. 1 root root 29 Sep 15 2019 .bash_logout
--rw-r--r-- 1 root root 144 Aug 12 00:00 .bash_profile
--rw-r--r-- 1 root root 3272 Dec 13 2021 .bashrc
-drwxr-xr-x 3 root root 303 Jan 27 19:43 .byobu/
-lrwxrwxrwx 1 root root 25 Jul 17 2024 .dovecot.sieve -> sieve/rainloop.user.sieve
--rw-r--r--. 1 root root 500 Sep 15 2019 .emacs
-drwxr-xr-x. 2 root root 35 Mar 27 2023 .irssi/
--rw-r--r--. 1 root root 11 Sep 15 2019 .logout
--rw-r--r-- 1 root root 124 Jul 18 2024 .mkshrc
--rw-r--r-- 1 root root 0 Mar 27 2020 .new_user
--rw-r--r-- 1 root root 816 Mar 27 2020 .profile
-drwxr-xr-x 2 root root 23 Nov 26 2021 public_gemini/
-drwxr-xr-x. 2 root root 23 Jan 27 19:43 public_gopher/
-drwxr-xr-x. 2 root root 24 Mar 27 2023 public_html/
--rw-r--r-- 1 root root 790 Dec 23 2022 README.md
--rw-r--r--. 1 root root 12 Sep 15 2019 .scrollzrc
-drwxr-xr-x 3 root root 44 Jul 17 2024 sieve/
-drwx------. 2 root root 29 Sep 15 2019 .ssh/
--rw-r--r-- 1 root root 299 Jul 20 2024 .zprofile
--rw-r--r-- 1 root root 658 Jul 20 2024 .zshrc
-./.byobu:
-total 56
-drwxr-xr-x 3 root root 303 Jan 27 19:43 ./
-drwxr-xr-x. 10 root root 4096 Jul 17 2024 ../
--rw-r--r-- 1 root root 19 Aug 23 2018 backend
-drwxr-xr-x 2 root root 6 Aug 23 2018 bin/
--rw-r--r-- 1 root root 38 Aug 23 2018 color
--rw-r--r-- 1 root root 96 Aug 23 2018 color.tmux
--rw-r--r-- 1 root root 45 Aug 23 2018 datetime.tmux
--rw-r--r-- 1 root root 52 Aug 23 2018 keybindings
--rw-r--r-- 1 root root 73 Aug 23 2018 keybindings.tmux
--rw-r--r-- 1 root root 0 Aug 23 2018 .metadata_available
--rw-r--r-- 1 root root 49 Aug 23 2018 profile
--rw-r--r-- 1 root root 47 Aug 23 2018 profile.tmux
--rw-r--r-- 1 root root 95 Aug 24 2018 prompt
--rw-r--r-- 1 root root 0 Aug 23 2018 .screenrc
--rw-r--r-- 1 root root 2833 Aug 23 2018 status
--rw-r--r-- 1 root root 2558 Oct 8 2019 statusrc
--rw-r--r-- 1 root root 103 Jan 27 19:43 .tmux.conf
--rw-r--r-- 1 root root 0 Aug 23 2018 windows
--rw-r--r-- 1 root root 8 Aug 23 2018 windows.tmux
+The `install ... /dev/null` command empties its destination. Do not use it on
+an existing account's key file. Add a starter `index.html` at mode 0644, for
+example:
-./.byobu/bin:
-total 0
-drwxr-xr-x 2 root root 6 Aug 23 2018 ./
-drwxr-xr-x 3 root root 303 Jan 27 19:43 ../
+```html
+
+
+
+
+
Edit ~/public_html/index.html to make this page your own.
+ +``` -./.irssi: -total 16 -drwxr-xr-x. 2 root root 35 Mar 27 2023 ./ -drwxr-xr-x. 10 root root 4096 Jul 17 2024 ../ --rw-r--r-- 1 root root 4977 Mar 27 2023 config --rw-r--r-- 1 root root 15 Mar 27 2023 startup +Add a short README covering SSH, editing the page, mail if offered, chat, +acceptable use, quotas, and how to reach an operator. Use your own domain and +contact details. Keep private defaults out of publicly served directories. -./public_gemini: -total 8 -drwxr-xr-x 2 root root 23 Nov 26 2021 ./ -drwxr-xr-x. 10 root root 4096 Jul 17 2024 ../ --rw-r--r-- 1 root root 96 Nov 26 2021 index.gmi +## Modes -./public_gopher: -total 8 -drwxr-xr-x. 2 root root 23 Jan 27 19:43 ./ -drwxr-xr-x. 10 root root 4096 Jul 17 2024 ../ --rw-r--r-- 1 root root 149 Jan 27 19:43 gophermap +| Path | Mode | Owner in the skeleton | Purpose | +| --- | --- | --- | --- | +| `/etc/skel` | 0755 | root:root | Template directory. | +| `.ssh` | 0700 | root:root | Private SSH configuration directory. | +| `.ssh/authorized_keys` | 0600 | root:root | Empty placeholder; never a shared key. | +| `public_html` | 0755 | root:root | Public web directory. | +| `public_html/index.html` | 0644 | root:root | Public starter page. | +| `public_gopher`, `public_gemini` | 0755 | root:root | Only if those services are offered. | +| Public text and menu files | 0644 | root:root | Readable publishing content. | +| Private mail/sieve directories | 0700 where appropriate | root:root | Not web content. | +| Private sieve files | 0600 | root:root | User-specific filtering state. | -./public_html: -total 8 -drwxr-xr-x. 2 root root 24 Mar 27 2023 ./ -drwxr-xr-x. 10 root root 4096 Jul 17 2024 ../ --rw-r--r-- 1 root root 2145 Mar 27 2023 index.html +`useradd` assigns the copied files to the new account. Check that ownership +rather than recursively changing an established home. Git records executable +bits but not arbitrary file modes or ownership, so a checkout alone is not a +permissions deployment procedure. -./sieve: -total 4 -drwxr-xr-x 3 root root 44 Jul 17 2024 ./ -drwxr-xr-x. 10 root root 4096 Jul 17 2024 ../ --rw------- 1 root root 0 Jul 17 2024 rainloop.user.sieve -drwx------ 2 root root 6 Jul 17 2024 tmp/ +The collected production skeleton also has Byobu, Irssi, `.gnupg`, Sieve files, +a `.dovecot.sieve` symlink, and a `.new_user` marker. Its `.ssh` and `.gnupg` +directories are 0700, the empty key file and personal Sieve file are 0600, +public directories are 0755, and public starter files are 0644. A symlink must +be checked at its destination as well; do not use recursive chmod across it. -./sieve/tmp: -total 0 -drwx------ 2 root root 6 Jul 17 2024 ./ -drwxr-xr-x 3 root root 44 Jul 17 2024 ../ +## First-login hook -./.ssh: -total 4 -drwx------. 2 root root 29 Sep 15 2019 ./ -drwxr-xr-x. 10 root root 4096 Jul 17 2024 ../ --rw-------. 1 root root 0 Sep 15 2019 authorized_keys +On the collected host, `.bash_profile` sources `.bashrc`, and interactive Bash +runs `firstlogin` when `.new_user` exists. Noninteractive Bash returns before +that hook. The collected Zsh startup files do not contain the same hook. +The Ruby wizard removes the marker at the end. Test interruption, failure, and +re-entry. Keep it out of noninteractive SCP/SFTP or command sessions, and do +not rely on the marker to enforce MFA or block an account. + +## Test a fresh copy + +Create a disposable account with the real account-creation method. Check: + +```bash +namei -l /home/member/.ssh/authorized_keys +namei -l /home/member/public_html/index.html +stat -c '%a %U:%G %n' /home/member /home/member/.ssh \ + /home/member/.ssh/authorized_keys /home/member/public_html +``` + +Use the home traversal mode explained in [the setup guide](how-to-set-up-a-tilde.md). +Check SSH and web access as the actual member and service users, including +SELinux labels when enforcing. Never repair a 403 by making the entire home +readable to everyone. diff --git a/docs/how-to-set-up-a-tilde.md b/docs/how-to-set-up-a-tilde.md index cf5f118..193c0bd 100644 --- a/docs/how-to-set-up-a-tilde.md +++ b/docs/how-to-set-up-a-tilde.md @@ -1,575 +1,271 @@ -## Build your own tilde-style server (beginner-friendly, step-by-step) +# Build a tilde-style machine -This guide is intentionally detailed for first-time operators. +A tilde is a shared Unix machine where people get a shell account and space to +publish things. The machine needs an operator, a backup, and some clear rules. +It does not need every service on the existing tilde.club host on the first day. -If you have never run a server before, follow this in order and do not skip ahead. +This guide uses Fedora Server and nginx. The production reference was collected +from tilde.club on October 22, 2026, running Fedora 43. Use a supported release +and its maintained packages. See [the system record](current-system.md) for +what was actually observed; examples below use `tilde.example.net` and a test +account named `member`. ---- +The commands in this guide are Bash. If your interactive shell is Fish, start +`bash` before using examples that assign variables or use Bash loops. Run +administrative commands from a root shell obtained with `sudo -i`, unless the +text says to run them on your own computer or as a regular user. -## What you are building +## 1. Plan the machine -A "tilde-style" host usually provides: +For a small initial group, 2 CPUs, 4 GiB RAM, and 60 GiB of SSD storage are a +reasonable starting allocation. Compilers, persistent bots, mail, and games can +change that quickly. Monitor actual use before increasing limits or buying more +capacity. The existing server's size is not a minimum requirement. -- A Linux shell account for each user -- Personal web publishing from `~/public_html` -- Shared Unix tools for learning, writing, coding, and socializing -- Community norms and moderation +You need: -In short: a small, friendly, multi-user Unix community. +- A domain you control and a host with a stable public address. +- Provider console or recovery access independent of SSH. +- An off-host backup destination and a place to keep recovery keys. +- An admin account, a second person or recovery process for emergencies, and a + published way to contact the operators. +- A decision about signups, acceptable use, account removal, and disk limits. ---- +Use separate service accounts for the web server, mail, Gopher, and other +services. Ordinary shell accounts do not belong in `wheel` or a container-engine +administration group. Shell access lets people run programs: PHP restrictions +and a blocked SSH forwarding option do not turn a shell account into a sandbox. -## 0) Before you touch a server +## 2. Install and update Fedora -### 0.1 Buy or prepare these things first - -1. A domain name (example: `domain.club`) -2. One Linux server (VPS is fine) -3. SSH client on your laptop -4. A text editor you can use comfortably - -### 0.2 Keep this safety rule in mind - -Never close your current SSH session until you have confirmed a **new** SSH session works with your latest config changes. - -This one habit prevents most accidental lockouts. - ---- - -## 1) Provision your Linux server - -You can use AWS, Hetzner, Linode, DigitalOcean, or any other provider. - -### Recommended minimum for a small starter community - -- 2 vCPU -- 4 GB RAM -- 40 GB SSD -- Ubuntu LTS or Debian stable (easiest for beginners) - -### 1.1 Point DNS at your server - -At your DNS provider: - -- Create an `A` record for `domain.club` -> your server IPv4 -- Optionally create `AAAA` for IPv6 - -DNS can take time to propagate. - -### 1.2 First login - -From your local machine: +Install Fedora Server with SSH access and time synchronization enabled. Keep +SELinux enforcing on a new installation. The production host currently has it +disabled; that is not a prerequisite for running a tilde. ```bash -ssh root@domain.club +dnf upgrade --refresh +dnf install sudo openssh-server git curl rsync tmux vim-enhanced nano jq \ + acl ripgrep policycoreutils-python-utils firewalld chrony +hostnamectl set-hostname tilde.example.net +systemctl enable --now sshd chronyd ``` -If your provider uses a default admin user (for example `ubuntu`), use that user and `sudo`. +Reboot if the update requires it and verify a fresh login. Record the OS version +and package sources. Do not add random repositories to recover an old program +name from a 2014 package list. ---- +## 3. Set DNS and the firewall -## 2) Base system setup (packages, hostname, firewall) +Create an A record for `tilde.example.net`. Add an AAAA record only if IPv6 works +through the host and provider firewalls. Test both address families from outside +the server. Reverse DNS is especially important if you plan to send mail directly. -This section includes both Debian/Ubuntu and Red Hat-family commands. - -### 2.1 Update the system - -**Debian / Ubuntu** +On a new host using firewalld, first check which zone owns the network interface: ```bash -apt update -apt -y upgrade +firewall-cmd --get-active-zones ``` -**RHEL / Rocky / Alma / Fedora** +In the example below, replace `public` with that zone. Confirm any custom SSH +port is allowed before changing a firewall remotely. ```bash -dnf -y upgrade -``` - -### 2.2 Install baseline tools - -**Debian / Ubuntu** - -```bash -apt -y install sudo git curl wget rsync tmux htop vim nano tree jq ufw -``` - -**RHEL / Rocky / Alma / Fedora** - -```bash -dnf -y install sudo git curl wget rsync tmux htop vim nano tree jq -``` - -### 2.3 Set hostname - -```bash -hostnamectl set-hostname domain.club -``` - -Check: - -```bash -hostnamectl -``` - -### 2.4 Configure a basic firewall - -If using `ufw` (common on Ubuntu): - -```bash -ufw default deny incoming -ufw default allow outgoing -ufw allow 22/tcp -ufw allow 80/tcp -ufw allow 443/tcp -ufw enable -ufw status verbose -``` - -If using `firewalld` (common on RHEL-family): - -```bash -systemctl enable --now firewalld -firewall-cmd --permanent --add-service=ssh -firewall-cmd --permanent --add-service=http -firewall-cmd --permanent --add-service=https +systemctl enable firewalld +firewall-cmd --permanent --zone=public --add-service=ssh +firewall-cmd --permanent --zone=public --add-service=http +firewall-cmd --permanent --zone=public --add-service=https firewall-cmd --reload -firewall-cmd --list-services +firewall-cmd --zone=public --list-all ``` ---- +These commands assume firewalld is already running. If it is stopped, prepare +the persistent zone with `firewall-offline-cmd` before starting it, including +your actual SSH port and the intended network-zone assignment. Then start it +and verify a new connection. Provider firewalls are independent. -## 3) Create a non-root admin account +The existing tilde.club host uses CSF. Do not enable a second firewall manager +on that machine as part of following this guide. See [ports and firewall +policy](shellserver.md#ports-and-firewall-policy) before adding optional services. -Running daily admin tasks as `root` is risky. +## 4. Establish an admin account -### 3.1 Create admin user +Create a private primary group and home. Set a password for sudo, then add the +account to `wheel`: ```bash -adduser admin +useradd --create-home --user-group --shell /bin/bash operator +passwd operator +usermod --append --groups wheel operator +install -d -o operator -g operator -m 0700 /home/operator/.ssh ``` -On some distros: +Copy the admin's **public** key to `/home/operator/.ssh/authorized_keys`, without +replacing an existing key file. Set its owner to `operator:operator` and mode +0600, and run `restorecon -RF /home/operator/.ssh` if SELinux is enabled. + +From a second terminal on your own computer, verify key login and `sudo -v`. +Keep the first connection open. Do not disable the only working login method +before its replacement has been tested. + +Follow [SSH access and MFA](ssh.md) to make the key-only baseline explicit. +That chapter also covers enrollment before enforcing MFA and a separate legacy +SSH service. Do not copy the production host's account-specific exceptions. + +## 5. Build the home skeleton + +Follow [the skeleton instructions](etc-skel-permissions.md). At a minimum, create +an empty private SSH directory, a public HTML directory, and a useful starter +page. Do this before creating the test account. + +The public [tilde.etcskel repository](https://github.com/tildeclub/tilde.etcskel) +is a reference to review, not a directory to copy over your live `/etc/skel` +without checking it. It contains host-specific defaults and first-login hooks. + +## 6. Install the web service ```bash -useradd -m -s /bin/bash admin -passwd admin +dnf install nginx +install -d -o root -g root -m 0755 /usr/share/nginx/html ``` -### 3.2 Give sudo privileges +Use [web publishing](web.md) and the +[static nginx example](../examples/nginx/tilde-static.conf). Start with static +pages. That makes it possible to test path mapping and private-file access +before adding PHP or CGI. -**Debian / Ubuntu** +The example belongs in `/etc/nginx/conf.d/tilde.conf`, which Fedora's normal +nginx configuration includes inside `http`. Check that include on your machine. +Remove or adjust a conflicting packaged default server block; do not keep two +servers both claiming to be the default on the same address and port. + +Replace the example domain, then validate and start the service: ```bash -usermod -aG sudo admin +nginx -t +systemctl enable --now nginx ``` -**RHEL-family** +The example is HTTP-only so it can be used before obtaining a certificate. Set +up TLS before accepting account passwords or enabling web applications. + +## 7. Create one test member + +The following is the independent Unix-account baseline. It does not announce an +account, send welcome mail, create ZNC accounts, or update the production signup +queue. Those are integrations described in [accounts](accounts.md). ```bash -usermod -aG wheel admin +useradd --create-home --user-group --shell /bin/bash member +passwd member +chmod 0711 /home/member +install -d -o member -g member -m 0700 /home/member/.ssh +install -d -o member -g member -m 0755 /home/member/public_html ``` -### 3.3 Test sudo +`useradd` should copy the empty key file from the prepared skeleton. Put the +member's public key there, then set its owner to `member:member` and mode 0600. +Never put private keys in the skeleton or generate a private key for a member +and email it to them. + +Keep the home at 0711 for this layout. That permits traversal to public content +without allowing a directory listing of the home. Private directories and files +still need their own modes: use 0700 for private directories and 0600 for private +files. A 0711 home alone does not hide a readable file at a known path. + +With SELinux enabled, apply and test the publishing policy described in the web +chapter, then restore the account's labels. Do not recursively make a home +world-readable just to fix a 403 response. + +From your own computer, test SSH, an unrelated key that should fail, and: ```bash -su - admin -sudo whoami +curl --fail http://tilde.example.net/~member/ +curl --fail http://tilde.example.net/u/member/ ``` -Expected output: `root` +Create a second disposable member and run the cross-account checks in the web +chapter before enabling executable user content. ---- +## 8. Add TLS -## 4) SSH hardening (safe order, no lockouts) - -Do this carefully. - -### 4.1 Set up admin SSH key directory and file +The static example exposes ACME challenges beneath `/var/lib/letsencrypt`. +Create that directory and install the packaged Certbot client: ```bash -install -d -m 700 /home/admin/.ssh -install -m 600 /dev/null /home/admin/.ssh/authorized_keys -chown -R admin:admin /home/admin/.ssh +dnf install certbot +install -d -o root -g root -m 0755 /var/lib/letsencrypt/.well-known/acme-challenge +certbot certonly --webroot -w /var/lib/letsencrypt -d tilde.example.net ``` -### 4.2 Add your public key +Verify that the returned certificate paths exist. Add the HTTPS server block +from the web chapter, run `nginx -t`, and reload nginx. Install a deploy hook to +validate and reload nginx when a renewed certificate is deployed. If the same +certificate is used by mail, reload those consumers as well. Confirm the +packaged renewal timer is enabled and perform `certbot renew --dry-run`. -On your local machine, show your public key: +This webroot setup can renew without stopping nginx. The existing server uses +a separate standalone renewal service that stops and starts nginx; its details +are recorded in the web chapter rather than imposed on a new host. + +## 9. Give people useful tools + +Install a small set first; expand it in response to actual requests: ```bash -cat ~/.ssh/id_ed25519.pub +dnf install fish zsh emacs irssi weechat alpine lynx links \ + git gcc make python3 ruby perl figlet ``` -Copy that line and paste it into: +Use `dnf info NAME` before adding optional packages. Packages, module streams, +and third-party builds change. Keep a record of manual installations and their +update procedure. Do not chmod system compilers to root-only as a substitute +for resource management; programming is part of the point of the machine. -`/home/admin/.ssh/authorized_keys` +Choose the shells you support in `/etc/shells`, and make sure each has working +startup files. Add [first-login setup](accounts.md#first-login-wizard) only after +its dependencies and exit behavior have been tested with those shells. -Example on server: +## 10. Set quotas and resource limits -```bash -printf '%s\n' 'ssh-ed25519 AAAA... your-key-comment' >> /home/admin/.ssh/authorized_keys -chown admin:admin /home/admin/.ssh/authorized_keys -chmod 600 /home/admin/.ssh/authorized_keys -``` +Use [the shell operations chapter](shellserver.md). For XFS, discover the mount +containing `/home` before running quota commands. On the collected machine that +mount is `/`, not `/home`. -### 4.3 Keep passwords ON while you test keys +A disk quota does not limit CPU or processes. Set and verify those policies +separately, with enough room for normal compiles, editors, mail, and chat clients. -Edit `/etc/ssh/sshd_config`: +## 11. Back up and restore before opening signups -```text -PermitRootLogin no -PubkeyAuthentication yes -PasswordAuthentication yes -ChallengeResponseAuthentication no -UsePAM yes -``` +Follow [backup and recovery](backups.md). Include account data, mail, service +configs, local programs, web applications, and consistent database dumps. Keep +the repository and its recovery keys off the host. Restore a test account into +an isolated directory and verify the result. -Reload: +A successful backup job alone is not a completed restore test. -```bash -systemctl reload sshd -``` +## 12. Add the services you intend to operate -From a **second local terminal**, test login: +- [Mail](mail.md): local delivery, IMAP, submission, DNS, filtering, and abuse checks. +- [IRC](ircserver.md): clients, persistent sessions, ident, ZNC, and optional network hosting. +- [Gopher and Gemini](publishing.md): separate public directories and service users. +- [Usenet](usenet.md): local groups before opening reader access or negotiating peers. +- [Website and community tools](community.md): signup handling, the wiki, and optional apps. -```bash -ssh admin@domain.club -``` +Each service adds an update job, a backup requirement, and a support obligation. +Write down who owns it and how to stop or restore it. -If this fails, fix it now. Do **not** continue. +## Before accepting members -### 4.4 Disable password auth only after key login works +- Test admin and member login from outside the host, including IPv6 if published. +- Verify MFA enrollment, account recovery, and any allowed automation accounts. +- Test static publishing, private-file denial, and executable content separately. +- Confirm disk quotas and process/memory limits with the test accounts. +- Complete an off-host backup and isolated restore. +- Publish rules, a contact address, maintenance notices, and an account-removal policy. +- Review every enabled network listener against the intended firewall policy. +- Remove diagnostic PHP pages, temporary archives, and test credentials from web roots. -Edit `/etc/ssh/sshd_config`: - -```text -PasswordAuthentication no -``` - -Reload and test again from a second terminal: - -```bash -systemctl reload sshd -ssh admin@domain.club -``` - -Only after success should you end your old session. - ---- - -## 5) Install and configure Apache for user pages - -This is the baseline many tilde hosts use for `~username` pages. - -### 5.1 Install Apache - -**Debian / Ubuntu** - -```bash -apt -y install apache2 -systemctl enable --now apache2 -``` - -**RHEL-family** - -```bash -dnf -y install httpd -systemctl enable --now httpd -``` - -Check service status: - -```bash -systemctl status apache2 --no-pager -# or -systemctl status httpd --no-pager -``` - -### 5.2 Enable user directories (`~username` URLs) - -#### Debian / Ubuntu - -```bash -a2enmod userdir -systemctl restart apache2 -``` - -By default, this serves `/home/USERNAME/public_html` as: - -`http://domain.club/~USERNAME/` - -#### RHEL-family - -Edit Apache config (often `/etc/httpd/conf/httpd.conf`) and ensure: - -```apache -UserDir public_html - -Edit this file to publish your page.
- - -HTML -``` - -```bash -cat > /etc/skel/README-FIRST.txt <<'TXT' -Welcome to the server. - -Useful first commands: -- pwd -- ls -la -- nano ~/public_html/index.html - -Your web page lives at: -http://domain.club/~YOURUSERNAME/ -TXT -``` - -See also: `docs/etc-skel-permissions.md`. - ---- - -## 7) Install baseline user tools - -Give new users a capable, friendly default toolbox. - -**Debian / Ubuntu** - -```bash -apt -y install \ - zsh fish \ - emacs vim nano \ - irssi weechat \ - mutt alpine \ - lynx w3m links \ - git build-essential python3 nodejs npm -``` - -**RHEL-family** - -```bash -dnf -y install \ - zsh fish \ - emacs vim nano \ - irssi weechat \ - mutt alpine \ - lynx links \ - git gcc make python3 nodejs npm -``` - -Add or remove packages based on your community. - ---- - -## 8) Add users safely and consistently - -Use a repeatable checklist every time. - -### 8.1 Account creation checklist - -```bash -useradd -m -s /bin/bash USERNAME -passwd -l USERNAME -install -d -m 700 /home/USERNAME/.ssh -install -m 600 /dev/null /home/USERNAME/.ssh/authorized_keys -install -d -m 755 /home/USERNAME/public_html -chown -R USERNAME:USERNAME /home/USERNAME -``` - -### 8.2 Add the user's public key - -```bash -printf '%s\n' 'ssh-ed25519 AAAA... user@device' >> /home/USERNAME/.ssh/authorized_keys -chown USERNAME:USERNAME /home/USERNAME/.ssh/authorized_keys -chmod 600 /home/USERNAME/.ssh/authorized_keys -``` - -### 8.3 Verify login and web publishing - -```bash -ssh USERNAME@domain.club -curl -I http://domain.club/~USERNAME/ -``` - ---- - -## 9) Add "tilde functionality" in manageable layers - -Do not launch everything on day one. - -### Layer A: personal publishing - -- User web pages in `public_html` -- Basic HTML templates -- Optional Gemini and Gopher directories - -### Layer B: communication - -- IRC client docs -- Local mail (postfix + local delivery) -- Server bulletin/MOTD updates - -### Layer C: collaboration - -- Shared Git repos -- Local pastebin/snippet service -- Community docs/wiki process - -### Layer D: culture and learning - -- New-user orientation checklist -- Mentoring or office-hours in chat -- Monthly "show your tilde" events - ---- - -## 10) Operations, backup, and recovery - -### 10.1 Back up the important data - -At minimum, back up: - -- `/home` -- `/etc` -- Web server config (`/etc/apache2` or `/etc/httpd`) -- Mail config if used - -### 10.2 Example nightly backup script - -Create `/usr/local/sbin/backup-tilde.sh`: - -```bash -#!/usr/bin/env bash -set -euo pipefail - -DEST=/var/backups/tilde -DATE=$(date +%F) -mkdir -p "$DEST/$DATE" - -tar -czf "$DEST/$DATE/home.tgz" /home -tar -czf "$DEST/$DATE/etc.tgz" /etc -``` - -Make executable: - -```bash -chmod 700 /usr/local/sbin/backup-tilde.sh -``` - -Run once manually: - -```bash -/usr/local/sbin/backup-tilde.sh -``` - -Then automate with cron or systemd timers. - -### 10.3 Test restore (critical) - -A backup is not real until you test restoring at least one file. - ---- - -## 11) First-week launch checklist - -- [ ] DNS points to server -- [ ] Firewall allows only intended ports -- [ ] Admin key login works from a second terminal -- [ ] Password auth disabled only after key validation -- [ ] Apache running and `~testuser` page reachable -- [ ] `/etc/skel` tested by creating a new account -- [ ] Backup job ran and one restore test passed -- [ ] Rules/moderation/contact info published -- [ ] At least one backup admin has emergency access - ---- - -## 12) Where to continue in this repository - -- `docs/shellserver.md` for shell host operational notes -- `docs/etc-skel-permissions.md` for current skeleton permissions -- `docs/ssh.md` for SSH key onboarding details -- `docs/server.org` for historical package/setup notes +Keep your local configuration changes under version control without secrets. +Record the version or commit used for every manually installed service. The +[upstream references](sources.md) explain the software-specific behavior behind +these examples. diff --git a/docs/ircserver-connections.md b/docs/ircserver-connections.md index 53debcc..e6edb4b 100644 --- a/docs/ircserver-connections.md +++ b/docs/ircserver-connections.md @@ -1,7 +1,22 @@ -### Newnet IRC Network +# Connecting to IRC -## How to connect +The endpoint recorded for NewNet in this repo is: -You can connect to irc.newnet.net on port 6697 with ssl +| Setting | Value | +| --- | --- | +| Server | `irc.newnet.net` | +| TLS port | `6697` | +| Browser client |-mkdir -m 700 -p ~/.ssh # Create your .ssh directory -ssh-keygen -t rsa -b 2048 -f ~/.ssh/tilde.club # Create your keys --1. The `ssh-keygen` program will generate two new keys (private and public), and prompt you for a password. Please set a good password (at least 9 characters, and not just letters or numbers), and make a note of it somewhere safe. -1. Now, in **Terminal**, type: `open ~/.ssh` to open a Finder window showing your `.ssh` directory. -1. Take the `tilde.club.pub` file and attach it to an email to ford@ (our friendly host). **Do not attach the file `tilde.club`** -- that is your *private* key, which you should never ever **ever** share with anyone. +```bash +ssh-keygen -t ed25519 -a 64 -f ~/.ssh/tilde_ed25519 +``` -### Using your key pair +Use a passphrase. Send only `tilde_ed25519.pub` to the operator or signup form. +The file without `.pub` is private and stays on your computer. A hardware-backed +key can also be used when both the client and host support it. -1. Open **Terminal**. -1. Type `ssh -i ~/.ssh/tilde.club username@tilde.club` (substituting your own username). +Connect with: -Note that it's possible to save an SSH configuration so you don't have to type this whole long thing every time; [here's a good primer](http://nerderati.com/2011/03/17/simplify-your-life-with-an-ssh-config-file/) on how to do that. If you save your private key passphrase in the OS X Keychain, you shouldn't need to enter it to login. +```bash +ssh -i ~/.ssh/tilde_ed25519 member@tilde.example.net +``` -## Linux +A local `~/.ssh/config` entry saves typing: -### Generating your key pair +```sshconfig +Host mytilde + HostName tilde.example.net + User member + IdentityFile ~/.ssh/tilde_ed25519 + IdentitiesOnly yes + ForwardAgent no +``` -If you're on Linux you can use the same commands as on the Mac to generate and use a `tilde.club` key pair, but attaching `tilde.club.pub` to email is left as an exercise for the reader. +Verify a new host's fingerprint through a separate trusted channel. If it +changes later, ask why before replacing the saved host key. Agent forwarding +is normally unnecessary for using the host; enable it only for a specific +reason you understand. -### Using your key pair +## What the collected host does -At a command prompt, type `ssh -i ~/.ssh/tilde.club username@tilde.club` (substituting your own username). +The main daemon reads `/etc/ssh/sshd_config`. It has public-key support, +`PasswordAuthentication no`, keyboard-interactive authentication enabled, and +`UsePAM yes`. `AuthenticationMethods` is not set to require a combination; +the observed effective value is `any`. -Note that it's possible to save an SSH configuration so that you don't have to type this whole long thing every time; [here's a good primer](http://nerderati.com/2011/03/17/simplify-your-life-with-an-ssh-config-file/) on how to do that. +Its SSH PAM authentication stack requires `pam_unix` and +`pam_google_authenticator`, except for a special help account. That means the +PAM route asks for the Unix password and a TOTP code. It does **not** mean +public-key login must also complete that route: the collected SSH configuration +accepts a valid public key on its own. Existing account-specific exceptions and +a separate restricted help command must be reviewed locally rather than copied +into a new installation. -## Windows +`setup-2fa` is a small wrapper around `/usr/bin/google-authenticator`. +`check_google_authenticator.sh` is not referenced by the collected SSH PAM file; +its printed output is not an enforced authentication policy. -### Generating your key pair +The main config collected from this host does not contain an `Include` for +`sshd_config.d`. The presence of files in that directory does not make them +active. Check the actual service command and configuration before assuming a +drop-in changes anything. -The easiest way to generate a key pair on Windows is to use [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/), a freely-available SSH client which comes with its own key generator. +## Key-only baseline for a new host -1. Download **PuTTY** [here](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html). (Specifically, get the Windows Installer version; if you download the binaries individually, make sure you get the **PuTTYgen** binary, which is what generates key pairs.) -1. Run **PuTTYgen**. -1. From the main window, click the **Generate** button; you'll be asked to move the mouse around to generate randomness, which you should do until **PuTTYgen** tells you that it's generating the key. -1. Once the key pair is generated, click the **Save public key** button, choose a directory to save it into, and in the "File name" field, type `tilde.club.pub`. Save the key. -1. Halfway down the main window, type a good password into the "Key passphrase" field (at least 9 characters, and not just letters or numbers), and repeat it in the "Confirm passphrase" field. -1. Click the **Save private key** button, choose the same directory as above, type `tilde.club` into the "File name" field, and save the private key. -1. Finally, email the `tilde.club.pub` file to ford@ (our friendly host). **Do not attach the file `tilde.club`** -- that is your *private* key, which you should never ever **ever** share with anyone. +Use [the example drop-in](../examples/ssh/00-tilde.conf) only after admin key +login and sudo have been tested. Put it in a directory that the actual main +config includes at global scope. Fedora's default layout normally provides +`/etc/ssh/sshd_config.d/*.conf`; existing customized configs may not. -### Using your key pair +OpenSSH generally uses the first value it obtains for a setting. A file placed +later in the include order will not necessarily override an earlier value. +Keep `Match` sections at the end and finish a conditional section with +`Match all` before adding unrelated global settings. -1. Run **PuTTY**. -1. In the first window that comes up, you should be in the main "Session" category (in the left-hand column). On the right, in the "Host Name" field, type `tilde.club`. -1. In the left-hand column, find the **SSH** entry underneath the **Connection** section and then click on the small + next to it to open it up; then find and click the **Auth** entry just below it. -1. On the right the last field should be "Private key file for authentication"; click the **Browse** button and find the `tilde.club` file you saved above. Select it and click the **Open** button. -1. Finally, click the **Open** button at the bottom of the main PuTTY window to open your connection. You'll be asked for your username ("login as:"); type it and hit Enter. You will then be asked for your key passphrase; type it and hit enter, and you're in. +Before applying anything, keep a backup of the main file, its includes, the +SSH PAM file, and any unit overrides. Keep a working connection and the provider +console available. Validate the **effective** settings as well as syntax: -Note that you can save these settings from that first page of the main PuTTY window, so you don't have to do this every time. You can also use [Pageant](http://the.earth.li/~sgtatham/putty/0.58/htmldoc/Chapter9.html) (another part of the **PuTTY** kit) to store your keys so you don't need to enter them each time you login. +```bash +sshd -t +sshd -T -C user=member,host=client.example.net,addr=192.0.2.20 +systemctl reload sshd +``` -## Android +Run connection tests from a second terminal on your own computer. Test the +admin and member separately. Match rules based on source address or local port +also need representative `addr`, `laddr`, and `lport` values in the test context. +A successful syntax check alone does not establish that the intended method +will be accepted. -### Generating your key pair +## Require MFA after enrollment -1. There are several SSH terminals in the Play store. [JuiceSSH](http://juicessh.com) is one of them, try it out, see if you like it. -2. Follow this [tutorial](http://sonelli.freshdesk.com/support/solutions/articles/187068-how-do-i-generate-an-ssh-key-within) for generating a key pair with JuiceSSH. +For a new host, keep the key-only baseline working while enrolling people. +The following is an example policy, not a report of the production host's +current enforcement: -### Using your key pair +1. Install `google-authenticator` and ensure time synchronization is healthy. +2. Create a group named `mfa-ready`. +3. Have the test member run `google-authenticator` interactively in their own + account. Use time-based tokens, save the recovery codes somewhere private + off the host, and complete a code check. Keep `.google_authenticator` private. +4. Keep the Fedora SSH PAM account/session stack. Add + `auth required pam_google_authenticator.so` to `/etc/pam.d/sshd` after its + normal authentication rules. Do not use `nullok` for an account that must + provide a second factor. Do not edit authselect-generated shared files to + make this SSH-only change. +5. Add the following conditional block at the end of the effective SSH config, + after the baseline global settings: -1. For JuiceSSH, follow this [tutorial](http://sonelli.freshdesk.com/support/solutions/articles/178806-how-to-create-define-a-new-ssh) on defining new credentials. -2. Let us know how it goes! +```sshconfig +Match Group mfa-ready + KbdInteractiveAuthentication yes + AuthenticationMethods publickey,keyboard-interactive:pam +Match all +``` -## Why go through all this? +With Fedora's password authentication PAM substack retained, this asks for a +key, the Unix password, and the authenticator code. If you want a key plus TOTP +without the Unix password, design and test that SSH-specific PAM stack instead +of removing password checks from the shared system authentication configuration. -One of the weakest links in online account security is your password. If someone were to take over the tilde server (specifically the SSH server, `sshd`), they would be able to capture the passwords of everyone who uses a password to log in. (This isn't theoretical -- [it has actually happened](http://www.apache.org/info/20010519-hack.html).) If users use their SSH keys to log in instead, hackers can't do this anymore; a user's private key never leaves their own computer and can't be captured on the server. +Validate the config, then add only the enrolled test account to the group: -Additionally, if you use an SSH agent, you can login without entering a password every time. On OS X the Apple Keychain provides this functionality. On Linux it's `ssh-agent` (optionally with assistance from [keychain](http://www.funtoo.org/Keychain)). On Windows `Pageant` provides this capability for [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/). +```bash +groupadd -f mfa-ready +usermod --append --groups mfa-ready member +sshd -t +sshd -T -C user=member,host=client.example.net,addr=192.0.2.20 +systemctl reload sshd +``` + +From the client, test correct and incorrect codes and confirm that key-only +batch authentication fails for the enrolled account: + +```bash +ssh -o BatchMode=yes -o PreferredAuthentications=publickey \ + -i ~/.ssh/tilde_ed25519 member@tilde.example.net +``` + +That last command is a negative test and should fail under the enforced policy. +Also verify normal interactive login, SFTP if offered, recovery codes, and admin +recovery. Scripts using SSH need a separately documented policy; silently +exempting all automation defeats a blanket MFA claim. + +Only enroll more members after the full test passes. A user-editable startup +file or `.new_user` marker is onboarding state, not an access-control boundary. + +## Legacy SSH + +The existing host has a second daemon on TCP 2222. Its service reads +`/etc/ssh/sshd_config_legacy`, disables root and password login, restricts its +allowed user, and deliberately enables old RSA/SHA-1 and group1 algorithms. +This is a compatibility exception, not a general access method. + +A new tilde does not need this service. If you have a specific legacy client +requirement, isolate it in a separate daemon and source/identity policy, verify +its effective settings with `sshd -T -f PATH`, and record who needs it and when +it can be retired. Do not weaken the normal daemon or the host-wide crypto +policy for every member. + +## Recovery + +Verify the person's identity before replacing a key or resetting MFA. Remove +lost factors, invalidate unused recovery codes when re-enrolling, and check +whether other services share the same account password. Test that the revoked +key or factor no longer works. Do not ask someone to send a private SSH key or +their TOTP secret in an ordinary support message. + +References: [OpenSSH server configuration](https://man.openbsd.org/sshd_config) +and [Google Authenticator PAM](https://github.com/google/google-authenticator-libpam). diff --git a/docs/usenet.md b/docs/usenet.md index fd9a9d5..b655ee2 100644 --- a/docs/usenet.md +++ b/docs/usenet.md @@ -1,130 +1,87 @@ -# Usenet in 60 Seconds +# Usenet and local newsgroups -## Installation +The current shell host runs INN 2.7.3. Its reader rules, peer list, and spool +configuration were not supplied. The old Fedora 32 setup and manual history +rebuild have been replaced here with a local-first deployment procedure. -Create Linux machine (This is based on Fedora 32) +## Install and inspect the packaged layout - $ sudo -i - root $ dnf update +```bash +dnf install inn slrn +rpm -ql inn +systemctl cat innd.service +``` +Use the config, service account, spool, history, and log paths supplied by that +package. Do not replace `pathnews` with `/usr/local/news` merely because an old +source-install guide used it. Keep a copy of the packaged examples and consult +the installed `inn.conf(5)` and `readers.conf(5)` manuals. - root $ dnf install innd +In `inn.conf`, set your own `organization`, `pathhost`, `domain`, and reader +`server` name. Retain the packaged storage/history layout until you have a +reason to change it. Keep the first deployment reachable only from loopback or +an explicit trusted network, using both the service's bind policy and the +firewall. - root $ vim /etc/hostname # set your hostname to something like `news` - root $ vim /etc/hosts # change local.localdomain to the new hostname +## Local reader policy - # should probably restart the machine to get the new hostname +INN separates reader authentication from reader access. A small example +`readers.conf` for trusted localhost shell clients is: - root $ sudo -u news bash - news $ vim /etc/inn.conf +```text +auth "shell-local" { + hosts: "127.0.0.1,::1" + default: "