OpenBSD shells VM provisioning (reproducible config + Go tooling)
Find a file
James 33a7f45254 feat(shells): pipx + ~/.local/bin on PATH
Ubuntu 24.04 ships PEP 668 (/usr/lib/python3.12/EXTERNALLY-MANAGED), so
`pip install --user` is refused and users had no supported way to install
Python CLI tools. Add pipx to the base toolset.

Also put ~/.local/bin on PATH via /etc/profile.d/pipx.sh rather than relying
on Ubuntu's stock ~/.profile, which only adds it when the directory already
exists at login -- it does not until the first pipx install -- and which
non-bash logins skip entirely. Without this the first install looks like it
worked and then the command is not found.

Covers applications only; importable libraries still need a user venv.
2026-08-14 09:01:29 -04:00
cmd feat(shells): no-root capability model, prefixless login, hardening 2026-06-19 13:27:20 -04:00
docs scaffold: repo skeleton + README 2026-06-18 01:40:17 -04:00
host feat(shells): pipx + ~/.local/bin on PATH 2026-08-14 09:01:29 -04:00
images feat(shells): latest Go/PHP + nginx, in-place upgrades, 512MB+shared limits 2026-06-19 22:09:19 -04:00
internal feat(shells): shell-served hostnames with auto-HTTPS + expand reserved names 2026-06-21 07:59:20 -04:00
pkg/shells feat(shells): <sub>.4evr.sh reverse DNS for container IPv6s 2026-07-18 01:21:55 -04:00
.gitignore feat: per-container metrics via Telegraf + host vmagent 2026-06-19 11:02:10 -04:00
go.mod feat(cert): wildcard *.sh.ircforever.org via Go ACME client + Traefik 2026-06-18 10:38:54 -04:00
go.sum feat(cert): wildcard *.sh.ircforever.org via Go ACME client + Traefik 2026-06-18 10:38:54 -04:00
README.md feat(shells): no-root capability model, prefixless login, hardening 2026-06-19 13:27:20 -04:00

shells-config

Public free-shell program for IRCNow / sh.ircforever.org.

Each user picks a name and gets <name>.sh.ircforever.org, an Incus container on the shells VPS (89.167.23.102, Hetzner Helsinki), and a dedicated IPv6 /128 from 2a01:4f9:c014:663f::/64 routed directly into their container.

Architecture

main VPS (control plane)  ──HMAC POST──▶  shellsd on shells VPS
                                              │
                                              ▼
                                       incus exec / useradd /
                                       Traefik fragment write /
                                       DNS provider call
  • pkg/shells/ — library. Backend interface + Incus implementation. IPv6 allocator, JSON state, Traefik file fragments, Cloudflare DNS client.
  • cmd/shellsd/ — webhook receiver daemon. systemd unit; listens on 127.0.0.1:8090, fronted by Traefik on api.sh.ircforever.org.
  • cmd/shellctl/ — break-glass CLI for the shells host.
  • internal/webhook/ — HMAC verify + HTTP handlers.
  • host/ — config that lives on the shells VPS:
    • bootstrap.sh — idempotent host bootstrap (incus + UFW + sysctl + sshd)
    • incus-init.yaml — preseed for incus admin init
    • sshd-shells.conf — Match Group shells block for sshd
    • shells-enter + shells-enter-root — login-shell wrapper into the user's container (via sudo + the sudoers rule below)
    • sudoers-shells — %shells ALL=(root) NOPASSWD: …shells-enter-root
    • shellsd.service — systemd unit
    • telegraf.conf — baked into the base image; exposes in-guest metrics on :9273
    • observability/ — host-side vmagent (scrapes each container's telegraf and remote-writes to the central VictoriaMetrics) + shells-targets.{py,service,timer} that regenerate the scrape list from incus list
  • images/build-shells-base.sh — bake the Ubuntu 24.04 base image (incl. Telegraf)

Webhook API

All requests POST JSON to https://api.sh.ircforever.org/v1/… signed with X-Signature: sha256=<hex(hmac_sha256(secret, body))> + X-Timestamp (5-minute skew window).

Endpoint Body Effect
POST /v1/provision {username, pubkey} create container, host account, Traefik fragment, DNS records
POST /v1/deprovision {username} reverse
POST /v1/block {username} stop the container (login refused, data kept)
POST /v1/unblock {username} start a blocked container
GET /v1/list – list provisioned shells
GET /healthz – 200 if incus reachable
GET /metrics – Prometheus exposition

Metrics

Each container runs Telegraf (baked into shells-base) exposing in-guest metrics (cpu/mem/disk/net/netstat/system/…) on :9273 — no credentials live inside the container (users control their own shell), so it only exposes metrics; nothing pushes with a secret from inside. Telegraf is un-killable by the user because shells are unprivileged (no sudo). A host-side vmagent (host/observability/) scrapes every container over the incus bridge and remote-writes to the central VictoriaMetrics on the main VPS; shells-targets.timer keeps the scrape list in sync with incus list. Grafana renders the per-shell Proxmox LXC Container Data community dashboard (ID 23742), retargeted to these metrics.

container telegraf :9273  ──scrape──▶  vmagent (host)  ──remote_write──▶  VictoriaMetrics (VPS)  ──▶  Grafana

Per-user surface

Path What lands there
ssh <user>@89.167.23.102 (IPv4) host sshd → shells-enter → incus exec shells-<user>
ssh <user>@<user>.sh.ircforever.org (IPv6) direct to container's own sshd (when DNS AAAA is set)
https://<user>.sh.ircforever.org Traefik (host) → container's :80

Bootstrap a fresh shells host

# On the host, as root:
git clone … /usr/local/share/shells-config
cd /usr/local/share/shells-config
sudo host/bootstrap.sh
sudo images/build-shells-base.sh

# Install the daemon binary + secret:
sudo install -m 755 shellsd /usr/local/bin/shellsd
SECRET=$(openssl rand -hex 32)
sudo sed -i "s|^SHELLSD_HMAC_SECRET=.*|SHELLSD_HMAC_SECRET=$SECRET|" /etc/shellsd/env
sudo systemctl enable --now shellsd

Plan

See the parent plan file at /Users/james/.claude/plans/polymorphic-gliding-rossum.md.