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.

Compare on GitHub

View on GitHubAdd to my skills

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>/
38GitHub stars
Not availablestars in 7 days
1skill in this repository
14commits in 30 days

Heat

Insufficient history

Not 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

Nov 3, 2026Nov 3, 202638

30-day change not yet available: not enough history.

Individual skills in this repository

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 tokens

Always available to the agent

SKILL.md

5,038 tokens

When the skill is activated

Reference documents

37,368 tokens

Loaded on request

Token counts are estimates. Scripts run separately; they are not counted as reference context.

The playbook

  1. 1. Rules that keep the user safe
  2. 2. Operating modes
  3. 3. Intake (ask at most 4 questions at a time)
  4. 4. Workflow
  5. 5. Levels
  6. 6. Definition of done
  7. 7. Final report
  8. 8. Reference map

Scripts executed separately

  • assets/firewall/ufw-cloudflare-sync.sh
  • assets/motd/60-vps-forge
  • assets/restic/vps-forge-backup.sh
  • evals/audit-production-shop/place-files.sh
  • evals/compromised-box/place-files.sh
  • scripts/audit.sh
  • scripts/deadman.sh
  • scripts/external-check.sh
  • scripts/install-alerts.sh

Files

  • ../../.claude-plugin/icon.pngasset5,477 bytes
  • ../../.claude-plugin/marketplace.jsonasset342 bytes
  • ../../.claude-plugin/plugin.jsonasset1,133 bytes
  • SKILL.mdcore19,640 bytes
  • SKILL.mdcore19,640 bytes
  • SKILL.mdcore19,640 bytes
  • assets/SERVER.mddoc3,153 bytes
  • assets/alerts/notify.env.exampleasset1,151 bytes
  • assets/apt/20auto-upgradesasset179 bytes
  • assets/apt/52vps-forge-unattendedasset630 bytes
  • assets/caddy/Caddyfileasset1,620 bytes
  • assets/cloud-init.yamlasset6,324 bytes
  • assets/docker/daemon.jsonasset205 bytes
  • assets/fail2ban/vps-forge.localasset1,930 bytes
  • assets/firewall/ufw-cloudflare-sync.serviceasset224 bytes
  • assets/firewall/ufw-cloudflare-sync.shruns3,042 bytes
  • assets/firewall/ufw-cloudflare-sync.timerasset158 bytes
  • assets/journald/10-vps-forge.confasset216 bytes
  • assets/motd/60-vps-forgeruns2,426 bytes
  • assets/needrestart/50-vps-forge.confasset257 bytes
  • assets/restic/env.exampleasset871 bytes
  • assets/restic/excludeasset379 bytes
  • assets/restic/includeasset247 bytes
  • assets/restic/vps-forge-backup-maint.serviceasset354 bytes
  • assets/restic/vps-forge-backup-maint.timerasset166 bytes
  • assets/restic/vps-forge-backup.serviceasset288 bytes
  • assets/restic/vps-forge-backup.shruns5,399 bytes
  • assets/restic/vps-forge-backup.timerasset159 bytes
  • assets/shell/vps-forge.bashrcasset1,090 bytes
  • assets/ssh/00-vps-forge.confasset944 bytes
  • assets/sysctl/99-vps-forge.confasset2,495 bytes
  • evals/audit-production-shop/case.yamlasset481 bytes
  • evals/audit-production-shop/files/audit.jsonasset2,564 bytes
  • evals/audit-production-shop/graders/01-the-response-rates-the-publicly-publishe.mddoc154 bytes
  • evals/audit-production-shop/graders/02-the-response-flags-the-adminer-container.mddoc142 bytes
  • evals/audit-production-shop/graders/03-the-response-flags-the-unknown-or-old-ol.mddoc173 bytes
  • evals/audit-production-shop/graders/04-the-response-explains-that-the-password.mddoc185 bytes
  • evals/audit-production-shop/graders/05-the-response-mentions-the-missing-backup.mddoc128 bytes
  • evals/audit-production-shop/graders/06-the-response-does-not-run-or-instruct-im.mddoc176 bytes
  • evals/audit-production-shop/place-files.shruns62 bytes
  • evals/audit-production-shop/prompt.mddoc295 bytes
  • evals/compromised-box/case.yamlasset495 bytes
  • evals/compromised-box/files/audit.txtdoc900 bytes
  • evals/compromised-box/graders/01-the-response-identifies-that-the-server.mddoc240 bytes
  • evals/compromised-box/graders/02-the-response-does-not-proceed-with-routi.mddoc148 bytes
  • evals/compromised-box/graders/03-the-response-recommends-rebuilding-from.mddoc137 bytes
  • evals/compromised-box/graders/04-the-response-recommends-rotating-credent.mddoc138 bytes
  • evals/compromised-box/graders/05-the-response-recommends-preserving-evide.mddoc135 bytes
  • evals/compromised-box/place-files.shruns62 bytes
  • evals/compromised-box/prompt.mddoc265 bytes
  • evals/coolify-host/case.yamlasset627 bytes
  • evals/coolify-host/graders/01-root-key-login-kept.mddoc248 bytes
  • evals/coolify-host/graders/02-panel-ssh-not-banned.mddoc336 bytes
  • evals/coolify-host/graders/03-no-second-proxy.mddoc200 bytes
  • evals/coolify-host/graders/04-dashboard-port.mddoc246 bytes
  • evals/coolify-host/graders/05-backs-up-coolify.mddoc144 bytes
  • evals/coolify-host/prompt.mddoc390 bytes
  • evals/fresh-ubuntu-docker-web/case.yamlasset616 bytes
  • evals/fresh-ubuntu-docker-web/graders/01-the-sshd-hardening-drop-in-is-named-so-i.mddoc234 bytes
  • evals/fresh-ubuntu-docker-web/graders/02-password-authentication-and-root-login-a.mddoc194 bytes
  • evals/fresh-ubuntu-docker-web/graders/03-ssh-or-firewall-changes-are-wrapped-in-a.mddoc218 bytes
  • evals/fresh-ubuntu-docker-web/graders/04-the-app-container-is-published-on-127-0.mddoc205 bytes
  • evals/fresh-ubuntu-docker-web/graders/05-apt-commands-are-non-interactive-debian.mddoc159 bytes
  • evals/fresh-ubuntu-docker-web/graders/06-fail2ban-is-configured-with-the-systemd.mddoc140 bytes
  • evals/fresh-ubuntu-docker-web/graders/07-the-telegram-bot-token-is-placed-in-a-ro.mddoc162 bytes
  • evals/fresh-ubuntu-docker-web/prompt.mddoc492 bytes
  • evals/k3s-node-out-of-scope/case.yamlasset285 bytes
  • evals/k3s-node-out-of-scope/graders/01-stops.mddoc150 bytes
  • evals/k3s-node-out-of-scope/graders/02-explains-why.mddoc217 bytes
  • evals/k3s-node-out-of-scope/prompt.mddoc290 bytes
  • evals/local-nginx-no-trigger/case.yamlasset196 bytes
  • evals/local-nginx-no-trigger/graders/01-no-skill.mddoc132 bytes
  • evals/local-nginx-no-trigger/graders/02-answers.mddoc191 bytes
  • evals/local-nginx-no-trigger/prompt.mddoc256 bytes
  • evals/lockout-trap/case.yamlasset406 bytes
  • evals/lockout-trap/graders/01-the-response-warns-that-disabling-passwo.mddoc137 bytes
  • evals/lockout-trap/graders/02-the-response-includes-generating-an-ssh.mddoc185 bytes
  • evals/lockout-trap/graders/03-the-response-creates-or-uses-a-non-root.mddoc131 bytes
  • evals/lockout-trap/graders/04-the-response-requires-verifying-login-fr.mddoc162 bytes
  • evals/lockout-trap/graders/05-any-sshd-change-is-validated-with-sshd-t.mddoc98 bytes
  • evals/lockout-trap/prompt.mddoc283 bytes
  • evals/narrow-ssh-port/case.yamlasset437 bytes
  • evals/narrow-ssh-port/graders/01-does-only-what-was-asked.mddoc202 bytes
  • evals/narrow-ssh-port/graders/02-offers-the-rest.mddoc184 bytes
  • evals/narrow-ssh-port/graders/03-port-change-is-safe.mddoc225 bytes
  • evals/narrow-ssh-port/graders/04-socket-activation.mddoc196 bytes
  • evals/narrow-ssh-port/graders/05-no-self-ban.mddoc152 bytes
  • evals/narrow-ssh-port/prompt.mddoc286 bytes
  • references/alerts-monitoring.mddoc4,869 bytes
  • references/audit-mode.mddoc4,659 bytes
  • references/backups.mddoc6,878 bytes
  • references/base.mddoc12,449 bytes
  • references/comfort.mddoc2,228 bytes
  • references/databases.mddoc3,596 bytes
  • references/distro-notes.mddoc5,878 bytes
  • references/docker.mddoc8,485 bytes
  • references/firewall.mddoc14,532 bytes
  • references/kernel.mddoc6,980 bytes
  • references/paas.mddoc7,654 bytes
  • references/reproducibility.mddoc3,179 bytes
  • references/script-mode.mddoc6,607 bytes
  • references/ssh.mddoc21,751 bytes
  • references/troubleshooting.mddoc7,344 bytes
  • references/vpn.mddoc5,237 bytes
  • references/web.mddoc5,198 bytes
  • scripts/audit.shruns64,534 bytes
  • scripts/deadman.shruns30,926 bytes
  • scripts/external-check.shruns18,901 bytes
  • scripts/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 GitHub

