Not in the top 1,000 by GitHub stars
mountain-beaver/vps-forge
Agent Skill that lets Claude Code, Codex, Cursor, Copilot and other AI agents set up and harden a Debian/Ubuntu VPS without locking you out: SSH, firewall, fail2ban, updates, Docker, TLS, backups, phone alerts. Benchmarked on 120 VM runs.
History
Review weekly changes to SKILL.md before updating. Dates mark the weeks in which we observed each version; checks apply to the recorded commits.
View weekly changes
skills/vps-forge/SKILL.md
Week of Nov 2, 2026 · Line counts unavailable
Before Not checked yet → After Network or broad shell
First observation → de8b1c084559
No earlier version was observed for comparison.
Verified commit de8b1c084559b4175be7aaabf5cb61a4d5a19580
git clone --filter=blob:none --no-checkout https://github.com/mountain-beaver/vps-forge.git .hotskills/mountain-beaver-vps-forge && git -C .hotskills/mountain-beaver-vps-forge checkout --detach de8b1c084559b4175be7aaabf5cb61a4d5a19580
# Copy each selected skill directory from .hotskills/mountain-beaver-vps-forge into .claude/skills/<skill-name>/git clone --filter=blob:none --no-checkout https://github.com/mountain-beaver/vps-forge.git .hotskills/mountain-beaver-vps-forge && git -C .hotskills/mountain-beaver-vps-forge checkout --detach de8b1c084559b4175be7aaabf5cb61a4d5a19580
# Copy each selected skill directory from .hotskills/mountain-beaver-vps-forge into .agents/skills/<skill-name>/Heat
Insufficient historyNot enough history yet to measure heat. It appears after a few days of data.
Heat never changes the order by stars, and sponsorship never touches it. How heat works
Stars over time
30-day change not yet available: not enough history.
Individual skills in this repository
- vps-forge
skills/vps-forge
How it works
vps-forge
When your agent uses it
Set up, harden, audit and polish a Linux VPS (Debian/Ubuntu) to production grade without ever losing access — SSH keys and crypto, firewall, fail2ban/CrowdSec, automatic security updates, kernel hardening, Docker, Caddy/nginx with TLS, WireGuard/Tailscale, restic backups, phone alerts, monitoring and a final verified score. Use this skill whenever the user mentions a VPS, VDS, cloud server, droplet, Hetzner/DigitalOcean/Vultr/Linode/OVH/Oracle Cloud/AWS EC2 or Lightsail box (x86 or ARM), a "fresh server", "secure/harden/lock down my server", SSH hardening, ufw, fail2ban, "set up Docker/Caddy/nginx on my server", preparing a server for Coolify, Dokploy, CapRover or Kamal, server backups or monitoring, or asks to audit or check an existing server — even if they don't say "harden". Not for RHEL/Fedora-family servers, Kubernetes nodes (k3s, kubeadm), Proxmox or other hypervisor hosts, managed or shared hosting without root, or the user's own desktop.
What it loads into context
Frontmatter
305 tokensAlways available to the agent
SKILL.md
5,038 tokensWhen the skill is activated
Reference documents
37,368 tokensLoaded on request
Token counts are estimates. Scripts run separately; they are not counted as reference context.
The playbook
- 1. Rules that keep the user safe
- 2. Operating modes
- 3. Intake (ask at most 4 questions at a time)
- 4. Workflow
- 5. Levels
- 6. Definition of done
- 7. Final report
- 8. Reference map
Scripts executed separately
assets/firewall/ufw-cloudflare-sync.shassets/motd/60-vps-forgeassets/restic/vps-forge-backup.shevals/audit-production-shop/place-files.shevals/compromised-box/place-files.shscripts/audit.shscripts/deadman.shscripts/external-check.shscripts/install-alerts.sh
Files
../../.claude-plugin/icon.pngasset5,477 bytes../../.claude-plugin/marketplace.jsonasset342 bytes../../.claude-plugin/plugin.jsonasset1,133 bytesSKILL.mdcore19,640 bytesSKILL.mdcore19,640 bytesSKILL.mdcore19,640 bytesassets/SERVER.mddoc3,153 bytesassets/alerts/notify.env.exampleasset1,151 bytesassets/apt/20auto-upgradesasset179 bytesassets/apt/52vps-forge-unattendedasset630 bytesassets/caddy/Caddyfileasset1,620 bytesassets/cloud-init.yamlasset6,324 bytesassets/docker/daemon.jsonasset205 bytesassets/fail2ban/vps-forge.localasset1,930 bytesassets/firewall/ufw-cloudflare-sync.serviceasset224 bytesassets/firewall/ufw-cloudflare-sync.shruns3,042 bytesassets/firewall/ufw-cloudflare-sync.timerasset158 bytesassets/journald/10-vps-forge.confasset216 bytesassets/motd/60-vps-forgeruns2,426 bytesassets/needrestart/50-vps-forge.confasset257 bytesassets/restic/env.exampleasset871 bytesassets/restic/excludeasset379 bytesassets/restic/includeasset247 bytesassets/restic/vps-forge-backup-maint.serviceasset354 bytesassets/restic/vps-forge-backup-maint.timerasset166 bytesassets/restic/vps-forge-backup.serviceasset288 bytesassets/restic/vps-forge-backup.shruns5,399 bytesassets/restic/vps-forge-backup.timerasset159 bytesassets/shell/vps-forge.bashrcasset1,090 bytesassets/ssh/00-vps-forge.confasset944 bytesassets/sysctl/99-vps-forge.confasset2,495 bytesevals/audit-production-shop/case.yamlasset481 bytesevals/audit-production-shop/files/audit.jsonasset2,564 bytesevals/audit-production-shop/graders/01-the-response-rates-the-publicly-publishe.mddoc154 bytesevals/audit-production-shop/graders/02-the-response-flags-the-adminer-container.mddoc142 bytesevals/audit-production-shop/graders/03-the-response-flags-the-unknown-or-old-ol.mddoc173 bytesevals/audit-production-shop/graders/04-the-response-explains-that-the-password.mddoc185 bytesevals/audit-production-shop/graders/05-the-response-mentions-the-missing-backup.mddoc128 bytesevals/audit-production-shop/graders/06-the-response-does-not-run-or-instruct-im.mddoc176 bytesevals/audit-production-shop/place-files.shruns62 bytesevals/audit-production-shop/prompt.mddoc295 bytesevals/compromised-box/case.yamlasset495 bytesevals/compromised-box/files/audit.txtdoc900 bytesevals/compromised-box/graders/01-the-response-identifies-that-the-server.mddoc240 bytesevals/compromised-box/graders/02-the-response-does-not-proceed-with-routi.mddoc148 bytesevals/compromised-box/graders/03-the-response-recommends-rebuilding-from.mddoc137 bytesevals/compromised-box/graders/04-the-response-recommends-rotating-credent.mddoc138 bytesevals/compromised-box/graders/05-the-response-recommends-preserving-evide.mddoc135 bytesevals/compromised-box/place-files.shruns62 bytesevals/compromised-box/prompt.mddoc265 bytesevals/coolify-host/case.yamlasset627 bytesevals/coolify-host/graders/01-root-key-login-kept.mddoc248 bytesevals/coolify-host/graders/02-panel-ssh-not-banned.mddoc336 bytesevals/coolify-host/graders/03-no-second-proxy.mddoc200 bytesevals/coolify-host/graders/04-dashboard-port.mddoc246 bytesevals/coolify-host/graders/05-backs-up-coolify.mddoc144 bytesevals/coolify-host/prompt.mddoc390 bytesevals/fresh-ubuntu-docker-web/case.yamlasset616 bytesevals/fresh-ubuntu-docker-web/graders/01-the-sshd-hardening-drop-in-is-named-so-i.mddoc234 bytesevals/fresh-ubuntu-docker-web/graders/02-password-authentication-and-root-login-a.mddoc194 bytesevals/fresh-ubuntu-docker-web/graders/03-ssh-or-firewall-changes-are-wrapped-in-a.mddoc218 bytesevals/fresh-ubuntu-docker-web/graders/04-the-app-container-is-published-on-127-0.mddoc205 bytesevals/fresh-ubuntu-docker-web/graders/05-apt-commands-are-non-interactive-debian.mddoc159 bytesevals/fresh-ubuntu-docker-web/graders/06-fail2ban-is-configured-with-the-systemd.mddoc140 bytesevals/fresh-ubuntu-docker-web/graders/07-the-telegram-bot-token-is-placed-in-a-ro.mddoc162 bytesevals/fresh-ubuntu-docker-web/prompt.mddoc492 bytesevals/k3s-node-out-of-scope/case.yamlasset285 bytesevals/k3s-node-out-of-scope/graders/01-stops.mddoc150 bytesevals/k3s-node-out-of-scope/graders/02-explains-why.mddoc217 bytesevals/k3s-node-out-of-scope/prompt.mddoc290 bytesevals/local-nginx-no-trigger/case.yamlasset196 bytesevals/local-nginx-no-trigger/graders/01-no-skill.mddoc132 bytesevals/local-nginx-no-trigger/graders/02-answers.mddoc191 bytesevals/local-nginx-no-trigger/prompt.mddoc256 bytesevals/lockout-trap/case.yamlasset406 bytesevals/lockout-trap/graders/01-the-response-warns-that-disabling-passwo.mddoc137 bytesevals/lockout-trap/graders/02-the-response-includes-generating-an-ssh.mddoc185 bytesevals/lockout-trap/graders/03-the-response-creates-or-uses-a-non-root.mddoc131 bytesevals/lockout-trap/graders/04-the-response-requires-verifying-login-fr.mddoc162 bytesevals/lockout-trap/graders/05-any-sshd-change-is-validated-with-sshd-t.mddoc98 bytesevals/lockout-trap/prompt.mddoc283 bytesevals/narrow-ssh-port/case.yamlasset437 bytesevals/narrow-ssh-port/graders/01-does-only-what-was-asked.mddoc202 bytesevals/narrow-ssh-port/graders/02-offers-the-rest.mddoc184 bytesevals/narrow-ssh-port/graders/03-port-change-is-safe.mddoc225 bytesevals/narrow-ssh-port/graders/04-socket-activation.mddoc196 bytesevals/narrow-ssh-port/graders/05-no-self-ban.mddoc152 bytesevals/narrow-ssh-port/prompt.mddoc286 bytesreferences/alerts-monitoring.mddoc4,869 bytesreferences/audit-mode.mddoc4,659 bytesreferences/backups.mddoc6,878 bytesreferences/base.mddoc12,449 bytesreferences/comfort.mddoc2,228 bytesreferences/databases.mddoc3,596 bytesreferences/distro-notes.mddoc5,878 bytesreferences/docker.mddoc8,485 bytesreferences/firewall.mddoc14,532 bytesreferences/kernel.mddoc6,980 bytesreferences/paas.mddoc7,654 bytesreferences/reproducibility.mddoc3,179 bytesreferences/script-mode.mddoc6,607 bytesreferences/ssh.mddoc21,751 bytesreferences/troubleshooting.mddoc7,344 bytesreferences/vpn.mddoc5,237 bytesreferences/web.mddoc5,198 bytesscripts/audit.shruns64,534 bytesscripts/deadman.shruns30,926 bytesscripts/external-check.shruns18,901 bytesscripts/install-alerts.shruns20,166 bytes
Read SKILL.md
# VPS Forge
Take a fresh or existing Debian/Ubuntu VPS to a state the user will be proud of: locked down, observable, backed up, documented, reproducible and pleasant to live on. Do it with a safety net under every risky step, so access to the server is never lost.
Speak the user's language. Before every change, say in one line what you are changing and why. When the user asks "why?", answer properly: the reasoning matters more than the command.
## 1. Rules that keep the user safe
These exist because one wrong `sshd` or `ufw` change can make a server unreachable, and the user may have no idea how to use the provider's rescue console.
1. **Never cut off access.** Every SSH, firewall, sudo or PAM change goes inside a dead-man switch: `scripts/deadman.sh arm` → change → validate → test from a **new, unmultiplexed** connection → disarm. If the new connection fails, do nothing and let the timer roll back (default 5 min). Arm longer (`arm 10`) when the change needs several edits or a package install, and if it runs late, `vps-forge-deadman extend <min>` moves the rollback while the snapshot stays the same. Never extend after a failed test login.
- **Before the first risky step,** make sure the user knows where the provider's web console (VNC/serial) is. Keep the existing session open throughout.
- **The test must be a new connection.** A plain `ssh host` reuses the live ControlMaster, which never touches the new sshd, port or firewall. Always test with `-o ControlPath=none`.
- **Disarming.** With NOPASSWD sudo (or the temporary grant) the test is the disarm: `ssh -o ControlPath=none -o BatchMode=yes -p <port> <admin>@<host> 'sudo -n vps-forge-deadman disarm'`. Under password sudo, `sudo -n -v` must fail with `a password is required`, and you disarm from the root session (ssh.md "Test and disarm").View the complete source on GitHubSkills included
- vps-forge
Repository facts
- License
- MIT
- Created
- Oct 24, 2026
- Last pushed
- Oct 26, 2026
- Forks
- 2
- Open issues
- 0
- Project activity
- Active
Before you install
Network or broad shellAutomated signals, not a guarantee
Checked Nov 5, 2026
Not fully analyzed: some files were too long, unreadable or missing. This never lowers the level.
Show 2 notes
skills/vps-forge/SKILL.md:1Analysis incomplete: Referenced executable target unavailable; not analyzedskills/vps-forge/SKILL.md:1Analysis incomplete: Referenced executable target unavailable; not analyzed
- No broad tool permissions
- 113 dangerous shell commandsin 23 files: vps-forge/scripts/deadman.sh:2 vps-forge/references/ssh.md:6 vps-forge/SKILL.md:18 and 20 more
- 106 network referencesin 17 files: vps-forge/scripts/external-check.sh:6 vps-forge/scripts/install-alerts.sh:55 vps-forge/references/docker.md:28 and 14 more
- No hidden or obfuscated text
- No instruction overrides
- 9 executable scriptsin 9 files: vps-forge/assets/firewall/ufw-cloudflare-sync.sh:1 vps-forge/assets/motd/60-vps-forge:1 vps-forge/assets/restic/vps-forge-backup.sh:1 and 6 more
- 27 references to secretsin 11 files: vps-forge/references/ssh.md:44 vps-forge/scripts/install-alerts.sh:138 vps-forge/SKILL.md:44 and 8 more
- No requests to vote in Hot
Show all 255 signals by file
.claude-plugin/plugin.json
- Network accessline 7
"name": "Hung Up" }, "homepage": "https://github.com/mountain-beaver/vps-forge", - Network accessline 8
}, "homepage": "https://github.com/mountain-beaver/vps-forge", "repository": "https://github.com/mountain-beaver/vps-forge", - Network accessline 9
"homepage": "https://github.com/mountain-beaver/vps-forge", - Network accessline 10
"repository": "https://github.com/mountain-beaver/vps-forge", - Network accessline 11
"documentationUrl": "https://github.com/mountain-beaver/vps-forge#readme", - Network accessline 12
"supportUrl": "https://github.com/mountain-beaver/vps-forge/issues", - Network accessline 13
"privacyPolicyUrl": "https://github.com/mountain-beaver/vps-forge/blob/main/PRIVACY.md",
skills/vps-forge/SKILL.md
- Dangerous shell operationline 18
1. **Never cut off access.** Every SSH, firewall, sudo or PAM change goes inside a dead-man switch: `scripts/deadman.sh arm` → change → validate → test from a **new, unmultiplexed** connection → disarm. If the new connection fails, do nothi - Dangerous shell operationline 21
- **Disarming.** With NOPASSWD sudo (or the temporary grant) the test is the disarm: `ssh -o ControlPath=none -o BatchMode=yes -p <port> <admin>@<host> 'sudo -n vps-forge-deadman disarm'`. Under password sudo, `sudo -n -v` must fail with `a - Dangerous shell operationline 24
- **The admin's IP** is `$(sudo vps-forge-deadman client-ip)`, because `$SSH_CLIENT` is empty under sudo. Stop if it's empty or not an IP. If you aren't on the user's own machine (cloud sandbox, CI), it's *your* IP: mark every entry (ufw, ` - Network accessline 29
6. **Secrets never travel through chat.** Generate passwords, keys and tokens on the server, store them with mode 600, and tell the user where they are. If the user pastes a secret anyway, use it, don't repeat it back, and suggest rotating - Dangerous shell operationline 31
8. **Stop if the box looks compromised.** Unknown UID-0 accounts, unknown `authorized_keys`, `/etc/ld.so.preload`, miners, `curl | sh` cron entries: stop hardening and follow the compromise section of `references/audit-mode.md`. You can't h - Network accessline 31
8. **Stop if the box looks compromised.** Unknown UID-0 accounts, unknown `authorized_keys`, `/etc/ld.so.preload`, miners, `curl | sh` cron entries: stop hardening and follow the compromise section of `references/audit-mode.md`. You can't h - Access to secretsline 44
[Credential-related source omitted] - Dangerous shell operationline 46
- Run bundled scripts without copying them by hand: `ssh host 'sudo bash -s -- --flags' < scripts/audit.sh`. Install the helpers that must stay on the server (deadman, alerts) into `/usr/local/sbin/` with `install -m 755`. - Dangerous shell operationline 64
- **A narrow request** (move SSH, add fail2ban, create a sudo user, a short checklist of such items): do exactly that, under every safety rule. Making the request safe is part of it, not extra: the admin's IP goes on fail2ban's ignore list - Dangerous shell operationline 74
| 3 | Admin user & SSH | `references/ssh.md` | Admin user with a key, sudo policy chosen, root login and passwords off, modern crypto, verified from a new session | - Dangerous shell operationline 83
| 12 | Document & hand off | `assets/SERVER.md`, `references/reproducibility.md`, ssh.md §Hand-off cleanup | `/root/SERVER.md` and the final report, agent-session entries and the temporary sudo grant removed; offer the cloud-init/Ansible ex - Dangerous shell operationline 122
- [ ] The deadman is disarmed and no temporary sudo or firewall exceptions are left behind - Access to secretsline 129
[Credential-related source omitted] - Dangerous shell operationline 130
3. **What only the user can do:** for example save the key, sudo and backup passwords offline, enable provider snapshots, confirm they received the test alert. Name anything you left on purpose, such as root login kept for a deploy key (ssh - Dangerous shell operationline 140
- `references/ssh.md`: admin user, sudo policy, the drop-in ordering trap, crypto, port change with socket activation, FIDO2/TOTP, cleanup of provider keys
skills/vps-forge/assets/SERVER.md
- Dangerous shell operationline 13
| Admin user / sudo | `<admin>`; sudo <password / NOPASSWD> | - Dangerous shell operationline 71
sudo vps-forge-deadman arm 5 # before touching ssh/ufw; 'disarm' after testing a NEW session - Dangerous shell operationline 72
sudo ufw status numbered - Dangerous shell operationline 73
sudo fail2ban-client status sshd - Dangerous shell operationline 76
sudo needrestart -r l
skills/vps-forge/assets/alerts/notify.env.example
skills/vps-forge/assets/caddy/Caddyfile
skills/vps-forge/assets/cloud-init.yaml
skills/vps-forge/assets/fail2ban/vps-forge.local
- Dangerous shell operationline 19
# The admin's IP (`sudo vps-forge-deadman client-ip`; $SSH_CLIENT is empty under sudo), written
skills/vps-forge/assets/firewall/ufw-cloudflare-sync.sh
- Executable scriptline 1
#!/usr/bin/env bash - Dangerous shell operationline 22
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT - Network accessline 24
curl -fsS --max-time 20 --retry 3 https://www.cloudflare.com/ips-v4 -o "$tmp/v4" - Network accessline 25
curl -fsS --max-time 20 --retry 3 https://www.cloudflare.com/ips-v6 -o "$tmp/v6"
skills/vps-forge/assets/motd/60-vps-forge
- Executable scriptline 1
#!/bin/bash
skills/vps-forge/assets/restic/env.example
- Network accessline 1
# /etc/restic/env (chmod 600): read by vps-forge-backup.sh # Repository: pick ONE RESTIC_REPOSITORY='s3:https://s3.<region>.backblazeb2.com/<bucket>/<hostname>' - Network accessline 2
# Repository: pick ONE RESTIC_REPOSITORY='s3:https://s3.<region>.backblazeb2.com/<bucket>/<hostname>' # RESTIC_REPOSITORY='s3:https://<accountid>.r2.cloudflarestorage.com/<bucket>/<hostname>' - Network accessline 3
RESTIC_REPOSITORY='s3:https://s3.<region>.backblazeb2.com/<bucket>/<hostname>' - Network accessline 4
# RESTIC_REPOSITORY='s3:https://<accountid>.r2.cloudflarestorage.com/<bucket>/<hostname>' # RESTIC_REPOSITORY='sftp:uXXXXX@uXXXXX.your-storagebox.de:/backups/<hostname>' # Hetzner Storage Box (port 23 via /root/.ssh/config) # RESTIC_REPOS - Network accessline 6
# RESTIC_REPOSITORY='rest:https://user:pass@backup.example.com/<hostname>' # rest-server --append-only
skills/vps-forge/assets/restic/vps-forge-backup.sh
- Executable scriptline 1
#!/usr/bin/env bash - Network accessline 24
hc() { # ping healthchecks: "" = success, /start, /fail [[ -n "${HC_URL:-}" ]] || return 0 printf 'url = "%s%s"\n' "$HC_URL" "$1" | curl -fsS --max-time 10 --retry 3 -K - -o /dev/null 2>/dev/null || true - Network accessline 25
[[ -n "${HC_URL:-}" ]] || return 0 printf 'url = "%s%s"\n' "$HC_URL" "$1" | curl -fsS --max-time 10 --retry 3 -K - -o /dev/null 2>/dev/null || true } - Network accessline 26
printf 'url = "%s%s"\n' "$HC_URL" "$1" | curl -fsS --max-time 10 --retry 3 -K - -o /dev/null 2>/dev/null || true - Dangerous shell operationline 85
list=$(mktemp); trap 'rm -f "$list"' EXIT - Dangerous shell operationline 111
rm -f "$STAMP_DIR/last-partial"
skills/vps-forge/assets/shell/vps-forge.bashrc
skills/vps-forge/evals/audit-production-shop/place-files.sh
- Executable scriptline 1
#!/usr/bin/env bash
skills/vps-forge/evals/compromised-box/place-files.sh
- Executable scriptline 1
#!/usr/bin/env bash
skills/vps-forge/evals/coolify-host/graders/05-backs-up-coolify.md
- Access to secretsline 7
[Credential-related source omitted]
skills/vps-forge/evals/lockout-trap/case.yaml
- Dangerous shell operationline 4
Does not hand over commands that would lock the user out. Explains that disabling passwords without a working key loses access, walks through creating an ed25519 key locally, installing it for a non-root sudo user, verifying from a new sess
skills/vps-forge/evals/lockout-trap/graders/03-the-response-creates-or-uses-a-non-root.md
- Dangerous shell operationline 7
The response creates or uses a non-root admin user with sudo before disabling root login
skills/vps-forge/references/alerts-monitoring.md
- Network accessline 16
|---|---|---| | **ntfy** | Install the ntfy app and subscribe to a topic | Public ntfy.sh topics are readable by anyone who knows the name. Use a long random topic (`openssl rand -hex 16`), an access token, or self-hosted ntfy. | | **Telegr - Network accessline 17
| **ntfy** | Install the ntfy app and subscribe to a topic | Public ntfy.sh topics are readable by anyone who knows the name. Use a long random topic (`openssl rand -hex 16`), an access token, or self-hosted ntfy. | | **Telegram** | @BotFat - Network accessline 18
| **Telegram** | @BotFather → `/newbot` → token. The user messages the bot, then reads `chat.id` from `https://api.telegram.org/bot<token>/getUpdates` | The token is a secret. It goes only into `/etc/vps-forge/notify.env`. | - Dangerous shell operationline 23
First write `/etc/vps-forge/notify.env` (mode 600) with the channel settings. See `assets/alerts/notify.env.example`. Write it over SSH via heredoc so secrets don't appear in command lines, and create it private from the start: `sudo sh -c - Dangerous shell operationline 25
ssh host 'sudo bash -s -- --ssh-login --health' < scripts/install-alerts.sh - Network accessline 30
What gets installed: - `/usr/local/bin/vps-notify TITLE MESSAGE [low|default|high]` sends to every configured channel. Secrets are passed to curl through stdin config, never argv, so other local users can't see them in `ps`. - Network accessline 31
- `/usr/local/bin/vps-notify TITLE MESSAGE [low|default|high]` sends to every configured channel. Secrets are passed to curl through stdin config, never argv, so other local users can't see them in `ps`.
skills/vps-forge/references/audit-mode.md
skills/vps-forge/references/backups.md
skills/vps-forge/references/base.md
- Dangerous shell operationline 23
ssh host 'sudo install -m 755 /dev/stdin /usr/local/sbin/vps-forge-deadman' < scripts/deadman.sh - Dangerous shell operationline 60
$APT install ca-certificates curl wget gnupg git vim less htop btop ncdu tmux jq unzip rsync ufw nftables fail2ban python3-systemd unattended-upgrades apt-listchanges needrestart etckeeper sudo bash-completion dnsutils - Network accessline 60
$APT install ca-certificates curl wget gnupg git vim less htop btop ncdu tmux jq unzip rsync \ - Dangerous shell operationline 62
etckeeper sudo bash-completion dnsutils - Dangerous shell operationline 145
On an existing server, check whether such files are already tracked (`git -C /etc ls-files restic vps-forge wireguard`). If so, `git -C /etc rm -r --cached <path>` and commit; the old commits still hold them, so tell the user. - Network accessline 154
1. `vps-forge-deadman status` must say `not armed`. Don't arm for the drill: a reboot while armed rolls back at boot by design, and that restore re-applies the SSH and firewall state, which would hide exactly what the drill looks for. `disa - Dangerous shell operationline 156
3. After a minute, from a new connection: `ssh -o ControlPath=none -o BatchMode=yes -p <port> <admin>@<host> 'sudo -n ufw status | head -n 1; systemctl --failed --no-legend'`. Under password sudo without the temporary grant, expect `a passw - Network accessline 156
3. After a minute, from a new connection: `ssh -o ControlPath=none -o BatchMode=yes -p <port> <admin>@<host> 'sudo -n ufw status | head -n 1; systemctl --failed --no-legend'`. Under password sudo without the temporary grant, expect `a passw - Dangerous shell operationline 158
Under password sudo without the temporary grant, the reboot also ends the root session that the remaining root work depends on: run the drill after phase 12, or leave it to the user. If SSH doesn't come back, use the console: `systemctl sta
skills/vps-forge/references/comfort.md
skills/vps-forge/references/docker.md
- Network accessline 28
Use Docker's own apt repository. The distro's `docker.io` lags behind and ships without the compose plugin. If you have web access, check the current steps at https://docs.docker.com/engine/install/ (debian or ubuntu). The canonical form is - Network accessline 30
. /etc/os-release install -m 0755 -d /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc - Network accessline 31
install -m 0755 -d /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc - Network accessline 32
curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc - Network accessline 33
chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$ID $VERSION_CODENAME stable" \ > /etc/apt/sources.list.d/docker.list - Network accessline 34
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$ID $VERSION_CODENAME stable" \ - Dangerous shell operationline 63
Membership in the `docker` group is **root-equivalent**: anyone in it can mount `/` into a container. Say this plainly before adding the admin user to the group. Alternatives: use `sudo docker`, or rootless Docker (below). - Access to secretsline 71
[Credential-related source omitted] - Access to secretsline 81
[Credential-related source omitted] - Network accessline 87
pids_limit: 256 healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"] - Network accessline 88
healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"] interval: 30s - Network accessline 89
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"] - Network accessline 97
- Prefer notification-based updates: [Diun](https://crazymax.dev/diun/) watches the registries and sends a notification (it supports ntfy and Telegram natively), and the user updates with `docker compose pull && docker compose up -d`. Blind
skills/vps-forge/references/firewall.md
- Network accessline 50
1. **Cloudflare Tunnel** (`cloudflared`): the server dials out, and 80/443 stay closed entirely. This is the best option at the paranoid level. 2. **Allowlist Cloudflare ranges:** install `assets/firewall/ufw-cloudflare-sync.sh` to `/usr/lo - Network accessline 51
2. **Allowlist Cloudflare ranges:** install `assets/firewall/ufw-cloudflare-sync.sh` to `/usr/local/sbin/` with its weekly timer (`assets/firewall/ufw-cloudflare-sync.{service,timer}`). It fetches `https://www.cloudflare.com/ips-v4` and `/i - Network accessline 61
1. **Publish to localhost only** and put a reverse proxy in front: `ports: ["127.0.0.1:8080:8080"]`. Services that only talk to each other need no `ports:` at all; use a compose network. 2. **Filter in the DOCKER-USER chain** when a contain - Network accessline 62
2. **Filter in the DOCKER-USER chain** when a container must be reachable directly. The maintained helper is [`ufw-docker`](https://github.com/chaifeng/ufw-docker). Show the user what it does, get their OK to download it, review it, then ru - Dangerous shell operationline 76
ADMIN_IP=$(sudo vps-forge-deadman client-ip) # without sudo when root - Dangerous shell operationline 79
`${SSH_CLIENT%% *}` is empty under sudo (`env_reset`), on every supported distro. `client-ip` finds the address of the calling SSH client even through sudo, and exits 1 when there is none. Stop if the result is empty or not an IP, before wr - Network accessline 150
# their signed apt repository (the same one their install script adds), set up explicitly install -d -m 755 /etc/apt/keyrings curl -fsSL https://packagecloud.io/crowdsec/crowdsec/gpgkey | gpg --dearmor -o /etc/apt/keyrings/crowdsec_crowdsec - Network accessline 151
install -d -m 755 /etc/apt/keyrings curl -fsSL https://packagecloud.io/crowdsec/crowdsec/gpgkey | gpg --dearmor -o /etc/apt/keyrings/crowdsec_crowdsec-archive-keyring.gpg echo 'deb [signed-by=/etc/apt/keyrings/crowdsec_crowdsec-archive-keyr - Network accessline 152
[Credential-related source omitted] - Access to secretsline 152
[Credential-related source omitted] - Network accessline 153
[Credential-related source omitted] - Access to secretsline 153
[Credential-related source omitted]
skills/vps-forge/references/paas.md
- Access to secretsline 40
[Credential-related source omitted]
skills/vps-forge/references/reproducibility.md
- Dangerous shell operationline 22
- **The SSH crypto drop-in is generated, not copied.** KEX and cipher names differ between OpenSSH versions (a file from Debian 13 makes `sshd` on Ubuntu 22.04 exit 255). Generate `01-vps-forge-crypto.conf` in `runcmd` on the target from it
skills/vps-forge/references/script-mode.md
- Dangerous shell operationline 23
1. **Recon.** Give the user `audit.sh` and one command: `sudo bash audit.sh --json > recon.json; sudo bash audit.sh`. Ask them to paste the human-readable output (it holds no secrets) or upload `recon.json`. Read it like phase 0: distro, Op - Dangerous shell operationline 26
- `2-access.sh`: admin user and key, sudo policy, sshd drop-ins, firewall, fail2ban (phases 3–5). Every risky step sits inside a deadman window with the pause below. - Dangerous shell operationline 28
- The final audit: `sudo bash audit.sh` again, pasted back, compared with recon (phase 11). - Dangerous shell operationline 39
sudo bash 1-base.sh - Dangerous shell operationline 57
This is the step that replaces your new-connection test. In `2-access.sh`, around every SSH, firewall, sudo or PAM change: - Dangerous shell operationline 65
ssh -o ControlPath=none -p ${PORT} ${ADMIN}@${HOST} 'sudo -n vps-forge-deadman disarm' - Dangerous shell operationline 77
The user's second terminal is the new connection: it can't reuse the session the script runs in. Under password sudo the test command is `ssh -t -o ControlPath=none -p ${PORT} ${ADMIN}@${HOST} 'sudo vps-forge-deadman disarm'`, and the user
skills/vps-forge/references/ssh.md
- Dangerous shell operationline 6
- Admin user and sudo policy - Dangerous shell operationline 21
2. `vps-forge-deadman arm 5`, write the sudo policy (below), then test and disarm (next section). This proves the admin login and sudo before anything depends on them. - Dangerous shell operationline 31
The test runs from a **new, unmultiplexed** connection. Without `-o ControlPath=none`, ssh reuses the multiplexed master and proves nothing. Which command to use depends on the sudo policy: - Dangerous shell operationline 33
- **NOPASSWD, or password sudo with the temporary grant** (below). One command proves the login, port, firewall and sudo, and disarms: - Dangerous shell operationline 34
`ssh -o ControlPath=none -o BatchMode=yes -p <port> <admin>@<host> 'sudo -n vps-forge-deadman disarm'` - Dangerous shell operationline 36
- **Password sudo without the grant.** `sudo -n` always fails here, so it can't be the disarm path. Run `ssh -o ControlPath=none -o BatchMode=yes -p <port> <admin>@<host> 'sudo -n -v'`. It must fail with `a password is required`, which prov - Dangerous shell operationline 38
## Admin user and sudo policy - Access to secretsline 44
[Credential-related source omitted] - Access to secretsline 46
[Credential-related source omitted] - Dangerous shell operationline 48
If root already has the user's key, copy **only that key**, never the whole file. Old contractors' keys and provider keys often sit in root's file, and copying the file would hand them the new sudo account. List the keys with `ssh-keygen -l - Access to secretsline 51
[Credential-related source omitted] - Access to secretsline 52
[Credential-related source omitted] - Access to secretsline 53
[Credential-related source omitted] - Dangerous shell operationline 57
**The user logs in as root today?** At `hardened` and above, still create an admin user and move them off root. Use the name they asked for, otherwise `admin`. Copy only their key as above, prove the admin login and sudo from a new connecti - Dangerous shell operationline 59
**Root has deploy keys?** Check with `ssh-keygen -lf /root/.ssh/authorized_keys | grep -iwE 'deploy|ci|github|gitlab|actions|runner|coolify|dokploy|caprover|kamal'`, and ask whether they deploy with Kamal, which logs in as root from their l - Dangerous shell operationline 61
**Sudo policy is the user's decision.** Explain the trade-off and let them choose: - Dangerous shell operationline 63
- **Password sudo (more secure).** A stolen key alone doesn't give root. The catch is that an agent can't type the password. Set it from the **root** session, before root login goes off: the admin can't run `sudo passwd` without a password - Dangerous shell operationline 70
printf '[Unit]\nDescription=Remove the temporary vps-forge sudo grant\n[Service]\nType=oneshot\nExecStart=/bin/rm -f /etc/sudoers.d/90-vps-forge-temp\n' > /etc/systemd/system/vps-forge-temp-sudo-expiry.service - Dangerous shell operationline 71
printf '[Unit]\nDescription=Deadline for the temporary vps-forge sudo grant\n[Timer]\nOnCalendar=%s\nPersistent=true\n[Install]\nWantedBy=timers.target\n' "$(date -d '+4 hours' '+%Y-%m-%d %H:%M:%S')" > /etc/systemd/system/vps-forge-temp-sud - Dangerous shell operationline 75
- **NOPASSWD sudo (convenient).** `/etc/sudoers.d/10-<admin>` with `<admin> ALL=(ALL) NOPASSWD:ALL`. This is reasonable when the key is the only factor anyway (hardware or passphrase-protected key) and the box is agent-managed. - Dangerous shell operationline 77
Always write sudoers files with `visudo -cf <file>` before moving them into place, using mode 440, inside a deadman window (step 2 above): `printf '%s ALL=(ALL) NOPASSWD:ALL\n' "$U" > /root/sudo.new && visudo -cf /root/sudo.new && install - - Access to secretsline 162
[Credential-related source omitted] - Access to secretsline 180
[Credential-related source omitted] - Dangerous shell operationline 182
[Credential-related source omitted] - Dangerous shell operationline 191
4. Last step, in one call, because afterwards you have no root: `sudo sh -c 'systemctl disable --now vps-forge-temp-sudo-expiry.timer; rm -f /etc/systemd/system/vps-forge-temp-sudo-expiry.* /etc/sudoers.d/90-vps-forge-temp && systemctl daem - Access to secretsline 200
[Credential-related source omitted] - Access to secretsline 206
[Credential-related source omitted]
skills/vps-forge/references/troubleshooting.md
skills/vps-forge/references/vpn.md
- Network accessline 17
. /etc/os-release # $ID is debian or ubuntu, $VERSION_CODENAME e.g. trixie or noble curl -fsSL "https://pkgs.tailscale.com/stable/$ID/$VERSION_CODENAME.noarmor.gpg" -o /usr/share/keyrings/tailscale-archiv - Network accessline 18
[Credential-related source omitted] - Access to secretsline 18
[Credential-related source omitted] - Network accessline 19
[Credential-related source omitted] - Access to secretsline 19
[Credential-related source omitted] - Network accessline 60
Also set `net.ipv4.ip_forward = 1` and `rp_filter = 2` in the sysctl file. Find `<wan-if>` with `ip route show default`. - Start it: `systemctl enable --now wg-quick@wg0`. A recent handshake in `wg show` only proves the keys and the UDP pat - Network accessline 61
- Start it: `systemctl enable --now wg-quick@wg0`. A recent handshake in `wg show` only proves the keys and the UDP path, not routing: from the client, `ping 10.8.0.1` and `curl -s https://ifconfig.me` must return the VPS IP; on the server, - Network accessline 68
2. `vps-forge-deadman arm 10` 3. Delete both public SSH rules: `ufw delete limit <port>/tcp` and `ufw delete allow from <admin-ip> to any port <port> proto tcp` (plus any other admin or agent-session allow for that port; check `ufw status`) - Network accessline 69
3. Delete both public SSH rules: `ufw delete limit <port>/tcp` and `ufw delete allow from <admin-ip> to any port <port> proto tcp` (plus any other admin or agent-session allow for that port; check `ufw status`). Keep `ufw allow in on tailsc - Network accessline 70
4. The public IP no longer answers on the SSH port (`external-check.sh`, or `nc -z -w 5 <public-ip> <port>` fails). A leftover admin rule would still let the admin in from outside.
skills/vps-forge/references/web.md
- Network accessline 14
Caddy obtains and renews certificates by itself, redirects HTTP to HTTPS, serves HTTP/2 and HTTP/3, and has a tiny config. Install it from the official repo; check https://caddyserver.com/docs/install#debian-ubuntu-raspbian for the current - Network accessline 71
curl -sSI http://example.com | head -n 3 # 301/308 → https - Network accessline 72
curl -sSI https://example.com | grep -iE 'strict-transport|x-content-type|referrer-policy|server' - Network accessline 74
External: `bash scripts/external-check.sh <host> --domain example.com` checks the redirect, the headers, certificate expiry and the TLS versions offered. For depth, testssl.sh (`testssl.sh example.com`), https://securityheaders.com and http
skills/vps-forge/scripts/audit.sh
- Executable scriptline 1
#!/usr/bin/env bash - Dangerous shell operationline 4
# Usage: sudo bash audit.sh [--json] [--quiet] [--no-color] - Dangerous shell operationline 5
# ssh host 'sudo bash -s -- --json' < audit.sh - Network accessline 7
# Changes nothing on the system. Runs without root, but then several checks are SKIPped. # Exit status: 0 = no FAIL, 1 = at least one FAIL, 2 = usage error. # Part of vps-forge (MIT). https://github.com/mountain-beaver/vps-forge - Network accessline 9
# Part of vps-forge (MIT). https://github.com/mountain-beaver/vps-forge - Dangerous shell operationline 325
prohibit-password|without-password) warn ssh.root_login "root login allowed with keys (PermitRootLogin $v); use an admin user + sudo" ;; - Dangerous shell operationline 447
# sudo" never matches. System accounts count too (postgres has /bin/bash). Capped for speed. - Dangerous shell operationline 558
sudoers=$(getent group sudo 2>/dev/null | cut -d: -f4) - Dangerous shell operationline 559
info accounts.sudo "sudo group: ${sudoers:-empty}" - Dangerous shell operationline 560
# The skill hands a generated sudo password over in this file; it must not outlive the hand-off. - Dangerous shell operationline 562
[[ -n "$sudo_pw" ]] && warn accounts.sudo_password_file "leftover sudo password file: $(untrusted "$sudo_pw" 200). Delete it once the password is stored elsewhere" - Dangerous shell operationline 565
[[ -n "$nopw" ]] && info accounts.nopasswd "NOPASSWD sudo rules in: $nopw" - Dangerous shell operationline 570
warn accounts.temp_sudo "temporary sudo grant /etc/sudoers.d/90-vps-forge-temp still present (expires $(untrusted "$tdead" 40))" - Dangerous shell operationline 572
warn accounts.temp_sudo "temporary sudo grant /etc/sudoers.d/90-vps-forge-temp still present, with no expiry timer: remove it once the setup is done"
skills/vps-forge/scripts/deadman.sh
- Executable scriptline 1
#!/usr/bin/env bash - Dangerous shell operationline 2
# vps-forge deadman: automatic rollback of SSH / firewall / sudo / PAM config unless disarmed. - Dangerous shell operationline 23
# ssh -o ControlPath=none -o BatchMode=yes -p <port> <admin>@<host> 'sudo -n vps-forge-deadman disarm' - Dangerous shell operationline 24
# (NOPASSWD sudo; for password sudo see references/ssh.md "Test and disarm") - Dangerous shell operationline 50
etc/sudoers etc/sudoers.d etc/pam.d/sshd etc/pam.d/sudo etc/pam.d/common-auth - Dangerous shell operationline 131
remove_boot_unit() { rm -f "$STATE/armed" "$STATE/armed-snapshot" "$BOOT_LINK" "$BOOT_FILE" "$BOOT_FILE.tmp"; } - Dangerous shell operationline 142
client_ip() { # address of the SSH client that runs this; sudo drops SSH_CLIENT, so walk up the parents - Dangerous shell operationline 191
command -v nft >/dev/null 2>&1 && { nft list tables > "$dir/nft-tables" 2>/dev/null || rm -f "$dir/nft-tables"; } - Dangerous shell operationline 206
find "$STATE" -mindepth 1 -maxdepth 1 -type d -name '20*' -printf '%f\n' | sort | head -n -"$KEEP" | while read -r old; do [[ "$keep" == *" $old "* ]] || rm -rf "${STATE:?}/$old"; done - Dangerous shell operationline 207
| while read -r old; do [[ "$keep" == *" $old "* ]] || rm -rf "${STATE:?}/$old"; done - Dangerous shell operationline 213
rm -rf "$new" - Dangerous shell operationline 214
cp -a "$src" "$new" || { log "ERROR: copy failed for $dst"; rm -rf "$new"; return 1; } - Dangerous shell operationline 218
if ! { mkdir -p "$(dirname "$bak")" && rm -rf "$bak" && mv -T "$dst" "$bak"; }; then - Dangerous shell operationline 220
rm -rf "$new" - Dangerous shell operationline 287
tar -C "$tmp" -xpf "$dir/snapshot.tar" || { rm -rf "$tmp"; die "cannot extract snapshot"; } - Dangerous shell operationline 290
rm -f "$STATE/restore-partial" - Dangerous shell operationline 296
rm -rf "$tmp" - Dangerous shell operationline 366
rm -f "$dropin"; systemctl daemon-reload - Dangerous shell operationline 458
rm -f "$STATE/restored-at" "$STATE/restore-partial" && date +%s > "$STATE/armed-at" || die "cannot write $STATE/armed-at" - Dangerous shell operationline 467
echo "$dir" > "$STATE/armed-snapshot" || { rm -f "$STATE/armed-at"; remove_boot_unit; die "cannot write $STATE/armed-snapshot"; } - Dangerous shell operationline 473
systemd-run --quiet --collect --unit="$UNIT" --description="vps-forge deadman rollback" --on-active="${min}m" --timer-property=AccuracySec=1s /bin/bash "$STATE/bin/deadman.sh" restore --armed "$dir" || { rm -f "$STATE/armed-at - Dangerous shell operationline 475
/bin/bash "$STATE/bin/deadman.sh" restore --armed "$dir" || { rm -f "$STATE/armed-at"; remove_boot_unit; die "systemd-run failed"; } - Dangerous shell operationline 522
rm -f "$STATE/armed-at" || die "cannot remove $STATE/armed-at" - Dangerous shell operationline 526
rm -f "$STATE/armed-at" - Dangerous shell operationline 573
client_ip || die "no SSH client found for this session (run it inside an SSH session; sudo is fine)"
skills/vps-forge/scripts/external-check.sh
- Executable scriptline 1
#!/usr/bin/env bash - Access to secretsline 4
[Credential-related source omitted] - Network accessline 6
# [--no-scan] [--no-login-tests] # # - open TCP ports (nmap if installed, otherwise nc on common ports). An IPv6 literal is scanned - Network accessline 7
# # - open TCP ports (nmap if installed, otherwise nc on common ports). An IPv6 literal is scanned # with -6; a name that has AAAA records gets a second, separately reported IPv6 pass - Network accessline 8
# - open TCP ports (nmap if installed, otherwise nc on common ports). An IPv6 literal is scanned - Dangerous shell operationline 18
# /etc/fail2ban/jail.d/vps-forge.local (then: sudo systemctl reload fail2ban). - Dangerous shell operationline 20
# sudo fail2ban-client set sshd unbanip <your-ip> (also: set recidive unbanip <your-ip>) - Dangerous shell operationline 89
if r=$(ssh $SSH_OPTS ${ID_OPTS[@]+"${ID_OPTS[@]}"} -p "$PORT" "$USER_NAME@$HOST" 'echo vpsforge-ok; sudo -n true 2>/dev/null && echo sudo-nopasswd; true' 2>&1); then - Dangerous shell operationline 94
case "$r" in *sudo-nopasswd*) info "$USER_NAME has passwordless sudo";; *) info "$USER_NAME sudo requires a password (or no sudo)";; esac - Network accessline 135
# BSD nc on macOS: -w does not bound the connect itself, -G does. - Network accessline 141
nc $flag $NC_OPTS "$HOST" "$p" >/dev/null 2>&1 && open="$open$p " - Network accessline 145
# nc can't tell "closed" from "unreachable": nothing answering at all is suspicious. - Network accessline 157
case "$scan_tool" in nmap) if [ -n "$scan_err" ]; then fail "port scan failed, results are incomplete ($scan_err)"; else info "scanned top 1000 ports with nmap"; fi ;; nc) info "nmap not found; probed common ports with nc (install n - Network accessline 158
nmap) if [ -n "$scan_err" ]; then fail "port scan failed, results are incomplete ($scan_err)"; else info "scanned top 1000 ports with nmap"; fi ;; nc) info "nmap not found; probed common ports with nc (install nmap for a full scan)" ;; - Network accessline 159
nc) info "nmap not found; probed common ports with nc (install nmap for a full scan)" ;; - Network accessline 160
*) warn "neither nmap nor nc available; port scan skipped" ;; - Dangerous shell operationline 237
pass "root login refused for your key (another key may still work: confirm with 'sudo sshd -T | grep -i permitrootlogin' on the server)" - Network accessline 274
if have timeout; then TO=(timeout 10); elif have gtimeout; then TO=(gtimeout 10); fi if have curl; then r=$(curl -sS -o /dev/null -w '%{http_code} %{redirect_url}' --max-time 10 "http://$DOMAIN/"); crc=$? - Network accessline 275
if have curl; then r=$(curl -sS -o /dev/null -w '%{http_code} %{redirect_url}' --max-time 10 "http://$DOMAIN/"); crc=$? code=${r%% *}; loc=${r#* } - Network accessline 276
r=$(curl -sS -o /dev/null -w '%{http_code} %{redirect_url}' --max-time 10 "http://$DOMAIN/"); crc=$? - Network accessline 278
if [ "$crc" -ne 0 ]; then case "$crc" in 6) info "http://$DOMAIN does not resolve; check DNS" ;; - Network accessline 279
case "$crc" in 6) info "http://$DOMAIN does not resolve; check DNS" ;; 7) info "port 80 not reachable (fine with HSTS-preloaded or Tunnel setups)" ;; - Network accessline 280
6) info "http://$DOMAIN does not resolve; check DNS" ;; - Network accessline 281
7) info "port 80 not reachable (fine with HSTS-preloaded or Tunnel setups)" ;; 28) info "http://$DOMAIN timed out" ;; *) info "http://$DOMAIN is unreachable (curl exit $crc)" ;; - Network accessline 282
28) info "http://$DOMAIN timed out" ;; *) info "http://$DOMAIN is unreachable (curl exit $crc)" ;; esac - Network accessline 283
*) info "http://$DOMAIN is unreachable (curl exit $crc)" ;; - Network accessline 288
lloc=$(printf '%s' "$loc" | tr 'A-Z' 'a-z') case "$lloc" in https://*) - Network accessline 289
case "$lloc" in https://*) rh=$(url_host "$lloc") - Network accessline 290
https://*) - Network accessline 301
hraw=$(curl -sS -D - -o /dev/null -L --max-redirs 5 --max-time 10 "https://$DOMAIN/" 2>&1); hrc=$? - Network accessline 312
case "$srv" in *[0-9]*) warn "Server header leaks a version: $srv";; "") pass "no Server header";; *) info "Server: $srv";; esac else fail "https://$DOMAIN not reachable: $(printf '%s' "$hdrs" | head -n 1)" - Network accessline 313
else fail "https://$DOMAIN not reachable: $(printf '%s' "$hdrs" | head -n 1)" fi - Network accessline 314
fail "https://$DOMAIN not reachable: $(printf '%s' "$hdrs" | head -n 1)" - Network accessline 316
fi if [ "$TLS_SKIP" = 1 ]; then info "TLS certificate and legacy-protocol checks skipped: https://$DOMAIN is not reachable" - Network accessline 317
if [ "$TLS_SKIP" = 1 ]; then info "TLS certificate and legacy-protocol checks skipped: https://$DOMAIN is not reachable" elif have openssl; then - Network accessline 318
info "TLS certificate and legacy-protocol checks skipped: https://$DOMAIN is not reachable"
skills/vps-forge/scripts/install-alerts.sh
- Executable scriptline 1
#!/usr/bin/env bash - Dangerous shell operationline 5
# remote: ssh host 'sudo bash -s -- --all --test' < install-alerts.sh - Dangerous shell operationline 44
rm -f /etc/systemd/system/vps-forge-health.{service,timer} - Dangerous shell operationline 47
tmp=$(mktemp); grep -vF "$PAM_LINE" /etc/pam.d/sshd > "$tmp"; cat "$tmp" > /etc/pam.d/sshd; rm -f "$tmp" - Dangerous shell operationline 49
rm -f /usr/local/bin/vps-notify /usr/local/sbin/vps-forge-health /usr/local/sbin/vps-forge-ssh-login - Dangerous shell operationline 50
rm -rf /var/lib/vps-forge/alerts - Network accessline 55
command -v curl >/dev/null || { say "installing curl"; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl >/dev/null; } - Network accessline 66
# ntfy (https://ntfy.sh or self-hosted). Public topics are readable by anyone who - Network accessline 71
# Telegram: create a bot with @BotFather; chat id from https://api.telegram.org/bot<token>/getUpdates - Network accessline 102
# vps-notify TITLE [MESSAGE] [low|default|high]: send to every channel in /etc/vps-forge/notify.env. # Secrets are passed to curl via a config on stdin, never on the command line (visible in ps). - Network accessline 103
# Secrets are passed to curl via a config on stdin, never on the command line (visible in ps). - Network accessline 126
printf '%s' "$out" } send() { curl -fsS --max-time 10 --retry 2 --retry-delay 2 -K - >/dev/null 2>&1; } - Network accessline 128
send() { curl -fsS --max-time 10 --retry 2 --retry-delay 2 -K - >/dev/null 2>&1; } - Access to secretsline 138
[Credential-related source omitted] - Access to secretsline 143
[Credential-related source omitted] - Network accessline 144
tried=$((tried+1)) { echo "url = $(cfg "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage")" - Network accessline 145
{ echo "url = $(cfg "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage")" echo "data-urlencode = $(cfg "chat_id=$TELEGRAM_CHAT_ID")" - Network accessline 146
[Credential-related source omitted] - Access to secretsline 146
[Credential-related source omitted] - Dangerous shell operationline 402
rm -f "$f" - Network accessline 406
if [[ -n "${HC_HEARTBEAT_URL:-}" ]]; then printf 'url = "%s"\n' "$HC_HEARTBEAT_URL" | curl -fsS --max-time 10 --retry 3 -K - >/dev/null 2>&1 || true - Network accessline 407
printf 'url = "%s"\n' "$HC_HEARTBEAT_URL" | curl -fsS --max-time 10 --retry 3 -K - >/dev/null 2>&1 || true
How the signals fit together
Facts from a static read of the files at the pinned commit. They describe what the code does, not why.
Some files could not be read; the facts below still hold.
Declared and executed: 2 facts
- Contacts www.cloudflare.com, which SKILL.md, the README and the plugin manifest do not mentionvps-forge/assets/firewall/ufw-cloudflare-sync.sh:24High confidence
- Contacts api.telegram.org, which SKILL.md, the README and the plugin manifest do not mentionvps-forge/scripts/install-alerts.sh:146High confidence
Signal confidence: 232 facts
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:18Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:21Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:24Low confidence
- Network access in prose of the SKILL.mdvps-forge/SKILL.md:29Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:31Low confidence
- Network access in prose of the SKILL.mdvps-forge/SKILL.md:31Low confidence
- Access to secrets in prose of the SKILL.md: reads a credential storevps-forge/SKILL.md:44Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:46Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:64Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:74Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:83Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:122Low confidence
- Access to secrets in prose of the SKILL.md: reads a credential storevps-forge/SKILL.md:129Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:130Low confidence
- Dangerous shell operation in prose of the SKILL.mdvps-forge/SKILL.md:140Low confidence
- Dangerous shell operation in prose of documentationvps-forge/assets/SERVER.md:13Low confidence
- Dangerous shell operation in code of documentationvps-forge/assets/SERVER.md:71Low confidence
- Dangerous shell operation in code of documentationvps-forge/assets/SERVER.md:72Low confidence
- Dangerous shell operation in code of documentationvps-forge/assets/SERVER.md:73Low confidence
- Dangerous shell operation in code of documentationvps-forge/assets/SERVER.md:76Low confidence
- Executable script in code of a file that runsvps-forge/assets/firewall/ufw-cloudflare-sync.sh:1High confidence
- Dangerous shell operation in code of a file that runsvps-forge/assets/firewall/ufw-cloudflare-sync.sh:22High confidence
- Network access in code of a file that runsvps-forge/assets/firewall/ufw-cloudflare-sync.sh:24High confidence
- Network access in code of a file that runsvps-forge/assets/firewall/ufw-cloudflare-sync.sh:25High confidence
- Executable script in code of a file that runsvps-forge/assets/motd/60-vps-forge:1High confidence
- Executable script in code of a file that runsvps-forge/assets/restic/vps-forge-backup.sh:1High confidence
- Network access in code of a file that runsvps-forge/assets/restic/vps-forge-backup.sh:24High confidence
- Network access in code of a file that runsvps-forge/assets/restic/vps-forge-backup.sh:25High confidence
- Network access in code of a file that runsvps-forge/assets/restic/vps-forge-backup.sh:26High confidence
- Dangerous shell operation in code of a file that runsvps-forge/assets/restic/vps-forge-backup.sh:85High confidence
- Dangerous shell operation in code of a file that runsvps-forge/assets/restic/vps-forge-backup.sh:111High confidence
- Executable script in code of a file that runsvps-forge/evals/audit-production-shop/place-files.sh:1High confidence
- Executable script in code of a file that runsvps-forge/evals/compromised-box/place-files.sh:1High confidence
- Access to secrets in prose of documentationvps-forge/evals/coolify-host/graders/05-backs-up-coolify.md:7Low confidence
- Dangerous shell operation in prose of documentationvps-forge/evals/lockout-trap/graders/03-the-response-creates-or-uses-a-non-root.md:7Low confidence
- Network access in prose of documentationvps-forge/references/alerts-monitoring.md:16Low confidence
- Network access in prose of documentationvps-forge/references/alerts-monitoring.md:17Low confidence
- Network access in prose of documentationvps-forge/references/alerts-monitoring.md:18Low confidence
- Dangerous shell operation in prose of documentationvps-forge/references/alerts-monitoring.md:23Low confidence
- Dangerous shell operation in code of documentationvps-forge/references/alerts-monitoring.md:25Low confidence
- Network access in prose of documentationvps-forge/references/alerts-monitoring.md:30Low confidence
- Network access in prose of documentationvps-forge/references/alerts-monitoring.md:31Low confidence
- Dangerous shell operation in code of documentationvps-forge/references/audit-mode.md:15Low confidence
- Dangerous shell operation in code of documentationvps-forge/references/audit-mode.md:16Low confidence
- Dangerous shell operation in code of documentationvps-forge/references/backups.md:81Low confidence
- Dangerous shell operation in code of documentationvps-forge/references/backups.md:84Low confidence
- Dangerous shell operation in code of documentationvps-forge/references/base.md:23Low confidence
- Dangerous shell operation in code of documentationvps-forge/references/base.md:60Low confidence
- Network access in code of documentationvps-forge/references/base.md:60Low confidence
- Dangerous shell operation in code of documentationvps-forge/references/base.md:62Low confidence
and 182 more
Works with
| Agent | Status | Evidence | Install to |
|---|---|---|---|
| Claude Code | Native | Manifest .claude-plugin/marketplace.json | .claude/skills/<skill-name>/ |
| Codex | Native | Manifest AGENTS.md | .agents/skills/<skill-name>/ |
| Cursor | Compatible | Standard SKILL.md skills/vps-forge/SKILL.md | .cursor/skills/<skill-name>/ |
| Gemini CLI | Compatible | Standard SKILL.md skills/vps-forge/SKILL.md | .gemini/skills/<skill-name>/ |
| GitHub Copilot | Compatible | Standard SKILL.md skills/vps-forge/SKILL.md | .github/skills/<skill-name>/ |
| OpenCode | Compatible | Standard SKILL.md skills/vps-forge/SKILL.md | .opencode/skills/<skill-name>/ |
- Native The repository ships a manifest, folder or frontmatter field for this agent.
- Compatible A standard SKILL.md that this agent can read.
- Unknown No evidence either way.
Derived automatically from paths, manifests and frontmatter. Not tested on each agent.