Cursor rule
.cursor/rules/harden-vps.mdcHarden 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.
Cursor rules
Quality
46/100
Scores the file, not the repository.Length
1,208 words
46 headings · 12 code blocksRepository
119
— · pushed 1 days agoLast changed
3 days ago
First indexed 3 days ago.123456# Harden VPS78Three-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.910## Quick start1112SSH as root into the VPS. Find credentials in the your VPS provider Customer Control Panel.1314> **HARD GATE** — Run `ufw status` first. No firewall = layer 1 takes priority over everything.1516## Layer 1 — Ubuntu OS1718```bash19# UFW20ufw default deny incoming && ufw default allow outgoing21ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw enable22# → verify: ufw status | grep -q active2324# fail2ban25apt install -y fail2ban26# Configure /etc/fail2ban/jail.local: sshd, maxretry=3, bantime=3600, findtime=60027systemctl restart fail2ban28# → verify: fail2ban-client status sshd2930# unattended-upgrades31apt install -y unattended-upgrades && dpkg-reconfigure -plow unattended-upgrades32# → verify: systemctl is-active unattended-upgrades | grep -q active3334# SSH: PermitRootLogin no, PasswordAuthentication no, PubkeyAuthentication yes35# → verify: sshd -T | grep -E 'permitrootlogin no|passwordauthentication no'3637# Deploy healthcheck.sh → /opt/your-app/scripts/healthcheck.sh38# Crontab: */5 * * * * /opt/your-app/scripts/healthcheck.sh39```4041## Layer 2 — applicationlication4243```bash44# systemd: User=your-app, NoNewPrivileges=yes, ProtectSystem=full,45# ProtectKernelTunables=yes, ProtectKernelModules=yes,46# ProtectControlGroups=yes, RestrictAddressFamilies=AF_INET AF_INET6,47# RestrictRealtime=yes, PrivateTmp=yes, LimitNOFILE=6553648# → verify: systemctl show your-app -p NoNewPrivileges -p ProtectSystem -p User4950# Alerts (your application requires auth; insert via SQLite)51sqlite3 /opt/your-app/data/your-app.db "52INSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)53VALUES ('a1','Disk >80%','disk_used_percent',80,'gt',1,300);54INSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)55VALUES ('a2','CPU >90%','cpu_percent',90,'gt',1,60);56INSERT INTO monitoring_alerts (id,name,metric,threshold,operator,enabled,duration_seconds)57VALUES ('a3','RAM >85%','mem_used_percent',85,'gt',1,120);58"59systemctl restart your-app6061# Backup crontab (root):62# 0 2 * * * cp /opt/your-app/data/your-app.db /backup/your-app-$(date +\%Y\%m\%d).db63# 0 3 * * * find /backup/ -name "your-app-*.db" -mtime +90 -delete64```6566## Layer 3 — VPS provider6768```bash69# cntb CLI70curl -sL "$(curl -sL https://api.github.com/repos/contabo/cntb/releases/latest \71 | grep browser_download_url.*linux_amd64.tar.gz | head -1 | cut -d'"' -f4)" \72 | tar xz -C /usr/local/bin7374# Snapshot script → /opt/your-app/scripts/contabo-snapshot.sh (reads from /opt/your-app/.env)75# Credentials as env vars in /opt/your-app/.env (deployed by GitHub Actions):76# CONTABO_CLIENT_ID, CONTABO_CLIENT_SECRET, CONTABO_API_USER, CONTABO_API_PASSWORD77# Crontab: 0 4 1 * * /opt/your-app/scripts/contabo-snapshot.sh7879# > HARD GATE — Snapshot cron silently fails until env vars are set in .env.80# Credentials source: your VPS provider Customer Panel → API Details.81# Local dev: add to .envrc. Production: GitHub Secrets → deploy → /opt/your-app/.env82```8384## CRITICAL GOTCHAS85861. **Shell escaping in Orca terminals:** `$VAR`, `$(…)`, and `%` get eaten by the local shell. Always use base64: `echo '<base64>' | base64 -d > script.sh`872. **Crontab `%`:** cron interprets `%` as newline. Escape as `\%` in `$(date +\%Y\%m\%d)`883. **fail2ban exit 255:** means a jail references a missing log file. Remove the broken jail, restart.894. **your application alerts need auth:** POST to `/api/monitoring/alerts` requires Bearer token. Workaround: insert directly into SQLite, then restart your application.9091## Verify all 8 gates9293```bash94ufw status|grep -q active||echo FAIL:ufw95fail2ban-client status sshd>/dev/null 2>&1||echo FAIL:fail2ban96systemctl is-active unattended-upgrades|grep -q active||echo FAIL:unattended97sshd -T|grep -q 'permitrootlogin no'||echo FAIL:sshd98systemctl show your-app -p NoNewPrivileges|grep -q yes||echo FAIL:systemd99systemctl is-active your-app|grep -q active||echo FAIL:your-app100sqlite3 /opt/your-app/data/your-app.db "SELECT count(*) FROM monitoring_alerts"|grep -q 3||echo FAIL:alerts101crontab -l|grep -q healthcheck&&crontab -l|grep -q your-app.db&&crontab -l|grep -q contabo-snapshot||echo FAIL:crontab102echo ALL 8 GATES PASSED103```104105→ verify: # requires VPS SSH — run the 8-gate one-liner on the VPS manually106107---108109# Harden VPS — Reference110111## Health check script112113Deploy to `/opt/your-app/scripts/healthcheck.sh`:114115```bash116#!/bin/bash117LOG=/var/log/your-app/health.log118mkdir -p /var/log/your-app119DISK=$(df / | awk 'NR==2 {print $5}' | tr -d '%')120RAM=$(free | awk '/Mem:/ {printf "%.0f", $3/$2*100}')121NOW=$(date -Iseconds)122[ "$DISK" -gt 85 ] && echo "$NOW ALERT: Disk ${DISK}%" >> "$LOG"123[ "$RAM" -gt 90 ] && echo "$NOW ALERT: RAM ${RAM}%" >> "$LOG"124systemctl is-active --quiet your-app || { echo "$NOW ALERT: your application DOWN" >> "$LOG"; systemctl restart your-app; }125[ -f /var/run/reboot-required ] && echo "$NOW WARN: kernel reboot pending" >> "$LOG"126```127128Make executable: `chmod +x /opt/your-app/scripts/healthcheck.sh`129130## your application systemd unit131132Full hardened unit at `/etc/systemd/system/your-app.service`:133134```ini135[Unit]136Description=your application BaaS Platform137Documentation=https://github.com/example-org/example-app138After=network.target caddy.service139Wants=caddy.service140141[Service]142Type=simple143User=your-app144Group=your-app145WorkingDirectory=/opt/your-app146ExecStart=/opt/your-app/bin/your-app serve \147 --port 8080 \148 --db /opt/your-app/data/your-app.db \149 --sites-domain <your-domain.example.com>150EnvironmentFile=-/opt/your-app/.env151Environment=APP_HOME=/opt/your-app152Environment=HOME=/opt/your-app153Environment=NPM_CONFIG_CACHE=/opt/your-app/.npm154Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin155Restart=always156RestartSec=5157StartLimitIntervalSec=60158StartLimitBurst=3159TimeoutStopSec=30160KillSignal=SIGTERM161NoNewPrivileges=yes162PrivateTmp=yes163ProtectSystem=full164ProtectHome=yes165ProtectKernelTunables=yes166ProtectKernelModules=yes167ProtectControlGroups=yes168RestrictAddressFamilies=AF_INET AF_INET6169RestrictRealtime=yes170ReadWritePaths=/opt/your-app/data171ReadWritePaths=/opt/your-app/backups172ReadWritePaths=/opt/your-app/secrets173ReadWritePaths=/opt/your-app/.npm174ReadWritePaths=/opt/your-app/logs175LimitNOFILE=65536176177[Install]178WantedBy=multi-user.target179```180181## fail2ban jail.local182183```ini184[sshd]185enabled = true186port = ssh187filter = sshd188logpath = /var/log/auth.log189maxretry = 3190bantime = 3600191findtime = 600192```193194Note: your application auth jail deferred — your application logs to journald, not a file. To enable, use `backend = systemd` with a journald filter.195196## provider-snapshot.sh — worked example (Contabo API)197198> Snapshot APIs are provider-specific. This is one worked example, not a199> bigpowers default; swap the auth and endpoint calls for your own provider.200201Deploy to `/opt/your-app/scripts/contabo-snapshot.sh`. Reads credentials from `/opt/your-app/.env` (same file used by your application systemd unit):202203```bash204#!/bin/bash205set -e206LOG=/var/log/your-app/snapshot.log207ENVFILE=/opt/your-app/.env208INSTANCE_ID=<your-contabo-instance-id>209210[ -f "$ENVFILE" ] || { echo "$(date -Iseconds) ERROR: $ENVFILE missing" >> "$LOG"; exit 1; }211set -a; source "$ENVFILE"; set +a212213[ -z "$CONTABO_CLIENT_ID" ] && { echo "$(date -Iseconds) ERROR: CONTABO_CLIENT_ID not set" >> "$LOG"; exit 1; }214215TOKEN=$(curl -s -d "client_id=$CONTABO_CLIENT_ID" \216 -d "client_secret=$CONTABO_CLIENT_SECRET" \217 --data-urlencode "username=$CONTABO_API_USER" \218 --data-urlencode "password=$CONTABO_API_PASSWORD" \219 -d 'grant_type=password' \220 'https://auth.contabo.com/auth/realms/contabo/protocol/openid-connect/token' \221 | jq -r '.access_token')222223[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { echo "$(date -Iseconds) ERROR: auth failed" >> "$LOG"; exit 1; }224225curl -s -X POST \226 -H "Authorization: Bearer $TOKEN" \227 -H "Content-Type: application/json" \228 -H "x-request-id: $(uuidgen 2>/dev/null || echo $RANDOM)" \229 "https://api.contabo.com/v1/compute/instances/${INSTANCE_ID}/snapshots" \230 | tee -a "$LOG"231232echo >> "$LOG"233echo "$(date -Iseconds) Snapshot requested for $INSTANCE_ID" >> "$LOG"234```235236## your VPS provider credentials237238Add to `/opt/your-app/.env` (same file used by your application systemd unit, deployed by GitHub Actions):239240```241CONTABO_CLIENT_ID=242CONTABO_CLIENT_SECRET=243CONTABO_API_USER=244CONTABO_API_PASSWORD=245```246247Source: 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.248249**Pipeline:** GitHub Secrets → deploy workflow → `/opt/your-app/.env` on VPS. For local dev (`cntb get instances`), add to `.envrc`.250251## your VPS provider instance info template252253Fill in your instance details from the your VPS provider Customer Control Panel:254255```256IP: <your-instance-ip>257IPv6: <your-instance-ipv6>258Region: <your-region>259OS: <your-os-version>260Disk: <your-disk-size>261Default user: root262Customer ID: <your-customer-id>263```264265## your application monitoring alert SQL266267```sql268INSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)269VALUES ('alert-001', 'VPS Disk above 80%', 'disk_used_percent', 80, 'gt', 1, 300);270271INSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)272VALUES ('alert-002', 'VPS CPU above 90%', 'cpu_percent', 90, 'gt', 1, 60);273274INSERT INTO monitoring_alerts (id, name, metric, threshold, operator, enabled, duration_seconds)275VALUES ('alert-003', 'VPS RAM above 85%', 'mem_used_percent', 85, 'gt', 1, 120);276```277278Alerts are loaded at your application startup. Restart with `systemctl restart your-app` after inserting.279280## Base64 encoding workaround281282When sending scripts through Orca terminal `--text`, the local bash shell interprets `$`, `(`, and `%`. Encode locally and decode remotely:283284```bash285# Local286cat script.sh | base64287288# Remote terminal289echo '<base64-output>' | base64 -d > script.sh290```291
Also in danielvm-git/bigpowers
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| danielvm-git/bigpowers.cursor/rules/align-grid.mdc · 119 | Cursor rules | lint-formatdo-notagent-behaviour | 65/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/assess-impact.mdc · 119 | Cursor rules | testtesting-strategydeployment | 66/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/audit-code.mdc · 119 | Cursor rules | setuptestlint-formatstyle+4 | 66/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/audit-plan.mdc · 119 | Cursor rules | buildteststylegit | 74/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/build-epic.mdc · 119 | Cursor rules | buildgit | 58/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/change-request.mdc · 119 | Cursor rules | no sections | 48/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/commit-message.mdc · 119 | Cursor rules | lint-formatstyletypesgit+3 | 82/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/compose-workflow.mdc · 119 | Cursor rules | styledo-notagent-behaviour | 65/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/context7-mcp.mdc · 119 | Cursor rules | style | 54/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/deepen-architecture.mdc · 119 | Cursor rules | testtesting-strategydo-not | 57/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/define-language.mdc · 119 | Cursor rules | lint-formatdo-not | 65/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/delegate-task.mdc · 119 | Cursor rules | git | 62/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/deploy.mdc · 119 | Cursor rules | setupbuildtestdeployment | 77/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/develop-tdd.mdc · 119 | Cursor rules | teststylearchtesting-strategy+5 | 85/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/diagnose-root.mdc · 119 | Cursor rules | no sections | 39/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/dispatch-agents.mdc · 119 | Cursor rules | git | 54/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/edit-document.mdc · 119 | Cursor rules | no sections | 39/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/elaborate-spec.mdc · 119 | Cursor rules | test | 58/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/enforce-first.mdc · 119 | Cursor rules | no sections | 50/100 | 3 days ago | |
| danielvm-git/bigpowers.cursor/rules/evolve-skill.mdc · 119 | Cursor rules | no sections | 50/100 | 3 days ago |
Diff against .cursor/rules/align-grid.mdc Diff against .cursor/rules/assess-impact.mdc Diff against .cursor/rules/audit-code.mdc Diff against .cursor/rules/audit-plan.mdc Diff against .cursor/rules/build-epic.mdc Diff against .cursor/rules/change-request.mdc Diff against .cursor/rules/commit-message.mdc Diff against .cursor/rules/compose-workflow.mdc Diff against .cursor/rules/context7-mcp.mdc Diff against .cursor/rules/deepen-architecture.mdc Diff against .cursor/rules/define-language.mdc Diff against .cursor/rules/delegate-task.mdc Diff against .cursor/rules/deploy.mdc Diff against .cursor/rules/develop-tdd.mdc Diff against .cursor/rules/diagnose-root.mdc Diff against .cursor/rules/dispatch-agents.mdc Diff against .cursor/rules/edit-document.mdc Diff against .cursor/rules/elaborate-spec.mdc Diff against .cursor/rules/enforce-first.mdc Diff against .cursor/rules/evolve-skill.mdc
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| TechSquidTV/Hermes.cursor/rules/10-hermes-api.mdc · 45 | Cursor rules | testlint-formatstylearch+5 | 100/100 | 3 days ago | |
| hiromaily/go-crypto-wallet.cursor/rules/typescript.mdc · 126 | Cursor rules | setupbuildtestlint-format+6 | 100/100 | 3 days ago | |
| markstev/mark-starter.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+6 | 99/100 | 3 days ago | |
| dodgecfr/combatfilms-webapp.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| deifos/clipmira-subtitles.cursor/rules/frontend.mdc · 1 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| Allymahmoud/case-intake-platform.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| langflow-ai/langflow.cursor/rules/docs_development.mdc · 153k | Cursor rules | setupbuildtestlint-format+7 | 97/100 | 3 days ago | |
| TechSquidTV/Hermes.cursor/rules/20-hermes-api-tests.mdc · 45 | Cursor rules | teststyletesting-strategysecurity+3 | 97/100 | 3 days ago |
