System diagram
Request flow: web page
User session lifecycle
Infrastructure
atl.sh runs on a physical dedicated server hosted by Hetzner, with DNS managed through Cloudflare. The production server is managed manually (bare-metal, not provisioned by Terraform). Terraform provisions only the staging environment.| Component | Provider / Technology |
|---|---|
| Server | Hetzner dedicated (physical, manual) |
| Staging | Hetzner Cloud VPS (Terraform) |
| DNS | Cloudflare (Terraform) |
| OS | Debian 13 (Trixie) |
| Config mgmt | Ansible |
Terraform
Terraform manages the staging VPS and Cloudflare DNS records. Production infrastructure is bare-metal and not managed by Terraform. Terraform state is stored interraform/terraform.tfstate (not committed).
Variables live in terraform/terraform.tfvars (also not committed — copy from
terraform.tfvars.example).
- Hetzner Cloud VPS for staging
- Cloudflare DNS records (
atl.sh,staging.atl.sh, and subdomains)
Ansible Structure
Configuration management is split into five roles applied byansible/site.yml.
Each role has a focused responsibility and corresponding --tags value for
selective runs.
Role: packages
All user-facing software:- Shells: bash, zsh, fish, mksh, tcsh, ksh93, rc, elvish, nushell, dash
- Nushell installed via Gemfury apt repository (not in Debian repos)
- Editors: vim, neovim, nano, emacs, micro, joe, vis, kakoune
- File managers: ranger, lf, mc, nnn, vifm
- Languages: Python, Node.js, Go, Rust, Ruby, C/C++, Haskell, Elixir, Java, and 20+ more
- CLI tools: ripgrep, fzf, jq, pandoc, miller, taskwarrior, newsboat, sc-im, duf, and many more
- Networking: gping, trippy, xh, drill, prettyping, fping, sipcalc, termshark, speedtest-cli, rclone
- Chat: irssi, weechat, profanity, talk, talkd (with openbsd-inetd)
- Mail: alpine, neomutt, aerc
- Browsers: lynx, w3m, elinks, links, amfora
- Games: nethack, crawl, angband, bsdgames, botany, arcade games
- Fun: fortune, cowsay, figlet, cmatrix, cbonsai, boxes, asciinema, nyancat
/etc/shellsregistration for all available login shells
Role: security
Security hardening, split into task files:- CIS kernel parameter hardening via
sysctl(ASLR, ptrace restrictions, network protections) - Kernel module blacklisting (uncommon filesystems, protocols)
- Password and sudo policy
- SSH configuration (key-only auth, ports 22 + 2222,
AllowGroups) - UFW firewall (allowlist — only required ports open)
- Fail2ban (5 failures in 10 minutes → 1-hour ban)
- Auditd with 40+ rules (identity files, privilege escalation, LOLBins, MITRE ATT&CK-tagged syscall rules)
- journald hardening (1 GB cap)
- AIDE file integrity monitoring (daily check at 05:00 UTC)
- Automatic security updates via
unattended-upgrades - Malware scanning (
rkhunter,chkrootkit,lynis)
Role: users
Populates the shared user environment:/etc/skel/— scaffold copied to every new user’s home on account creation- MOTD — welcome message shown on SSH login
- PAM resource limits — per-user caps enforced via
/etc/security/limits.conf - Social commands:
menu,plan,lastplan,online,community - Private
/tmpviapam_namespacepolyinstantiation - Per-user logrotate cron job
Role: environment
Per-user resource isolation and environment setup:- Cgroup v2 user slices — systemd enforces 1.5 GB RAM, 200% CPU, 200 process limits per user session
- Disk quotas — 5 GB soft / 6 GB hard per user (XFS or ext4 quota)
- Private
/tmp—pam_namespacepolyinstantiation gives each session an isolated tmpdir - XDG directories —
XDG_CONFIG_HOME,XDG_CACHE_HOME,XDG_DATA_HOME,XDG_STATE_HOMEall set via/etc/profile.d/ - PATH additions —
~/.local/bin,~/.cargo/bin,~/.go/bin,~/.local/share/gem/bin,~/.deno/bin, and others added at login
Role: services
All public-facing services and community features:| Task file | Service |
|---|---|
web.yml | Nginx + fcgiwrap for tilde sites and CGI |
gemini.yml | molly-brown Gemini server |
gopher.yml | Gophernicus Gopher server |
finger.yml | efingerd (systemd socket-activated) |
games.yml | NetHack, Botany, Angband, Crawl, arcade games |
webring.yml | Self-managing member ring with nginx auto-injection |
dict.yml | dictd with 7 offline dictionaries (localhost:2628) |
Network
Open Ports
| Port(s) | Protocol | Service |
|---|---|---|
| 22, 2222 | TCP | SSH |
| 70 | TCP | Gopher |
| 79 | TCP | Finger |
| 80 | TCP | HTTP (nginx) |
| 443 | TCP | HTTPS (nginx + Cloudflare) |
| 1965 | TCP | Gemini |
| 21 | TCP | FTP control (vsftpd) |
| 40000–40100 | TCP | FTP passive data |
Internal Services
| Service | Bind address | Notes |
|---|---|---|
| Node Exporter | 127.0.0.1:9100 | Prometheus metrics, not public |
| dictd | 127.0.0.1:2628 | Dictionary server (RFC 2229) |
| talkd | 0.0.0.0:517-518 | Talk daemon via inetd, UFW blocked |
| fcgiwrap | Unix socket | CGI execution for tilde sites |
| molly-brown | 0.0.0.0:1965 | Gemini TLS server |
| Gophernicus | 0.0.0.0:70 | Gopher server |
| efingerd | systemd socket | Finger protocol, socket-activated |
Tilde Web Architecture
Each user’s~/public_html/ is served at https://atl.sh/~username/ via nginx.
public_html/ execute as the user’s UID via fcgiwrap with
suexec-style per-user wrappers.
The webring lives at /var/www/html/ring/:
go.cgi— Python CGI for next/prev/random navigationmembers.json— generated every 15 minutes by a cron job scanning~/.ringfileswidget.js— auto-injected into all tilde pages via nginxsub_filter
Environments
Three environments share the same Ansible playbook and roles:| Environment | Infrastructure | Purpose |
|---|---|---|
dev | Local Vagrant VM | Ansible development and testing |
staging | Hetzner Cloud VPS (Terraform) | Pre-production validation |
prod | Hetzner dedicated (bare-metal) | Live production system |