summaryrefslogtreecommitdiff
path: root/vps
diff options
context:
space:
mode:
authordoc <doc@filenotfound.org>2025-06-30 20:14:17 +0000
committerdoc <doc@filenotfound.org>2025-06-30 20:14:17 +0000
commita8cd1c324c0541b0d26542168aeced085ec13201 (patch)
treea99d398008b46aa4df5dcae997e1690298d2fc70 /vps
initial failzero commitHEADmaster
Diffstat (limited to 'vps')
-rw-r--r--vps/.env5
-rwxr-xr-xvps/check-hardened.sh37
-rwxr-xr-xvps/check_rdns_retry.sh27
-rwxr-xr-xvps/functions/destroy_vps_by_label.sh28
-rwxr-xr-xvps/functions/disable_backups_by_label.sh23
-rw-r--r--vps/functions/disable_ip.sh18
-rwxr-xr-xvps/functions/enable_backups_by_label.sh23
-rwxr-xr-xvps/functions/list_all_vps.sh9
-rwxr-xr-xvps/functions/provision.sh135
-rwxr-xr-xvps/functions/reboot_vps.sh7
-rwxr-xr-xvps/functions/resize_vps.sh27
-rwxr-xr-xvps/functions/safe_create_dataset.sh12
-rwxr-xr-xvps/functions/status_vps.sh8
-rwxr-xr-xvps/functions/usage.sh22
-rwxr-xr-xvps/functions/verify_ptr.sh29
-rwxr-xr-xvps/genesis_squeaky.sh44
-rwxr-xr-xvps/genesisctl.sh104
17 files changed, 558 insertions, 0 deletions
diff --git a/vps/.env b/vps/.env
new file mode 100644
index 0000000..1be49f0
--- /dev/null
+++ b/vps/.env
@@ -0,0 +1,5 @@
+LINODE_API_TOKEN=8140523e8d64f16f490b70096b04d221a44236eda552b0caa35fe9be35442f6d
+# Cloudflare API
+CF_API_TOKEN="PrUbZD1bj0ky1T32waiis2hp91e4Az1ZiCule9Ys"
+CF_ZONE_ID="c9b0c727c2c55594f62d38227133e3ac"
+CF_DOMAIN="failzero.net"
diff --git a/vps/check-hardened.sh b/vps/check-hardened.sh
new file mode 100755
index 0000000..cdaeef8
--- /dev/null
+++ b/vps/check-hardened.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# check-hardened.sh - Scan all known Genesis VPSes for hardening status
+# Requirements: ssh access to all VPSes by label or IP
+
+LOG_BASE="/home/doc/vpslogs"
+MARKER_FILE="/var/log/genesis-hardened.ok"
+
+if [ ! -d "$LOG_BASE" ]; then
+ echo "āŒ Log directory $LOG_BASE does not exist. Are you running this on Krang?"
+ exit 1
+fi
+
+cd "$LOG_BASE" || exit 1
+
+echo "šŸ” Scanning for hardened Genesis VPSes..."
+echo
+
+for LOG in *.log; do
+ VPS_LABEL="${LOG%.log}"
+ LAST_KNOWN_IP=$(grep -Eo '\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\)' "$LOG" | tail -1 | tr -d '()')
+
+ if [ -z "$LAST_KNOWN_IP" ]; then
+ echo "āš ļø $VPS_LABEL: No IP found in log. Skipping."
+ continue
+ fi
+
+ echo -n "šŸ”§ $VPS_LABEL ($LAST_KNOWN_IP): "
+
+ ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no root@"$LAST_KNOWN_IP" "test -f $MARKER_FILE" >/dev/null 2>&1
+
+ if [ $? -eq 0 ]; then
+ echo "āœ… Hardened"
+ else
+ echo "āŒ Not marked as hardened"
+ fi
+
+done
diff --git a/vps/check_rdns_retry.sh b/vps/check_rdns_retry.sh
new file mode 100755
index 0000000..b11208b
--- /dev/null
+++ b/vps/check_rdns_retry.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+set -e
+[ -f ".env" ] && source .env
+LOGFILE="/home/doc/vpslogs/pending_rdns.log"
+TMPFILE="/tmp/rdns_retry.log"
+
+touch "$TMPFILE"
+
+while IFS="|" read -r LINODE_ID IP LABEL; do
+ CURRENT_RDNS=$(dig -x "$IP" +short)
+ EXPECTED_RDNS="$LABEL.failzero.net."
+
+ if [[ "$CURRENT_RDNS" == "$EXPECTED_RDNS" ]]; then
+ echo "āœ… $IP already has correct rDNS ($CURRENT_RDNS)"
+ else
+ echo "ā³ rDNS not set correctly for $LABEL ($IP). Retrying..."
+ RESPONSE=$(curl -s -X PUT "https://api.linode.com/v4/linode/instances/$LINODE_ID/ips/$IP" \
+ -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"rdns": "'"$LABEL.failzero.net"'"}')
+ echo "šŸ” Retry result for $IP: $RESPONSE"
+ fi
+
+ echo "$LINODE_ID|$IP|$LABEL" >> "$TMPFILE"
+done < "$LOGFILE"
+
+mv "$TMPFILE" "$LOGFILE"
diff --git a/vps/functions/destroy_vps_by_label.sh b/vps/functions/destroy_vps_by_label.sh
new file mode 100755
index 0000000..09d807e
--- /dev/null
+++ b/vps/functions/destroy_vps_by_label.sh
@@ -0,0 +1,28 @@
+destroy_vps_by_label() {
+ LABEL="$1"
+ echo "Looking for VPS with label '$LABEL'..."
+ LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ https://api.linode.com/v4/linode/instances | \
+ jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id')
+
+ if [ -z "$LINODE_ID" ]; then
+ echo "Error: No Linode found with label '$LABEL'"
+ exit 1
+ fi
+
+ read -rp "Are you sure you want to destroy VPS '$LABEL' (ID: $LINODE_ID)? [y/N] " confirm
+ if [[ "$confirm" =~ ^[Yy]$ ]]; then
+ echo "Destroying Linode with ID $LINODE_ID (label: $LABEL)..."
+ HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
+ https://api.linode.com/v4/linode/instances/$LINODE_ID \
+ -H "Authorization: Bearer $LINODE_API_TOKEN")
+
+ if [[ "$HTTP_STATUS" == "204" ]]; then
+ echo "āœ… Linode $LABEL (ID $LINODE_ID) has been destroyed."
+ else
+ echo "āŒ Failed to destroy VPS. HTTP status: $HTTP_STATUS"
+ fi
+ else
+ echo "Cancelled. VPS '$LABEL' not destroyed."
+ fi
+}
diff --git a/vps/functions/disable_backups_by_label.sh b/vps/functions/disable_backups_by_label.sh
new file mode 100755
index 0000000..417bdb8
--- /dev/null
+++ b/vps/functions/disable_backups_by_label.sh
@@ -0,0 +1,23 @@
+disable_backups_by_label() {
+ LABEL="$1"
+ LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ https://api.linode.com/v4/linode/instances | \
+ jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id')
+
+ if [ -z "$LINODE_ID" ]; then
+ echo "āŒ No Linode found with label '$LABEL'"
+ exit 1
+ fi
+
+ echo "Disabling backups for Linode '$LABEL' (ID: $LINODE_ID)..."
+
+ HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+ https://api.linode.com/v4/linode/instances/$LINODE_ID/backups/disable \
+ -H "Authorization: Bearer $LINODE_API_TOKEN")
+
+ if [[ "$HTTP_STATUS" == "200" ]]; then
+ echo "āœ… Backups disabled for Linode $LABEL."
+ else
+ echo "āŒ Failed to disable backups (HTTP $HTTP_STATUS)"
+ fi
+}
diff --git a/vps/functions/disable_ip.sh b/vps/functions/disable_ip.sh
new file mode 100644
index 0000000..0021b74
--- /dev/null
+++ b/vps/functions/disable_ip.sh
@@ -0,0 +1,18 @@
+disable_ip() {
+ local ip="$1"
+
+ if [[ -z "$ip" ]]; then
+ echo "[!] No IP specified."
+ exit 1
+ fi
+
+ echo "[*] Disabling access to VPS with IP: $ip"
+
+ # Block all traffic to/from that IP via iptables
+ iptables -A INPUT -s "$ip" -j DROP
+ iptables -A OUTPUT -d "$ip" -j DROP
+
+ echo "$ip - disabled on $(date)" >> /var/log/genesis-disabled.log
+
+ echo "[āœ“] $ip has been blocked and logged."
+}
diff --git a/vps/functions/enable_backups_by_label.sh b/vps/functions/enable_backups_by_label.sh
new file mode 100755
index 0000000..08fb31d
--- /dev/null
+++ b/vps/functions/enable_backups_by_label.sh
@@ -0,0 +1,23 @@
+enable_backups_by_label() {
+ LABEL="$1"
+ LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ https://api.linode.com/v4/linode/instances | \
+ jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id')
+
+ if [ -z "$LINODE_ID" ]; then
+ echo "āŒ No Linode found with label '$LABEL'"
+ exit 1
+ fi
+
+ echo "Enabling backups for Linode '$LABEL' (ID: $LINODE_ID)..."
+
+ HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+ https://api.linode.com/v4/linode/instances/$LINODE_ID/backups/enable \
+ -H "Authorization: Bearer $LINODE_API_TOKEN")
+
+ if [[ "$HTTP_STATUS" == "200" ]]; then
+ echo "āœ… Backups enabled for Linode $LABEL."
+ else
+ echo "āŒ Failed to enable backups (HTTP $HTTP_STATUS)"
+ fi
+}
diff --git a/vps/functions/list_all_vps.sh b/vps/functions/list_all_vps.sh
new file mode 100755
index 0000000..8ce99eb
--- /dev/null
+++ b/vps/functions/list_all_vps.sh
@@ -0,0 +1,9 @@
+list_all_vps() {
+ curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ https://api.linode.com/v4/linode/instances | \
+ jq -r '
+ .data[] | [.label, .id, .region, .type, .ipv4[0], .status] |
+ @tsv' | column -t -s $'\t' | \
+ awk 'BEGIN { print "LABEL ID REGION TYPE IP STATUS" }
+ { printf "%-11s %-10s %-10s %-16s %-15s %s\n", $1, $2, $3, $4, $5, $6 }'
+}
diff --git a/vps/functions/provision.sh b/vps/functions/provision.sh
new file mode 100755
index 0000000..f6e9d39
--- /dev/null
+++ b/vps/functions/provision.sh
@@ -0,0 +1,135 @@
+provision_vps() {
+ LABEL="$1"
+ REGION="$2"
+ TYPE="$3"
+ IMAGE="$4"
+ ROOT_PASS="${5:-$(openssl rand -base64 16)}"
+
+ if [[ "$LINODE_API_TOKEN" == "REPLACE_WITH_YOUR_LINODE_API_TOKEN" ]]; then
+ echo "āŒ Error: You must set your LINODE_API_TOKEN at the top of this script."
+ exit 1
+ fi
+
+ CLOUD_INIT=$(cat <<EOF
+#cloud-config
+hostname: genesis-vps
+manage_etc_hosts: true
+write_files:
+ - path: /usr/local/bin/genesis_squeaky.sh
+ permissions: '0755'
+ content: |
+ #!/bin/bash
+ set -e
+ GEN_HOSTNAME="genesis-vps-$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 6)"
+ LOGDIR="/home/doc/vpslogs"
+ LOGFILE="$LOGDIR/$GEN_HOSTNAME.log"
+ IP_ADDR=$(hostname -I | awk '{print $1}')
+
+ iptables -A OUTPUT -p icmp --icmp-type time-exceeded -j DROP
+ iptables -A INPUT -p udp --dport 33434:33534 -j DROP
+ iptables -A INPUT -p tcp --dport 33434:33534 -j DROP
+
+ hostnamectl set-hostname "$GEN_HOSTNAME"
+ sed -i "s/^127.0.1.1.*/127.0.1.1 $GEN_HOSTNAME/" /etc/hosts
+
+ systemctl stop linode-cloudinit 2>/dev/null || true
+ systemctl disable linode-cloudinit 2>/dev/null || true
+ touch /etc/cloud/cloud-init.disabled
+ rm -rf /etc/cloud /var/lib/cloud /var/log/cloud-init.log
+
+ rm -f /etc/motd /etc/update-motd.d/linode
+ rm -rf /usr/share/linode*
+ rm -f /etc/apt/sources.list.d/linode.list
+ apt remove --purge -y linode-cli linode-config 2>/dev/null || true
+
+ echo "[genesisctl] Attempting to log to Krang via webhook..." >> /var/log/genesis-harden.log
+ curl -s -X POST -H "Content-Type: application/json" \
+ -d "{\"host\": \"$GEN_HOSTNAME\", \"ip\": \"$IP_ADDR\", \"timestamp\": \"$(date)\"}" \
+ http://krang.core.sshjunkie.com:8080/genesislog >> /var/log/genesis-harden.log 2>&1 || echo "[genesisctl] Krang webhook logging failed" >> /var/log/genesis-harden.log
+
+ touch /var/log/genesis-hardened.ok
+
+runcmd:
+ - [ bash, /usr/local/bin/genesis_squeaky.sh ]
+EOF
+)
+
+ USER_DATA=$(echo "$CLOUD_INIT" | base64 -w 0)
+
+ echo "Provisioning VPS '$LABEL' in $REGION with type $TYPE and image $IMAGE..."
+ TMP_FILE=$(mktemp)
+ JSON_PAYLOAD=$(cat <<EOF
+{
+ "label": "$LABEL",
+ "region": "$REGION",
+ "type": "$TYPE",
+ "image": "$IMAGE",
+ "authorized_users": [],
+ "root_pass": "$ROOT_PASS",
+ "booted": true,
+ "metadata": {
+ "user_data": "$USER_DATA"
+ }
+}
+EOF
+)
+
+ HTTP_STATUS=$(curl -s -o "$TMP_FILE" -w "%{http_code}" -X POST https://api.linode.com/v4/linode/instances \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ -d "$JSON_PAYLOAD")
+
+ echo -e "\n--- HTTP STATUS: $HTTP_STATUS ---"
+ echo "--- RAW RESPONSE: ---"
+ cat "$TMP_FILE"
+
+ if [[ "$HTTP_STATUS" != "200" && "$HTTP_STATUS" != "201" ]]; then
+ echo -e "\nāŒ Failed to provision VPS (HTTP $HTTP_STATUS)"
+ jq . "$TMP_FILE"
+ exit 1
+ fi
+
+ echo -e "\nāœ… VPS provisioned:"
+ IP=$(jq -r '.ipv4[0]' "$TMP_FILE")
+ LINODE_ID=$(jq -r '.id' "$TMP_FILE")
+ echo "Label: $LABEL"
+ echo "IP Address: $IP"
+ echo "Root Password: $ROOT_PASS"
+
+ # Add DNS record to Cloudflare
+ echo "šŸ“” Adding A record for $LABEL.$CF_DOMAIN → $IP..."
+ echo "[DEBUG] CF_API_TOKEN=$CF_API_TOKEN"
+ curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
+ -H "Authorization: Bearer $CF_API_TOKEN" \
+ -H "Content-Type: application/json" \
+ --data-binary @<(cat <<JSON
+{
+ "type": "A",
+ "name": "$LABEL.$CF_DOMAIN",
+ "content": "$IP",
+ "ttl": 120,
+ "proxied": false
+}
+JSON
+) | jq '.success, .errors, .messages'
+
+ echo "ā³ Waiting indefinitely for DNS to propagate before setting rDNS..."
+i=1
+while true; do
+ CURRENT_IP=$(dig +short "$LABEL.$CF_DOMAIN")
+ if [[ "$CURRENT_IP" == "$IP" ]]; then
+ echo "āœ… A record resolved. Setting rDNS..."
+ curl -s -X PUT "https://api.linode.com/v4/linode/instances/$LINODE_ID/ips/$IP" \
+ -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"rdns": "'"$LABEL.$CF_DOMAIN"'"}'
+ break
+ fi
+ echo "ā³ Attempt $i: DNS not ready. Waiting 15s..."
+ sleep 15
+ ((i++))
+done
+
+
+ echo "$LINODE_ID|$IP|$LABEL" >> /home/doc/vpslogs/pending_rdns.log
+}
diff --git a/vps/functions/reboot_vps.sh b/vps/functions/reboot_vps.sh
new file mode 100755
index 0000000..2741b9c
--- /dev/null
+++ b/vps/functions/reboot_vps.sh
@@ -0,0 +1,7 @@
+reboot_vps() {
+ LINODE_ID="$1"
+ echo "Rebooting Linode VPS ID $LINODE_ID..."
+
+ curl -s -X POST https://api.linode.com/v4/linode/instances/$LINODE_ID/reboot \
+ -H "Authorization: Bearer $LINODE_API_TOKEN" | jq
+}
diff --git a/vps/functions/resize_vps.sh b/vps/functions/resize_vps.sh
new file mode 100755
index 0000000..c06ea91
--- /dev/null
+++ b/vps/functions/resize_vps.sh
@@ -0,0 +1,27 @@
+resize_vps() {
+ LABEL="$1"
+ NEW_TYPE="$2"
+
+ LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ https://api.linode.com/v4/linode/instances | \
+ jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id')
+
+ if [ -z "$LINODE_ID" ]; then
+ echo "āŒ No Linode found with label '$LABEL'"
+ exit 1
+ fi
+
+ echo "Resizing Linode '$LABEL' to type '$NEW_TYPE'..."
+
+ HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ -d '{"type": "'"$NEW_TYPE"'"}' \
+ https://api.linode.com/v4/linode/instances/$LINODE_ID/resize)
+
+ if [[ "$HTTP_STATUS" == "200" ]]; then
+ echo "āœ… Linode $LABEL resized to $NEW_TYPE."
+ else
+ echo "āŒ Failed to resize VPS. HTTP status: $HTTP_STATUS"
+ fi
+}
diff --git a/vps/functions/safe_create_dataset.sh b/vps/functions/safe_create_dataset.sh
new file mode 100755
index 0000000..1960e55
--- /dev/null
+++ b/vps/functions/safe_create_dataset.sh
@@ -0,0 +1,12 @@
+safe_create_dataset() {
+ FULLPATH="$1"
+
+ # Remove any trailing slash
+ FULLPATH="${FULLPATH%/}"
+
+ POOL="${FULLPATH%%/*}"
+ DATASET="${FULLPATH#*/}"
+
+ echo "šŸ›° Connecting to Shredder to safely create '${POOL}/${DATASET}'..."
+ ssh shredder "/usr/local/bin/genesis-safe-zfs.sh $POOL $DATASET"
+}
diff --git a/vps/functions/status_vps.sh b/vps/functions/status_vps.sh
new file mode 100755
index 0000000..91996e9
--- /dev/null
+++ b/vps/functions/status_vps.sh
@@ -0,0 +1,8 @@
+status_vps() {
+ LABEL="$1"
+ curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ https://api.linode.com/v4/linode/instances | \
+ jq -r --arg LABEL "$LABEL" '
+ .data[] | select(.label == $LABEL) |
+ "Label: \(.label)\nID: \(.id)\nRegion: \(.region)\nType: \(.type)\nStatus: \(.status)\nIP: \(.ipv4[0])\nCreated: \(.created)"'
+}
diff --git a/vps/functions/usage.sh b/vps/functions/usage.sh
new file mode 100755
index 0000000..25861b8
--- /dev/null
+++ b/vps/functions/usage.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+function usage() {
+ echo "Usage: genesisctl [command]"
+ echo "Commands:"
+ echo " watch-abuse Start abuse monitoring via IPTables"
+}
+
+function watch_abuse() {
+ echo "[*] Launching abuse watch via screen..."
+ screen -dmS abusewatch /usr/local/bin/genesisctl-watch-abuse.sh
+ echo "[āœ“] Abuse watch running in detached screen session 'abusewatch'"
+}
+
+case "$1" in
+ watch-abuse)
+ watch_abuse
+ ;;
+ *)
+ usage
+ ;;
+esac
diff --git a/vps/functions/verify_ptr.sh b/vps/functions/verify_ptr.sh
new file mode 100755
index 0000000..8ce2f6c
--- /dev/null
+++ b/vps/functions/verify_ptr.sh
@@ -0,0 +1,29 @@
+verify_ptr() {
+ LABEL="$1"
+ IP=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" https://api.linode.com/v4/linode/instances \
+ | jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .ipv4[0]')
+ LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" https://api.linode.com/v4/linode/instances \
+ | jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id')
+
+ if [[ -z "$IP" || -z "$LINODE_ID" ]]; then
+ echo "āŒ Could not retrieve IP or Linode ID for label '$LABEL'"
+ return 1
+ fi
+
+ echo "Re-attempting rDNS update for $LABEL ($IP)..."
+ PTR_NAME="${LABEL}.doinkle.pro"
+ RDNS_PAYLOAD=$(cat <<EOF
+{
+ "rdns": "$PTR_NAME"
+}
+EOF
+)
+
+ RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" -X PUT \
+ -H "Authorization: Bearer $LINODE_API_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "$RDNS_PAYLOAD" \
+ "https://api.linode.com/v4/linode/instances/$LINODE_ID/ips/$IP")
+
+ echo "$RESPONSE"
+}
diff --git a/vps/genesis_squeaky.sh b/vps/genesis_squeaky.sh
new file mode 100755
index 0000000..431227b
--- /dev/null
+++ b/vps/genesis_squeaky.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -e
+
+# === CONFIG ===
+GEN_HOSTNAME="genesis-vps-$RANDOM"
+TG_API_URL="https://api.telegram.org/bot<OPTIONAL-BOT>/sendMessage"
+TG_CHAT_ID="<OPTIONAL-ID>"
+
+# === STEP 1: Obfuscate Traceroute (ICMP & UDP/TCP Ports) ===
+echo "[*] Obfuscating traceroute and TTL paths..."
+iptables -A OUTPUT -p icmp --icmp-type time-exceeded -j DROP
+iptables -A INPUT -p udp --dport 33434:33534 -j DROP
+iptables -A INPUT -p tcp --dport 33434:33534 -j DROP
+echo "[+] Firewall rules added."
+
+# === STEP 2: Set a Neutral Hostname ===
+echo "[*] Setting hostname to $GEN_HOSTNAME"
+hostnamectl set-hostname "$GEN_HOSTNAME"
+sed -i "s/^127.0.1.1.*/127.0.1.1 $GEN_HOSTNAME/" /etc/hosts
+echo "[+] Hostname set."
+
+# === STEP 3: Remove Linode Metadata Access ===
+echo "[*] Disabling Linode metadata agent (if present)..."
+systemctl stop linode-cloudinit 2>/dev/null || true
+systemctl disable linode-cloudinit 2>/dev/null || true
+touch /etc/cloud/cloud-init.disabled
+rm -rf /etc/cloud /var/lib/cloud /var/log/cloud-init.log
+echo "[+] Cloud-init neutered."
+
+# === STEP 4: Scrub Linode Stuff ===
+echo "[*] Scrubbing Linode fingerprints..."
+rm -f /etc/motd /etc/update-motd.d/linode
+rm -rf /usr/share/linode*
+rm -f /etc/apt/sources.list.d/linode.list
+apt remove --purge -y linode-cli linode-config 2>/dev/null || true
+yum remove -y linode-cli linode-config 2>/dev/null || true
+echo "[+] Linode packages and branding removed."
+
+# === STEP 5: Optional Telegram Notice ===
+# Uncomment if you want to alert yourself when a VPS is hardened
+# curl -s -X POST "$TG_API_URL" -d chat_id="$TG_CHAT_ID" -d text="Genesis VPS hardened: $GEN_HOSTNAME is stealth-ready." > /dev/null
+
+# === STEP 6: Final Touch ===
+echo "[āœ…] Genesis VPS hardened. You are now off-the-grid and good to go."
diff --git a/vps/genesisctl.sh b/vps/genesisctl.sh
new file mode 100755
index 0000000..21fdf7d
--- /dev/null
+++ b/vps/genesisctl.sh
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+# genesisctl - Genesis VPS Provisioning and Reboot CLI
+# Usage:
+# genesisctl provision <label> <region> <type> <image> [root_pass]
+# genesisctl reboot <linode-id>
+# genesisctl list regions|types|images
+# genesisctl ultra <label> [root_pass]
+# genesisctl safe <label> [root_pass]
+# genesisctl micro <label> [root_pass]
+# genesisctl mastodon <label> [root_pass]
+# genesisctl destroy <label>
+
+LINODE_API_TOKEN="f8b1552bf1f2f791e16fed0c1474d56014330de1c33810527523e44a7389cb6f"
+
+# Package presets
+PACKAGE_ULTRA_REGION="us-east"
+PACKAGE_ULTRA_TYPE="g6-dedicated-4"
+PACKAGE_ULTRA_IMAGE="linode/ubuntu22.04"
+
+PACKAGE_SAFE_REGION="us-east"
+PACKAGE_SAFE_TYPE="g6-standard-2"
+PACKAGE_SAFE_IMAGE="linode/ubuntu22.04"
+
+PACKAGE_MICRO_REGION="us-east"
+PACKAGE_MICRO_TYPE="g6-nanode-1"
+PACKAGE_MICRO_IMAGE="linode/ubuntu22.04"
+
+PACKAGE_MASTODON_REGION="us-east"
+PACKAGE_MASTODON_TYPE="g6-standard-4"
+PACKAGE_MASTODON_IMAGE="linode/ubuntu22.04"
+
+for f in functions/*.sh; do source "$f"; done
+
+# Helper for DNS pre-propagation check (used after provisioning)
+await_dns_propagation() {
+ HOSTNAME="$1"
+ EXPECTED_IP="$2"
+
+ echo "ā³ Waiting for DNS A record to propagate for $HOSTNAME to $EXPECTED_IP..."
+ for i in {1..10}; do
+ ACTUAL_IP=$(dig +short "$HOSTNAME")
+ if [[ "$ACTUAL_IP" == "$EXPECTED_IP" ]]; then
+ echo "āœ… DNS A record found: $HOSTNAME → $EXPECTED_IP"
+ return 0
+ fi
+ echo "...still waiting ($i/10)..."
+ sleep 10
+ done
+ echo "āŒ DNS A record for $HOSTNAME did not propagate in time. Skipping rDNS setup."
+ return 1
+}
+
+case "$1" in
+ provision)
+ provision_vps "$2" "$3" "$4" "$5" "$6"
+ ;;
+ reboot)
+ reboot_vps "$2"
+ ;;
+ destroy)
+ destroy_vps_by_label "$2"
+ ;;
+ safe)
+ provision_vps "$2" "$PACKAGE_SAFE_REGION" "$PACKAGE_SAFE_TYPE" "$PACKAGE_SAFE_IMAGE" "$3"
+ ;;
+ ultra)
+ provision_vps "$2" "$PACKAGE_ULTRA_REGION" "$PACKAGE_ULTRA_TYPE" "$PACKAGE_ULTRA_IMAGE" "$3"
+ ;;
+ micro)
+ provision_vps "$2" "$PACKAGE_MICRO_REGION" "$PACKAGE_MICRO_TYPE" "$PACKAGE_MICRO_IMAGE" "$3"
+ ;;
+ mastodon)
+ provision_vps "$2" "$PACKAGE_MASTODON_REGION" "$PACKAGE_MASTODON_TYPE" "$PACKAGE_MASTODON_IMAGE" "$3"
+ ;;
+ backup)
+ enable_backups_by_label "$2"
+ ;;
+ disable-backup)
+ disable_backups_by_label "$2"
+ ;;
+ status)
+ status_vps "$2"
+ ;;
+ listvps)
+ list_all_vps
+ ;;
+ disable)
+ disable_ip "$2"
+ ;;
+ resize)
+ resize_vps "$2" "$3"
+ ;;
+ safe-create)
+ safe_create_dataset "$2" "$3"
+ ;;
+ verify_ptr)
+ verify_ptr "$2"
+ ;;
+ *)
+ echo "Usage: $0 <command> [...]"
+ echo "Available commands: provision, reboot, destroy, safe, ultra, micro, mastodon"
+ exit 1
+ ;;
+esac