Skills 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 shell

Automated 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:1 Analysis incomplete: Referenced executable target unavailable; not analyzed
  • skills/vps-forge/SKILL.md:1 Analysis incomplete: Referenced executable target unavailable; not analyzed
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

  • Network accessline 4
    # ntfy (https://ntfy.sh or self-hosted). Public topics are readable by anyone who
  • Network accessline 9
    # Telegram: create a bot with @BotFather; chat id from https://api.telegram.org/bot<token>/getUpdates

skills/vps-forge/assets/caddy/Caddyfile

  • Network accessline 7
    # Behind Cloudflare (proxied): trust its ranges so logs and rate limits see real client IPs. 	# Ranges: https://www.cloudflare.com/ips-v4 and https://www.cloudflare.com/ips-v6
  • Network accessline 8
    # Ranges: https://www.cloudflare.com/ips-v4 and https://www.cloudflare.com/ips-v6

skills/vps-forge/assets/cloud-init.yaml

  • Network accessline 32
    - etckeeper   - curl   - git
  • Network accessline 33
    - curl   - git   - htop
  • Dangerous shell operationline 166
    sshd -t || rm -f "$f"

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

  • Dangerous shell operationline 21
    alias ports='sudo ss -tulpn'
  • Dangerous shell operationline 22
    alias fw='sudo ufw status numbered'

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

  • Dangerous shell operationline 15
    ssh host 'sudo bash -s -- --json' < scripts/audit.sh > audit-before.json
  • Dangerous shell operationline 16
    ssh host 'sudo bash -s' < scripts/audit.sh          # human-readable view

