[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71HTJW8TXFTRESADAARS1NA":3,"skill-same:01M71HTJW8TXFTRESADAARS1NA":72},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":56,"ownerDescription":6,"updatedAt":57,"install":58,"resources":71},"01M71HTJW8TXFTRESADAARS1NA","harden-vps",null,"Harden a production Linux VPS for your application across three layers — application (systemd hardening, monitoring alerts, backup automation), Ubuntu OS (UFW firewall, fail2ban SSH, unattended-upgrades, SSH hardening), and VPS provider (health checks, daily backups, monthly snapshots). Use when the user wants to secure a production server, harden a VPS, audit server security, or mentions production hardening, VPS security, or harden the server.","danielvm-git\u002Fbigpowers","danielvm-git","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1020864?v=4","01M71H1N1CTQ529S0KN7KETVG6","Daniel V M","01M71H1N218Y3S8WHCJ58NPRSW",289,"MIT","https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers\u002Fblob\u002Fmain\u002F.copilot\u002Fskills\u002Fharden-vps\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"harden vps","harden","vps","for","three","unattended","security","os","secure","audit","haiku","alerts","server","across","monitoring","when","snapshots","Ubuntu","firewall","ufw","2026-11-10 10:12:30",false,"---\nname: harden-vps\nmodel: haiku\ndescription: \"Harden a production Linux VPS for your application across three layers — application (systemd hardening, monitoring alerts, backup automation), Ubuntu OS (UFW firewall, fail2ban SSH, unattended-upgrades, SSH hardening), and VPS provider (health checks, daily backups, monthly snapshots). Use when the user wants to secure a production server, harden a VPS, audit server security, or mentions production hardening, VPS security, or harden the server.\"\n---\n\n# Harden VPS\n\nThree-layer production hardening for a self-hosted app on any VPS. Each layer independently verifiable. Apply OS first (firewall blocks attacks immediately), then your application (alerts + backups), then your VPS provider (snapshots). See [REFERENCE.md](REFERENCE.md) for full script bodies, systemd unit template, and gotchas.\n\n## Quick start\n\nSSH as root into the VPS. Find credentials in the your VPS provider Customer Control Panel.\n\n> **HARD GATE** — Run `ufw status` first. No firewall = layer 1 takes priority over everything.\n\n## Layer 1 — Ubuntu OS\n\n```bash\n# UFW\nufw default deny incoming && ufw default allow outgoing\nufw allow 22\u002Ftcp && ufw allow 80\u002Ftcp && ufw allow 443\u002Ftcp && ufw enable\n# → verify: ufw status | grep -q active\n\n# fail2ban\napt install -y fail2ban\n# Configure \u002Fetc\u002Ffail2ban\u002Fjail.local: sshd, maxretry=3, bantime=3600, findtime=600\nsystemctl restart fail2ban\n# → verify: fail2ban-client status sshd\n\n# unattended-upgrades\napt install -y unattended-upgrades && dpkg-reconfigure -plow unattended-upgrades\n# → verify: systemctl is-active unattended-upgrades | grep -q active\n\n# SSH: PermitRootLogin no, PasswordAuthentication no, PubkeyAuthentication yes\n# → verify: sshd -T | grep -E 'permitrootlogin no|passwordauthentication no'\n\n# Deploy healthcheck.sh → \u002Fopt\u002Fyour-app\u002Fscripts\u002Fhealthcheck.sh\n# Crontab: *\u002F5 * * * * \u002Fopt\u002Fyour-app\u002Fscripts\u002Fhealthcheck.sh\n```\n\n## Layer 2 — applicationlication\n\n```bash\n# systemd: User=your-app, NoNewPrivileges=yes, ProtectSystem=full,\n#   ProtectKernelTunables=yes, ProtectKernelModules=yes,\n#   ProtectControlGroups=yes, RestrictAddressFamilies=AF_INET AF_INET6,\n#   RestrictRealtime=yes, PrivateTmp=yes, LimitNOFILE=65536\n# → verify: systemctl show your-app -p NoNewPrivileges -p ProtectSystem -p User\n\n# Alerts (your application requires auth; insert via SQLite)\nsqlite3 \u002Fopt\u002Fyour-app\u002Fdata\u002Fyour-app.db \"\nINSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)\nVALUES ('a1','Disk >80%','disk_used_percent',80,'gt',1,300);\nINSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)\nVALUES ('a2','CPU >90%','cpu_percent',90,'gt',1,60);\nINSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)\nVALUES ('a3','RAM >85%','mem_used_percent',85,'gt',1,120);\n\"\nsystemctl restart your-app\n\n# Backup crontab (root):\n# 0 2 * * * cp \u002Fopt\u002Fyour-app\u002Fdata\u002Fyour-app.db \u002Fbackup\u002Fyour-app-$(date +\\%Y\\%m\\%d).db\n# 0 3 * * * find \u002Fbackup\u002F -name \"your-app-*.db\" -mtime +90 -delete\n```\n\n## Layer 3 — VPS provider\n\n```bash\n# cntb CLI\ncurl -sL \"$(curl -sL https:\u002F\u002Fapi.github.com\u002Frepos\u002Fcontabo\u002Fcntb\u002Freleases\u002Flatest \\\n  | grep browser_download_url.*linux_amd64.tar.gz | head -1 | cut -d'\"' -f4)\" \\\n  | tar xz -C \u002Fusr\u002Flocal\u002Fbin\n\n# Snapshot script → \u002Fopt\u002Fyour-app\u002Fscripts\u002Fcontabo-snapshot.sh (reads from \u002Fopt\u002Fyour-app\u002F.env)\n# Credentials as env vars in \u002Fopt\u002Fyour-app\u002F.env (deployed by GitHub Actions):\n#   CONTABO_CLIENT_ID, CONTABO_CLIENT_SECRET, CONTABO_API_USER, CONTABO_API_PASSWORD\n# Crontab: 0 4 1 * * \u002Fopt\u002Fyour-app\u002Fscripts\u002Fcontabo-snapshot.sh\n\n# > HARD GATE — Snapshot cron silently fails until env vars are set in .env.\n# Credentials source: your VPS provider Customer Panel → API Details.\n# Local dev: add to .envrc. Production: GitHub Secrets → deploy → \u002Fopt\u002Fyour-app\u002F.env\n```\n\n## CRITICAL GOTCHAS\n\n1. **Shell escaping in Orca terminals:** `$VAR`, `$(…)`, and `%` get eaten by the local shell. Always use base64: `echo '\u003Cbase64>' | base64 -d > script.sh`\n2. **Crontab `%`:** cron interprets `%` as newline. Escape as `\\%` in `$(date +\\%Y\\%m\\%d)`\n3. **fail2ban exit 255:** means a jail references a missing log file. Remove the broken jail, restart.\n4. **your application alerts need auth:** POST to `\u002Fapi\u002Fmonitoring\u002Falerts` requires Bearer token. Workaround: insert directly into SQLite, then restart your application.\n\n## Verify all 8 gates\n\n```bash\nufw status|grep -q active||echo FAIL:ufw\nfail2ban-client status sshd>\u002Fdev\u002Fnull 2>&1||echo FAIL:fail2ban\nsystemctl is-active unattended-upgrades|grep -q active||echo FAIL:unattended\nsshd -T|grep -q 'permitrootlogin no'||echo FAIL:sshd\nsystemctl show your-app -p NoNewPrivileges|grep -q yes||echo FAIL:systemd\nsystemctl is-active your-app|grep -q active||echo FAIL:your-app\nsqlite3 \u002Fopt\u002Fyour-app\u002Fdata\u002Fyour-app.db \"SELECT count(*) FROM monitoring_alerts\"|grep -q 3||echo FAIL:alerts\ncrontab -l|grep -q healthcheck&&crontab -l|grep -q your-app.db&&crontab -l|grep -q contabo-snapshot||echo FAIL:crontab\necho ALL 8 GATES PASSED\n```\n\n→ verify: # requires VPS SSH — run the 8-gate one-liner on the VPS manually\n\n---\n\n# Harden VPS — Reference\n\n## Health check script\n\nDeploy to `\u002Fopt\u002Fyour-app\u002Fscripts\u002Fhealthcheck.sh`:\n\n```bash\n#!\u002Fbin\u002Fbash\nLOG=\u002Fvar\u002Flog\u002Fyour-app\u002Fhealth.log\nmkdir -p \u002Fvar\u002Flog\u002Fyour-app\nDISK=$(df \u002F | awk 'NR==2 {print $5}' | tr -d '%')\nRAM=$(free | awk '\u002FMem:\u002F {printf \"%.0f\", $3\u002F$2*100}')\nNOW=$(date -Iseconds)\n[ \"$DISK\" -gt 85 ] && echo \"$NOW ALERT: Disk ${DISK}%\" >> \"$LOG\"\n[ \"$RAM\" -gt 90 ]   && echo \"$NOW ALERT: RAM ${RAM}%\" >> \"$LOG\"\nsystemctl is-active --quiet your-app || { echo \"$NOW ALERT: your application DOWN\" >> \"$LOG\"; systemctl restart your-app; }\n[ -f \u002Fvar\u002Frun\u002Freboot-required ] && echo \"$NOW WARN: kernel reboot pending\" >> \"$LOG\"\n```\n\nMake executable: `chmod +x \u002Fopt\u002Fyour-app\u002Fscripts\u002Fhealthcheck.sh`\n\n## your application systemd unit\n\nFull hardened unit at `\u002Fetc\u002Fsystemd\u002Fsystem\u002Fyour-app.service`:\n\n```ini\n[Unit]\nDescription=your application BaaS Platform\nDocumentation=https:\u002F\u002Fgithub.com\u002Fexample-org\u002Fexample-app\nAfter=network.target caddy.service\nWants=caddy.service\n\n[Service]\nType=simple\nUser=your-app\nGroup=your-app\nWorkingDirectory=\u002Fopt\u002Fyour-app\nExecStart=\u002Fopt\u002Fyour-app\u002Fbin\u002Fyour-app serve \\\n    --port 8080 \\\n    --db \u002Fopt\u002Fyour-app\u002Fdata\u002Fyour-app.db \\\n    --sites-domain \u003Cyour-domain.example.com>\nEnvironmentFile=-\u002Fopt\u002Fyour-app\u002F.env\nEnvironment=APP_HOME=\u002Fopt\u002Fyour-app\nEnvironment=HOME=\u002Fopt\u002Fyour-app\nEnvironment=NPM_CONFIG_CACHE=\u002Fopt\u002Fyour-app\u002F.npm\nEnvironment=PATH=\u002Fusr\u002Flocal\u002Fsbin:\u002Fusr\u002Flocal\u002Fbin:\u002Fusr\u002Fsbin:\u002Fusr\u002Fbin:\u002Fsbin:\u002Fbin\nRestart=always\nRestartSec=5\nStartLimitIntervalSec=60\nStartLimitBurst=3\nTimeoutStopSec=30\nKillSignal=SIGTERM\nNoNewPrivileges=yes\nPrivateTmp=yes\nProtectSystem=full\nProtectHome=yes\nProtectKernelTunables=yes\nProtectKernelModules=yes\nProtectControlGroups=yes\nRestrictAddressFamilies=AF_INET AF_INET6\nRestrictRealtime=yes\nReadWritePaths=\u002Fopt\u002Fyour-app\u002Fdata\nReadWritePaths=\u002Fopt\u002Fyour-app\u002Fbackups\nReadWritePaths=\u002Fopt\u002Fyour-app\u002Fsecrets\nReadWritePaths=\u002Fopt\u002Fyour-app\u002F.npm\nReadWritePaths=\u002Fopt\u002Fyour-app\u002Flogs\nLimitNOFILE=65536\n\n[Install]\nWantedBy=multi-user.target\n```\n\n## fail2ban jail.local\n\n```ini\n[sshd]\nenabled = true\nport = ssh\nfilter = sshd\nlogpath = \u002Fvar\u002Flog\u002Fauth.log\nmaxretry = 3\nbantime = 3600\nfindtime = 600\n```\n\nNote: your application auth jail deferred — your application logs to journald, not a file. To enable, use `backend = systemd` with a journald filter.\n\n## provider-snapshot.sh — worked example (Contabo API)\n\n> Snapshot APIs are provider-specific. This is one worked example, not a\n> bigpowers default; swap the auth and endpoint calls for your own provider.\n\nDeploy to `\u002Fopt\u002Fyour-app\u002Fscripts\u002Fcontabo-snapshot.sh`. Reads credentials from `\u002Fopt\u002Fyour-app\u002F.env` (same file used by your application systemd unit):\n\n```bash\n#!\u002Fbin\u002Fbash\nset -e\nLOG=\u002Fvar\u002Flog\u002Fyour-app\u002Fsnapshot.log\nENVFILE=\u002Fopt\u002Fyour-app\u002F.env\nINSTANCE_ID=\u003Cyour-contabo-instance-id>\n\n[ -f \"$ENVFILE\" ] || { echo \"$(date -Iseconds) ERROR: $ENVFILE missing\" >> \"$LOG\"; exit 1; }\nset -a; source \"$ENVFILE\"; set +a\n\n[ -z \"$CONTABO_CLIENT_ID\" ] && { echo \"$(date -Iseconds) ERROR: CONTABO_CLIENT_ID not set\" >> \"$LOG\"; exit 1; }\n\nTOKEN=$(curl -s -d \"client_id=$CONTABO_CLIENT_ID\" \\\n  -d \"client_secret=$CONTABO_CLIENT_SECRET\" \\\n  --data-urlencode \"username=$CONTABO_API_USER\" \\\n  --data-urlencode \"password=$CONTABO_API_PASSWORD\" \\\n  -d 'grant_type=password' \\\n  'https:\u002F\u002Fauth.contabo.com\u002Fauth\u002Frealms\u002Fcontabo\u002Fprotocol\u002Fopenid-connect\u002Ftoken' \\\n  | jq -r '.access_token')\n\n[ -z \"$TOKEN\" ] || [ \"$TOKEN\" = \"null\" ] && { echo \"$(date -Iseconds) ERROR: auth failed\" >> \"$LOG\"; exit 1; }\n\ncurl -s -X POST \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"x-request-id: $(uuidgen 2>\u002Fdev\u002Fnull || echo $RANDOM)\" \\\n  \"https:\u002F\u002Fapi.contabo.com\u002Fv1\u002Fcompute\u002Finstances\u002F${INSTANCE_ID}\u002Fsnapshots\" \\\n  | tee -a \"$LOG\"\n\necho >> \"$LOG\"\necho \"$(date -Iseconds) Snapshot requested for $INSTANCE_ID\" >> \"$LOG\"\n```\n\n## your VPS provider credentials\n\nAdd to `\u002Fopt\u002Fyour-app\u002F.env` (same file used by your application systemd unit, deployed by GitHub Actions):\n\n```\nCONTABO_CLIENT_ID=\nCONTABO_CLIENT_SECRET=\nCONTABO_API_USER=\nCONTABO_API_PASSWORD=\n```\n\nSource: your VPS provider Customer Control Panel → API Details. ClientId and ClientSecret are generated there. API User is your email. API Password is set separately in the panel.\n\n**Pipeline:** GitHub Secrets → deploy workflow → `\u002Fopt\u002Fyour-app\u002F.env` on VPS. For local dev (`cntb get instances`), add to `.envrc`.\n\n## your VPS provider instance info template\n\nFill in your instance details from the your VPS provider Customer Control Panel:\n\n```\nIP:           \u003Cyour-instance-ip>\nIPv6:         \u003Cyour-instance-ipv6>\nRegion:       \u003Cyour-region>\nOS:           \u003Cyour-os-version>\nDisk:         \u003Cyour-disk-size>\nDefault user: root\nCustomer ID:  \u003Cyour-customer-id>\n```\n\n## your application monitoring alert SQL\n\n```sql\nINSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)\nVALUES ('alert-001', 'VPS Disk above 80%', 'disk_used_percent', 80, 'gt', 1, 300);\n\nINSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)\nVALUES ('alert-002', 'VPS CPU above 90%', 'cpu_percent', 90, 'gt', 1, 60);\n\nINSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)\nVALUES ('alert-003', 'VPS RAM above 85%', 'mem_used_percent', 85, 'gt', 1, 120);\n```\n\nAlerts are loaded at your application startup. Restart with `systemctl restart your-app` after inserting.\n\n## Base64 encoding workaround\n\nWhen sending scripts through Orca terminal `--text`, the local bash shell interprets `$`, `(`, and `%`. Encode locally and decode remotely:\n\n```bash\n# Local\ncat script.sh | base64\n\n# Remote terminal\necho '\u003Cbase64-output>' | base64 -d > script.sh\n```\n","# Harden VPS\n\nThree-layer production hardening for a self-hosted app on any VPS. Each layer independently verifiable. Apply OS first (firewall blocks attacks immediately), then your application (alerts + backups), then your VPS provider (snapshots). See [REFERENCE.md](REFERENCE.md) for full script bodies, systemd unit template, and gotchas.\n\n## Quick start\n\nSSH as root into the VPS. Find credentials in the your VPS provider Customer Control Panel.\n\n> **HARD GATE** — Run `ufw status` first. No firewall = layer 1 takes priority over everything.\n\n## Layer 1 — Ubuntu OS\n\n```bash\n# UFW\nufw default deny incoming && ufw default allow outgoing\nufw allow 22\u002Ftcp && ufw allow 80\u002Ftcp && ufw allow 443\u002Ftcp && ufw enable\n# → verify: ufw status | grep -q active\n\n# fail2ban\napt install -y fail2ban\n# Configure \u002Fetc\u002Ffail2ban\u002Fjail.local: sshd, maxretry=3, bantime=3600, findtime=600\nsystemctl restart fail2ban\n# → verify: fail2ban-client status sshd\n\n# unattended-upgrades\napt install -y unattended-upgrades && dpkg-reconfigure -plow unattended-upgrades\n# → verify: systemctl is-active unattended-upgrades | grep -q active\n\n# SSH: PermitRootLogin no, PasswordAuthentication no, PubkeyAuthentication yes\n# → verify: sshd -T | grep -E 'permitrootlogin no|passwordauthentication no'\n\n# Deploy healthcheck.sh → \u002Fopt\u002Fyour-app\u002Fscripts\u002Fhealthcheck.sh\n# Crontab: *\u002F5 * * * * \u002Fopt\u002Fyour-app\u002Fscripts\u002Fhealthcheck.sh\n```\n\n## Layer 2 — applicationlication\n\n```bash\n# systemd: User=your-app, NoNewPrivileges=yes, ProtectSystem=full,\n#   ProtectKernelTunables=yes, ProtectKernelModules=yes,\n#   ProtectControlGroups=yes, RestrictAddressFamilies=AF_INET AF_INET6,\n#   RestrictRealtime=yes, PrivateTmp=yes, LimitNOFILE=65536\n# → verify: systemctl show your-app -p NoNewPrivileges -p ProtectSystem -p User\n\n# Alerts (your application requires auth; insert via SQLite)\nsqlite3 \u002Fopt\u002Fyour-app\u002Fdata\u002Fyour-app.db \"\nINSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)\nVALUES ('a1','Disk >80%','disk_used_percent',80,'gt',1,300);\nINSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)\nVALUES ('a2','CPU >90%','cpu_percent',90,'gt',1,60);\nINSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)\nVALUES ('a3','RAM >85%','mem_used_percent',85,'gt',1,120);\n\"\nsystemctl restart your-app\n\n# Backup crontab (root):\n# 0 2 * * * cp \u002Fopt\u002Fyour-app\u002Fdata\u002Fyour-app.db \u002Fbackup\u002Fyour-app-$(date +\\%Y\\%m\\%d).db\n# 0 3 * * * find \u002Fbackup\u002F -name \"your-app-*.db\" -mtime +90 -delete\n```\n\n## Layer 3 — VPS provider\n\n```bash\n# cntb CLI\ncurl -sL \"$(curl -sL https:\u002F\u002Fapi.github.com\u002Frepos\u002Fcontabo\u002Fcntb\u002Freleases\u002Flatest \\\n  | grep browser_download_url.*linux_amd64.tar.gz | head -1 | cut -d'\"' -f4)\" \\\n  | tar xz -C \u002Fusr\u002Flocal\u002Fbin\n\n# Snapshot script → \u002Fopt\u002Fyour-app\u002Fscripts\u002Fcontabo-snapshot.sh (reads from \u002Fopt\u002Fyour-app\u002F.env)\n# Credentials as env vars in \u002Fopt\u002Fyour-app\u002F.env (deployed by GitHub Actions):\n#   CONTABO_CLIENT_ID, CONTABO_CLIENT_SECRET, CONTABO_API_USER, CONTABO_API_PASSWORD\n# Crontab: 0 4 1 * * \u002Fopt\u002Fyour-app\u002Fscripts\u002Fcontabo-snapshot.sh\n\n# > HARD GATE — Snapshot cron silently fails until env vars are set in .env.\n# Credentials source: your VPS provider Customer Panel → API Details.\n# Local dev: add to .envrc. Production: GitHub Secrets → deploy → \u002Fopt\u002Fyour-app\u002F.env\n```\n\n## CRITICAL GOTCHAS\n\n1. **Shell escaping in Orca terminals:** `$VAR`, `$(…)`, and `%` get eaten by the local shell. Always use base64: `echo '\u003Cbase64>' | base64 -d > script.sh`\n2. **Crontab `%`:** cron interprets `%` as newline. Escape as `\\%` in `$(date +\\%Y\\%m\\%d)`\n3. **fail2ban exit 255:** means a jail references a missing log file. Remove the broken jail, restart.\n4. **your application alerts need auth:** POST to `\u002Fapi\u002Fmonitoring\u002Falerts` requires Bearer token. Workaround: insert directly into SQLite, then restart your application.\n\n## Verify all 8 gates\n\n```bash\nufw status|grep -q active||echo FAIL:ufw\nfail2ban-client status sshd>\u002Fdev\u002Fnull 2>&1||echo FAIL:fail2ban\nsystemctl is-active unattended-upgrades|grep -q active||echo FAIL:unattended\nsshd -T|grep -q 'permitrootlogin no'||echo FAIL:sshd\nsystemctl show your-app -p NoNewPrivileges|grep -q yes||echo FAIL:systemd\nsystemctl is-active your-app|grep -q active||echo FAIL:your-app\nsqlite3 \u002Fopt\u002Fyour-app\u002Fdata\u002Fyour-app.db \"SELECT count(*) FROM monitoring_alerts\"|grep -q 3||echo FAIL:alerts\ncrontab -l|grep -q healthcheck&&crontab -l|grep -q your-app.db&&crontab -l|grep -q contabo-snapshot||echo FAIL:crontab\necho ALL 8 GATES PASSED\n```\n\n→ verify: # requires VPS SSH — run the 8-gate one-liner on the VPS manually\n\n---\n\n# Harden VPS — Reference\n\n## Health check script\n\nDeploy to `\u002Fopt\u002Fyour-app\u002Fscripts\u002Fhealthcheck.sh`:\n\n```bash\n#!\u002Fbin\u002Fbash\nLOG=\u002Fvar\u002Flog\u002Fyour-app\u002Fhealth.log\nmkdir -p \u002Fvar\u002Flog\u002Fyour-app\nDISK=$(df \u002F | awk 'NR==2 {print $5}' | tr -d '%')\nRAM=$(free | awk '\u002FMem:\u002F {printf \"%.0f\", $3\u002F$2*100}')\nNOW=$(date -Iseconds)\n[ \"$DISK\" -gt 85 ] && echo \"$NOW ALERT: Disk ${DISK}%\" >> \"$LOG\"\n[ \"$RAM\" -gt 90 ]   && echo \"$NOW ALERT: RAM ${RAM}%\" >> \"$LOG\"\nsystemctl is-active --quiet your-app || { echo \"$NOW ALERT: your application DOWN\" >> \"$LOG\"; systemctl restart your-app; }\n[ -f \u002Fvar\u002Frun\u002Freboot-required ] && echo \"$NOW WARN: kernel reboot pending\" >> \"$LOG\"\n```\n\nMake executable: `chmod +x \u002Fopt\u002Fyour-app\u002Fscripts\u002Fhealthcheck.sh`\n\n## your application systemd unit\n\nFull hardened unit at `\u002Fetc\u002Fsystemd\u002Fsystem\u002Fyour-app.service`:\n\n```ini\n[Unit]\nDescription=your application BaaS Platform\nDocumentation=https:\u002F\u002Fgithub.com\u002Fexample-org\u002Fexample-app\nAfter=network.target caddy.service\nWants=caddy.service\n\n[Service]\nType=simple\nUser=your-app\nGroup=your-app\nWorkingDirectory=\u002Fopt\u002Fyour-app\nExecStart=\u002Fopt\u002Fyour-app\u002Fbin\u002Fyour-app serve \\\n    --port 8080 \\\n    --db \u002Fopt\u002Fyour-app\u002Fdata\u002Fyour-app.db \\\n    --sites-domain \u003Cyour-domain.example.com>\nEnvironmentFile=-\u002Fopt\u002Fyour-app\u002F.env\nEnvironment=APP_HOME=\u002Fopt\u002Fyour-app\nEnvironment=HOME=\u002Fopt\u002Fyour-app\nEnvironment=NPM_CONFIG_CACHE=\u002Fopt\u002Fyour-app\u002F.npm\nEnvironment=PATH=\u002Fusr\u002Flocal\u002Fsbin:\u002Fusr\u002Flocal\u002Fbin:\u002Fusr\u002Fsbin:\u002Fusr\u002Fbin:\u002Fsbin:\u002Fbin\nRestart=always\nRestartSec=5\nStartLimitIntervalSec=60\nStartLimitBurst=3\nTimeoutStopSec=30\nKillSignal=SIGTERM\nNoNewPrivileges=yes\nPrivateTmp=yes\nProtectSystem=full\nProtectHome=yes\nProtectKernelTunables=yes\nProtectKernelModules=yes\nProtectControlGroups=yes\nRestrictAddressFamilies=AF_INET AF_INET6\nRestrictRealtime=yes\nReadWritePaths=\u002Fopt\u002Fyour-app\u002Fdata\nReadWritePaths=\u002Fopt\u002Fyour-app\u002Fbackups\nReadWritePaths=\u002Fopt\u002Fyour-app\u002Fsecrets\nReadWritePaths=\u002Fopt\u002Fyour-app\u002F.npm\nReadWritePaths=\u002Fopt\u002Fyour-app\u002Flogs\nLimitNOFILE=65536\n\n[Install]\nWantedBy=multi-user.target\n```\n\n## fail2ban jail.local\n\n```ini\n[sshd]\nenabled = true\nport = ssh\nfilter = sshd\nlogpath = \u002Fvar\u002Flog\u002Fauth.log\nmaxretry = 3\nbantime = 3600\nfindtime = 600\n```\n\nNote: your application auth jail deferred — your application logs to journald, not a file. To enable, use `backend = systemd` with a journald filter.\n\n## provider-snapshot.sh — worked example (Contabo API)\n\n> Snapshot APIs are provider-specific. This is one worked example, not a\n> bigpowers default; swap the auth and endpoint calls for your own provider.\n\nDeploy to `\u002Fopt\u002Fyour-app\u002Fscripts\u002Fcontabo-snapshot.sh`. Reads credentials from `\u002Fopt\u002Fyour-app\u002F.env` (same file used by your application systemd unit):\n\n```bash\n#!\u002Fbin\u002Fbash\nset -e\nLOG=\u002Fvar\u002Flog\u002Fyour-app\u002Fsnapshot.log\nENVFILE=\u002Fopt\u002Fyour-app\u002F.env\nINSTANCE_ID=\u003Cyour-contabo-instance-id>\n\n[ -f \"$ENVFILE\" ] || { echo \"$(date -Iseconds) ERROR: $ENVFILE missing\" >> \"$LOG\"; exit 1; }\nset -a; source \"$ENVFILE\"; set +a\n\n[ -z \"$CONTABO_CLIENT_ID\" ] && { echo \"$(date -Iseconds) ERROR: CONTABO_CLIENT_ID not set\" >> \"$LOG\"; exit 1; }\n\nTOKEN=$(curl -s -d \"client_id=$CONTABO_CLIENT_ID\" \\\n  -d \"client_secret=$CONTABO_CLIENT_SECRET\" \\\n  --data-urlencode \"username=$CONTABO_API_USER\" \\\n  --data-urlencode \"password=$CONTABO_API_PASSWORD\" \\\n  -d 'grant_type=password' \\\n  'https:\u002F\u002Fauth.contabo.com\u002Fauth\u002Frealms\u002Fcontabo\u002Fprotocol\u002Fopenid-connect\u002Ftoken' \\\n  | jq -r '.access_token')\n\n[ -z \"$TOKEN\" ] || [ \"$TOKEN\" = \"null\" ] && { echo \"$(date -Iseconds) ERROR: auth failed\" >> \"$LOG\"; exit 1; }\n\ncurl -s -X POST \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"x-request-id: $(uuidgen 2>\u002Fdev\u002Fnull || echo $RANDOM)\" \\\n  \"https:\u002F\u002Fapi.contabo.com\u002Fv1\u002Fcompute\u002Finstances\u002F${INSTANCE_ID}\u002Fsnapshots\" \\\n  | tee -a \"$LOG\"\n\necho >> \"$LOG\"\necho \"$(date -Iseconds) Snapshot requested for $INSTANCE_ID\" >> \"$LOG\"\n```\n\n## your VPS provider credentials\n\nAdd to `\u002Fopt\u002Fyour-app\u002F.env` (same file used by your application systemd unit, deployed by GitHub Actions):\n\n```\nCONTABO_CLIENT_ID=\nCONTABO_CLIENT_SECRET=\nCONTABO_API_USER=\nCONTABO_API_PASSWORD=\n```\n\nSource: your VPS provider Customer Control Panel → API Details. ClientId and ClientSecret are generated there. API User is your email. API Password is set separately in the panel.\n\n**Pipeline:** GitHub Secrets → deploy workflow → `\u002Fopt\u002Fyour-app\u002F.env` on VPS. For local dev (`cntb get instances`), add to `.envrc`.\n\n## your VPS provider instance info template\n\nFill in your instance details from the your VPS provider Customer Control Panel:\n\n```\nIP:           \u003Cyour-instance-ip>\nIPv6:         \u003Cyour-instance-ipv6>\nRegion:       \u003Cyour-region>\nOS:           \u003Cyour-os-version>\nDisk:         \u003Cyour-disk-size>\nDefault user: root\nCustomer ID:  \u003Cyour-customer-id>\n```\n\n## your application monitoring alert SQL\n\n```sql\nINSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)\nVALUES ('alert-001', 'VPS Disk above 80%', 'disk_used_percent', 80, 'gt', 1, 300);\n\nINSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)\nVALUES ('alert-002', 'VPS CPU above 90%', 'cpu_percent', 90, 'gt', 1, 60);\n\nINSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)\nVALUES ('alert-003', 'VPS RAM above 85%', 'mem_used_percent', 85, 'gt', 1, 120);\n```\n\nAlerts are loaded at your application startup. Restart with `systemctl restart your-app` after inserting.\n\n## Base64 encoding workaround\n\nWhen sending scripts through Orca terminal `--text`, the local bash shell interprets `$`, `(`, and `%`. Encode locally and decode remotely:\n\n```bash\n# Local\ncat script.sh | base64\n\n# Remote terminal\necho '\u003Cbase64-output>' | base64 -d > script.sh\n```\n",{"name":5,"model":34,"description":7},"main","72f5e4c8b43f60cf86b37078ad8cd7e22daf67aa",".copilot\u002Fskills\u002Fharden-vps",24,"https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers","https:\u002F\u002Fdanielvm-git.github.io\u002Fbigpowers\u002F","https:\u002F\u002Fgithub.com\u002Fdanielvm-git","danielvm.net","2026-11-10 01:18:54",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add danielvm-git\u002Fbigpowers --skill harden-vps",{"tool":64,"command":65},"bunx","bunx skills add danielvm-git\u002Fbigpowers --skill harden-vps",{"tool":67,"command":68},"pnpm","pnpm dlx skills add danielvm-git\u002Fbigpowers --skill harden-vps","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers\u002Fblob\u002Fmain\u002F.copilot\u002Fskills\u002Fharden-vps\u002FSKILL.md",true,[],[73,91,98,127,150,171],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":82,"publishedAt":44},"01M71KBG9F8X644MJB16H4V8M7","skill-verify-fail-open-fixture","Negative-path fixture for run-skill-verify.sh — must never PASS.","https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers\u002Fblob\u002Fmain\u002Fspecs\u002Fverifications\u002Ffixtures\u002Fskill-verify-fail-open\u002FSKILL.md",[79],{"categoryNo":80,"categoryName":81,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[83,84,85,86,87,88,89,27,90],"fixture","open","verify","skill verify fail open fixture","skill verify","path","run","never",{"skillNo":92,"name":93,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":96,"publishedAt":44},"01M71KBE3BKEMKNE0C5DGVDWZH","negative-path","https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers\u002Fblob\u002Fmain\u002Fspecs\u002Fverifications\u002Ffixtures\u002Fnegative-path\u002FSKILL.md",[],[97,88],"negative path",{"skillNo":99,"name":100,"nameZh":6,"description":101,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":102,"categories":103,"keywords":107,"publishedAt":44},"01M71KBBGF7Y4EB4AMHBZ8CJFC","write-document","Write, organize, and sync high-integrity technical documents using the BMAD methodology. Ensures every document is Bold, Minimal, Actionable, and Durable. Use when creating architectural docs, technical guides, or organizing the specs\u002F directory.","https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers\u002Fblob\u002Fmain\u002Fskills\u002Fwrite-document\u002FSKILL.md",[104],{"categoryNo":105,"categoryName":106,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000002","文档处理",[108,109,110,111,112,113,114,115,116,117,118,39,119,120,121,122,123,124,125,126],"write","write document","document","documents","actionable","technical","methodology","high","bmad","standard","use","minimal","bold","sonnet","integrity","organize","sync","directory","guides",{"skillNo":128,"name":129,"nameZh":6,"description":130,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":133,"publishedAt":44},"01M71KB8TM089JJD5AY5KAFSDM","wire-observability","Add structured JSON logging, observability commands, and idempotent setup scripts to a project. Use when a project needs production-readiness instrumentation, when user wants structured logging, or as a production-readiness gate at any phase of development.","https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers\u002Fblob\u002Fmain\u002Fskills\u002Fwire-observability\u002FSKILL.md",[],[134,135,136,117,137,118,138,139,140,141,121,142,143,144,145,146,147,39,148,149],"observability","wire observability","readiness","commands","development","as","logging","add","instrumentation","at","structured logging","production-readiness","idempotent","phase","project","scripts",{"skillNo":151,"name":152,"nameZh":6,"description":153,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":154,"categories":155,"keywords":156,"publishedAt":44},"01M71KB6CNRANP449DQA3NJZ96","wire-ci","CI pipeline setup with bundled, forge-neutral templates and local validation. Detects the forge from the git remote, generates workflows for supported forges, and skips honestly for the rest. The CI equivalent of wire-observability.","https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers\u002Fblob\u002Fmain\u002Fskills\u002Fwire-ci\u002FSKILL.md",[],[157,158,159,160,161,162,163,164,165,166,167,117,168,27,134,121,169,170],"wire ci","ci","remote","local","pipeline","forge","validation","setup","templates","REST","with","ci pipeline setup","git","workflows",{"skillNo":172,"name":173,"nameZh":6,"description":174,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":175,"categories":176,"keywords":188,"publishedAt":44},"01M71KB2ZV98P03X88QR8MT9F1","visual-dashboard","Start a browser-based dashboard that visualizes architecture, implementation plans, and project status. Persists artifacts in .bigpowers\u002Fdashboard\u002F. Reads specs\u002Fstate.yaml, release-plan.yaml, epics, and planning-status via HTTP API or opencode panel.","https:\u002F\u002Fgithub.com\u002Fdanielvm-git\u002Fbigpowers\u002Fblob\u002Fmain\u002Fskills\u002Fvisual-dashboard\u002FSKILL.md",[177,180,185],{"categoryNo":178,"categoryName":179,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":181,"categoryName":182,"taxonomyType":183,"taxonomyTypeDesc":184},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",{"categoryNo":186,"categoryName":187,"taxonomyType":183,"taxonomyTypeDesc":184},"01JBSEEDCAT000000000000033","数据分析师",[189,190,191,192,193,194,195,196,197,121,198,199,200,201,202,203,204,148,205,206],"dashboard","visual","visual dashboard","status","artifacts","http","plans","planning","panel","start","release","opencode","plan","browser","api","architecture","project status","http api"]