12 KiB
Build a tilde-style machine
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.
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 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.
1. Plan the machine
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.
You need:
- 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.
2. Install and update Fedora
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.
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
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
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.
On a new host using firewalld, first check which zone owns the network interface:
firewall-cmd --get-active-zones
In the example below, replace public with that zone. Confirm any custom SSH
port is allowed before changing a firewall remotely.
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 --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.
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 before adding optional services.
4. Establish an admin account
Create a private primary group and home. Set a password for sudo, then add the
account to wheel:
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
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 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. 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
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
dnf install nginx
install -d -o root -g root -m 0755 /usr/share/nginx/html
Use web publishing and the static nginx example. Start with static pages. That makes it possible to test path mapping and private-file access before adding PHP or CGI.
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:
nginx -t
systemctl enable --now nginx
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.
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
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:
curl --fail http://tilde.example.net/~member/
curl --fail http://tilde.example.net/u/member/
Create a second disposable member and run the cross-account checks in the web chapter before enabling executable user content.
8. Add TLS
The static example exposes ACME challenges beneath /var/lib/letsencrypt.
Create that directory and install the packaged Certbot client:
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
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.
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:
dnf install fish zsh emacs irssi weechat alpine lynx links \
git gcc make python3 ruby perl figlet
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.
Choose the shells you support in /etc/shells, and make sure each has working
startup files. Add first-login setup only after
its dependencies and exit behavior have been tested with those shells.
10. Set quotas and resource limits
Use the shell operations chapter. For XFS, discover the mount
containing /home before running quota commands. On the collected machine that
mount is /, not /home.
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.
11. Back up and restore before opening signups
Follow backup and recovery. 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.
A successful backup job alone is not a completed restore test.
12. Add the services you intend to operate
- Mail: local delivery, IMAP, submission, DNS, filtering, and abuse checks.
- IRC: clients, persistent sessions, ident, ZNC, and optional network hosting.
- Gopher and Gemini: separate public directories and service users.
- Usenet: local groups before opening reader access or negotiating peers.
- Website and community tools: signup handling, the wiki, and optional apps.
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.
Before accepting members
- 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.
Keep your local configuration changes under version control without secrets. Record the version or commit used for every manually installed service. The upstream references explain the software-specific behavior behind these examples.