skills/vps-forge/references/backups.md

  • Dangerous shell operationline 81
    rm -rf /root/restore-test && restic restore latest --target /root/restore-test --include "$F" ${DUMP:+--include "$DUMP"}
  • Dangerous shell operationline 84
    rm -rf /root/restore-test

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

  • Dangerous shell operationline 21
    - Safe aliases: `ll`, `ports` (`ss -tulpn`), `logs` (`journalctl -e`), `fw` (`sudo ufw status numbered`), `dps` (`docker ps --format` table).
  • Access to secretsline 32
    [Credential-related source omitted]
  • Access to secretsline 33
    [Credential-related source omitted]

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

  • Access to secretsline 19
    [Credential-related source omitted]
  • Access to secretsline 25
    [Credential-related source omitted]

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
Signal confidence: 232 facts

and 182 more

Works with

Agent compatibility, from the files in this repository
AgentStatusEvidenceInstall to
Claude CodeNativeManifest .claude-plugin/marketplace.json.claude/skills/<skill-name>/
CodexNativeManifest AGENTS.md.agents/skills/<skill-name>/
CursorCompatibleStandard SKILL.md skills/vps-forge/SKILL.md.cursor/skills/<skill-name>/
Gemini CLICompatibleStandard SKILL.md skills/vps-forge/SKILL.md.gemini/skills/<skill-name>/
GitHub CopilotCompatibleStandard SKILL.md skills/vps-forge/SKILL.md.github/skills/<skill-name>/
OpenCodeCompatibleStandard 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.