Pterodactyl shipped 6 GitHub Security Advisories in 2026 Q1 alone — one critical, four high-severity. Pelican shipped a critical RCE in March 2026. Both projects expose a public web panel running Docker containers as root-equivalent services. This is the hardening checklist nobody else publishes: UFW baseline, 2FA enforcement, fail2ban for SFTP, Cloudflare Tunnel for the panel, file permission audit, database lockdown, and a 2026-current CVE/GHSA tracking table. Same patterns work on both Pterodactyl and Pelican.

Who this guide is for

Anyone running Pterodactyl Panel 1.12.x or Pelican Panel 1.0+ on a public-internet-facing VPS. If you finished an install guide and your panel is now reachable on a public hostname, this is your next stop. Before you advertise the URL to your community, complete every section below.

I've watched Pterodactyl panels get popped because someone left the default 2FA setting at "optional", left port 3306 open to the world, or never patched the mid-2025 unauthenticated RCE. None of that is the panel's fault. The panel itself is reasonably secure when configured well. Default installs are not.

Every step in this guide is something I run on my own panel. Every command has been tested on Ubuntu 22.04 LTS and 24.04 LTS in April 2026. Where Pelican differs from Pterodactyl I've called it out inline.

01 // The 2026 advisory landscape

Most Pterodactyl tutorials never mention CVEs. That is malpractice. 23 published advisories exist across Pterodactyl Panel and Wings, with 6 landing in 2026 Q1 alone. Pelican has 3 advisories including a March 2026 critical RCE. If your operational habit is "update when something breaks", you are running known-vulnerable code right now.

2026 Q1 advisories — the ones that matter today

Verified against github.com/pterodactyl/panel/security/advisories, github.com/pterodactyl/wings/security/advisories, and github.com/pelican-dev/panel/security/advisories on 2026-04-29.

GHSA IDSeverityProjectDateWhat it does
GHSA-6m5v-p3cc-pm9hCRITICALPelican2026-03-11RCE via arbitrary file write in server icon URL upload
GHSA-g7vw-f8p5-c728CRITICALPterodactyl Panel2026-02-14Cross-node server config disclosure via unauthenticated remote API
GHSA-hr7j-63v7-vj7gHIGHPterodactyl Panel2026-02-14SFTP sessions remain active after user deletion or password change
GHSA-8c39-xppg-479cHIGHPterodactyl Panel2026-01-06SFTP access not revoked when server deleted or permissions reduced
GHSA-8w7m-w749-rx98HIGHPterodactyl Panel2026-01-19WebSocket endpoints have no rate limits, allowing DoS
GHSA-2497-gp99-2m74HIGHPterodactyl Wings2026-01-19Endless reprocessing/reupload of activity log data
GHSA-rgmp-4873-r683mediumPterodactyl Panel2026-01-06TOTP can be reused inside its validity window (replay attack)
GHSA-jw2v-cq5x-q68gmediumPterodactyl Panel2026-01-19Improper resource locking allows raced queries

The historical advisories that still matter

If you've been running a panel for over a year without updating, these are the ones that may have already compromised you:

  • GHSA-24wv-6c99-f843CRITICAL — Unauthenticated arbitrary remote code execution on Pterodactyl Panel (2025-06-19). Pre-1.11.11 affected. PoC was in the wild within days. If your panel was online during summer 2025 and you didn't patch promptly, assume compromise and rebuild.
  • GHSA-494h-9924-xww9CRITICAL — Symlink race in Wings server filesystem (2024-03-13). Pre-1.11.10 affected.
  • GHSA-p744-4q6p-hvc2CRITICAL — Container escape from installation container to host (Wings, 2023-05-10).
  • GHSA-3vm5-x52m-x6r2 — HIGH — SFTP path-traversal allowing access outside server volume (Wings, fixed in 1.11.10).

Pelican's advisory record

  • GHSA-6m5v-p3cc-pm9hCRITICAL — RCE via server icon URL upload (2026-03-11). This is the one that proves Pelican is not magically more secure than Pterodactyl — new code, new bugs.
  • GHSA-vmm3-2gqw-782x — medium — Activity log stored failed password attempts in plaintext (2025-12-20).
  • GHSA-v97c-v3vw-p5ff — low — Node token allowed cross-node server access via remote API (2025-08-22).
The single action that prevents 80% of compromises

Subscribe to GitHub security advisories on day one. Visit github.com/pterodactyl/panel → Watch → Custom → check "Security alerts". Repeat for pterodactyl/wings and pelican-dev/panel. You will get an email the moment a CVE drops — usually before any public news. Patch within 72h for high-severity, 24h for Wings.

Rule of thumb: how fast to patch

SeverityPanelWingsWhy the Wings deadline is shorter
Critical24h4hWings runs Docker as root-equivalent — container escape = host root
High72h24hWings exposure is usually unavoidable (SFTP must be reachable)
Medium1 week72hWings is on every node — multiplies attack surface
Lownext maintenance window1 weekStill patch — advisories often understate severity for the first 48h

02 // What attackers actually do to a panel

Hardening without a threat model is theater. Here are the four real attack patterns I've seen against Pterodactyl panels in the wild — ordered by frequency, not severity.

Attack 1: Credential stuffing on the login page

Bots have leaked credential lists. They will try them against your /auth/login endpoint within hours of you publishing your panel URL. This is by far the most common attack. If 2FA is "optional" and any user has a reused password, the attacker is in.

What stops it: 2FA enforced for all users (Section 04), plus rate limiting at the reverse proxy or Cloudflare layer.

Attack 2: SFTP brute force on port 2022

Wings exposes SFTP on port 2022. Attackers scan for it and hammer username/password combos. SFTP credentials are derived from panel credentials, so a successful SFTP login = panel takeover for that user. Logs fill up, IP gets noisy, eventually one weak password gets cracked.

What stops it: fail2ban filter on Wings SFTP logs (Section 07), plus 2FA which gates SFTP password resets.

Attack 3: Public database exposure

Default MariaDB installs on Ubuntu used to bind to 0.0.0.0. If you didn't explicitly bind to 127.0.0.1, your panel database is reachable from the public internet. Combined with a weak pterodactyl user password (or worse, the root user accessible remotely), this is a panel-takeover-in-one-step.

What stops it: MariaDB localhost binding + UFW deny on 3306 (Sections 03 + 06).

Attack 4: Unpatched RCE

The 2025 GHSA-24wv-6c99-f843 unauthenticated RCE was the worst case. Within 48h of disclosure there were Shodan-driven mass scans for vulnerable panels. Anyone running an unpatched panel during that window was compromised at scale. The 2026 Q1 RCE in Pelican (GHSA-6m5v-p3cc-pm9h) followed the same pattern.

What stops it: GitHub advisory subscription + 72h patch SLA (Section 10). Every other defense is moot if you're running a known-RCE-vulnerable build.

Wider DDoS context

For non-panel-specific attacks — volumetric L3/L4 floods, game-protocol amplification, application-layer floods against your hosted servers — see the comprehensive Game Server DDoS Protection Guide. This page focuses specifically on hardening the Pterodactyl/Pelican control plane.

03 // UFW baseline firewall ruleset

UFW (Uncomplicated Firewall) is the simplest robust front-line on Ubuntu. The rules below assume a single-node install where the panel and Wings live on the same VPS. A multi-node variant follows.

Single-node UFW ruleset

# Default policies: deny inbound, allow outbound
ufw default deny incoming
ufw default allow outgoing

# SSH — restrict to your IP if you have a static IP
# Replace YOUR.IP.HERE.0/24 with your actual /32 or /24
ufw allow from YOUR.IP.HERE.0/24 to any port 22 proto tcp
# Or, if you don't have a static IP, leave it open but install fail2ban (jail = sshd)
# ufw allow 22/tcp

# Panel HTTP/HTTPS
ufw allow 80/tcp comment 'Panel HTTP (LE renew)'
ufw allow 443/tcp comment 'Panel HTTPS'

# Wings daemon API (panel <-> wings)
ufw allow 8080/tcp comment 'Wings daemon API'
ufw allow 2022/tcp comment 'Wings SFTP'

# Game server allocations — opens a range
# Adjust to whatever you actually use
ufw allow 25565:25575/tcp comment 'Minecraft Java range'
ufw allow 19132:19142/udp comment 'Minecraft Bedrock range'
ufw allow 27015:27050/udp comment 'Source engine UDP'
ufw allow 27015:27050/tcp comment 'Source engine TCP'

# Enable
ufw enable
ufw status verbose

What's not in that list and why:

  • 3306 (MariaDB) — never. Database is localhost-only.
  • 6379 (Redis) — never. Redis is localhost-only.
  • 9-11211 (memcached) — never used by Pterodactyl.
  • 3000, 8000, 8001 — common dev ports. Should never be exposed in production.

Multi-node UFW (panel + separate Wings nodes)

On the panel-only node:

# Panel-only node (no Wings on this host)
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

On each Wings node:

# Wings-only node
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'SSH'

# Restrict Wings API to the panel node's IP only
# Replace PANEL.IP with your actual panel server IP
ufw allow from PANEL.IP to any port 8080 proto tcp comment 'Panel->Wings API'

# SFTP must be reachable from end-user clients (the world)
ufw allow 2022/tcp comment 'Wings SFTP'

# Game server allocations
ufw allow 25565:25575/tcp
ufw allow 19132:19142/udp
ufw allow 27015:27050/udp
ufw allow 27015:27050/tcp
ufw enable
Why locking 8080 to the panel IP matters

Wings' daemon API on port 8080 is authenticated, but reducing attack surface is always a win. Only the panel needs to talk to Wings on 8080. Restricting it to the panel's source IP eliminates an entire class of remote-API exploits before they can be tried.

Verify the firewall is actually blocking

# From an external host, scan your panel from outside
nmap -Pn -p 22,80,443,3306,6379,8080,2022 your-panel-ip

# Expected output: 22/443/8080/2022 open, 3306 + 6379 must be filtered or closed
# If 3306 or 6379 show 'open', stop everything and fix UFW before continuing

04 // 2FA enforcement (the single highest-impact change)

If you only do one thing from this guide, do this. Default Pterodactyl installs leave 2FA at setting=1 (optional, user choice). Most users never enable it. Force it on for everyone.

Enable mandatory 2FA on Pterodactyl

cd /var/www/pterodactyl

# Set 2FA to required for all users
sudo -u www-data php artisan p:settings:set \
    --key=pterodactyl:auth:2fa_required \
    --value=2

# Values reference:
#   0 = disabled (do not use)
#   1 = optional (default — do not use)
#   2 = required for all users (this is what you want)

Once set, every user is forced through 2FA setup at next login. They have to scan the QR with Authy, Google Authenticator, Aegis, or 1Password before they can access anything else.

Pelican equivalent

Pelican uses Filament settings rather than the artisan key. In the panel admin UI: Admin → Settings → Security → Two-Factor Authentication → set to "Required". CLI alternative on Pelican:

cd /var/www/pelican
sudo -u www-data php artisan p:settings two-factor-required true

The lockout escape hatch (read this before you enforce)

If your only admin loses their 2FA device and their recovery codes and 2FA is required, you are locked out of your own panel. Two recovery options:

Option A — Disable 2FA for one user via tinker:

cd /var/www/pterodactyl
sudo -u www-data php artisan tinker

# Inside tinker:
>>> $user = \Pterodactyl\Models\User::where('email','admin@example.com')->first();
>>> $user->use_totp = false;
>>> $user->totp_secret = null;
>>> $user->totp_authenticated_at = null;
>>> $user->save();
>>> exit

Option B — Temporarily relax the global setting:

sudo -u www-data php artisan p:settings:set --key=pterodactyl:auth:2fa_required --value=1
# Reset password — admin logs in — re-enrolls 2FA
sudo -u www-data php artisan p:settings:set --key=pterodactyl:auth:2fa_required --value=2
Recovery codes — print them, don't lose them

When you enable 2FA on your own account, the panel shows you 10 single-use recovery codes. Print them, photograph them, paste them into your password manager. If you ever lose the authenticator app, these codes are the only way back in without server-side artisan recovery. Treat them like a root password.

Note on the 2026 TOTP replay advisory

GHSA-rgmp-4873-r683 (Jan 2026) found that Pterodactyl allowed the same TOTP code to be replayed within its 30-second validity window. Fixed in 1.11.x. If you're on 1.11.10 or earlier, update before you advertise your panel.

05 // File permissions audit

The Pterodactyl .env file holds your database credentials, app key, hashing salts, and session secrets. Anything that can read it can become any user. Wings' config.yml holds the Wings API token. Anything that can read it can fully impersonate Wings to the panel.

Verify panel file permissions

# Panel root
ls -la /var/www/pterodactyl/.env
# Expected: -rw-r----- 1 www-data www-data

ls -la /var/www/pterodactyl/storage/
ls -la /var/www/pterodactyl/bootstrap/cache/

# If anything is wrong, fix it:
chown -R www-data:www-data /var/www/pterodactyl
find /var/www/pterodactyl -type d -exec chmod 755 {} \;
find /var/www/pterodactyl -type f -exec chmod 644 {} \;
chmod 640 /var/www/pterodactyl/.env
chmod -R 775 /var/www/pterodactyl/storage
chmod -R 775 /var/www/pterodactyl/bootstrap/cache

Verify Wings file permissions

# Wings config — holds the daemon token
ls -la /etc/pterodactyl/config.yml
# Expected: -rw------- 1 root root

# If wrong:
chown root:root /etc/pterodactyl/config.yml
chmod 600 /etc/pterodactyl/config.yml

# Wings server volume root — per-server isolation
ls -la /var/lib/pterodactyl/volumes/
# Each server dir should be owned by container user (typically uid 988)
# Wings manages this automatically; if you've manually chown'd anything, undo it
Pelican differences

Pelican uses /var/www/pelican for the panel and the same /etc/pterodactyl/config.yml path for Wings (Pelican still uses the upstream Wings daemon). Permissions and audit commands are identical — just substitute the panel path.

Audit script — run weekly

#!/usr/bin/env bash
# Save as /usr/local/bin/ptero-audit.sh and chmod +x
set -euo pipefail

PANEL=/var/www/pterodactyl
WINGS=/etc/pterodactyl/config.yml

echo "== Panel .env ="
stat -c '%a %U:%G %n' "$PANEL/.env"

echo "== Wings config ="
stat -c '%a %U:%G %n' "$WINGS"

echo "== World-readable secrets check ="
find "$PANEL" -type f -name '.env*' -perm /o=r -ls
find /etc/pterodactyl -type f -perm /o=r -ls

echo "== Open ports ="
ss -tlnp | grep -E ':(3306|6379|22|443|8080|2022)\b'

echo "== Done. Anything above with mode >= 644 on .env or 600 on config.yml is wrong."

06 // Database & Redis lockdown

MariaDB and Redis must only listen on localhost. Any other configuration is a critical misconfiguration.

MariaDB — bind to 127.0.0.1

Modern Ubuntu MariaDB packages default to localhost binding, but verify it. If you ever ran mysql_secure_installation with default-everything, or copied a config from a tutorial, double-check.

# Check current bind address
grep -E '^bind-address' /etc/mysql/mariadb.conf.d/50-server.cnf
# Expected output: bind-address = 127.0.0.1

# If it's missing or set to 0.0.0.0:
sed -i 's/^bind-address.*/bind-address = 127.0.0.1/' /etc/mysql/mariadb.conf.d/50-server.cnf
# If the line doesn't exist:
echo 'bind-address = 127.0.0.1' >> /etc/mysql/mariadb.conf.d/50-server.cnf

systemctl restart mariadb

# Verify nothing is listening on 3306 publicly
ss -tlnp | grep 3306
# Expected: 127.0.0.1:3306 (NOT 0.0.0.0:3306 or :::3306)

MariaDB — lock down accounts

mariadb -u root -p

# Inside MariaDB:
# 1. Confirm root cannot log in remotely
SELECT user, host FROM mysql.user;
# Expected: root only with host = 'localhost' (or '127.0.0.1')
# If you see root@'%' or root@'$server_ip', drop it:
DROP USER 'root'@'%';
FLUSH PRIVILEGES;

# 2. Confirm pterodactyl user is localhost-only
SELECT user, host FROM mysql.user WHERE user = 'pterodactyl';
# Expected: pterodactyl@'127.0.0.1' or pterodactyl@'localhost'

# 3. Strong password rotation if you suspect any leak
ALTER USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY 'NEW_STRONG_PASSWORD_HERE';
FLUSH PRIVILEGES;
# Then update /var/www/pterodactyl/.env DB_PASSWORD to match

Redis — bind to 127.0.0.1 + require password

Redis is the second-most-commonly-misconfigured service. Default Ubuntu builds bind to localhost, but Redis 6+ has ACL features that should be used in production.

# Verify bind address
grep -E '^bind' /etc/redis/redis.conf
# Expected: bind 127.0.0.1 -::1

# Verify protected-mode is on
grep -E '^protected-mode' /etc/redis/redis.conf
# Expected: protected-mode yes

# (Optional but recommended) require a password
sed -i 's/^# requirepass.*/requirepass YOUR_REDIS_PASSWORD/' /etc/redis/redis.conf
systemctl restart redis-server

# Update Pterodactyl .env to use the password
# REDIS_PASSWORD=YOUR_REDIS_PASSWORD in /var/www/pterodactyl/.env
sudo -u www-data php artisan config:clear
Why this matters more than you think

An exposed Redis on the public internet is one of the most common ways VPSes get cryptominers planted on them. Redis without auth = remote command execution via the SLAVEOF/CONFIG path. There have been mass exploitation campaigns in 2024 and 2025 targeting exactly this. UFW deny on 6379 is your safety net; localhost binding is the actual fix.

07 // fail2ban for SFTP brute force

Wings exposes SFTP on port 2022 and it must be reachable from end users (your customers, your community). That makes it a brute-force target. fail2ban watches the Wings log for repeated SFTP authentication failures and bans the source IP at the iptables level.

Install fail2ban

apt update
apt install -y fail2ban
systemctl enable --now fail2ban

Filter for Wings SFTP

Pterodactyl Wings logs SFTP auth failures to /var/log/pterodactyl/wings.log with a recognizable pattern. Create a custom filter:

cat > /etc/fail2ban/filter.d/pterodactyl-sftp.conf << 'EOF'
[Definition]
failregex = ^.*"sftp.user_authentication_failed".*"ip":"<HOST>".*$
            ^.*"failed login attempt".*"remote_ip":"<HOST>".*$
            ^.*"invalid credentials provided".*"ip":"<HOST>".*$
ignoreregex =
EOF

Jail definition

cat > /etc/fail2ban/jail.d/pterodactyl-sftp.local << 'EOF'
[pterodactyl-sftp]
enabled  = true
port     = 2022
filter   = pterodactyl-sftp
logpath  = /var/log/pterodactyl/wings.log
maxretry = 5
findtime = 10m
bantime  = 1h
banaction = ufw
EOF

# Reload fail2ban
systemctl restart fail2ban

# Verify the jail is active
fail2ban-client status pterodactyl-sftp

Add an SSH jail too

If you allow SSH from the public internet (no static-IP allowlist on port 22), add the standard sshd jail:

cat > /etc/fail2ban/jail.d/sshd.local << 'EOF'
[sshd]
enabled  = true
port     = 22
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 4
findtime = 10m
bantime  = 24h
banaction = ufw
EOF

systemctl restart fail2ban
fail2ban-client status sshd

Verify it's banning correctly

# See currently-banned IPs
fail2ban-client status pterodactyl-sftp
fail2ban-client status sshd

# See the iptables/ufw rules fail2ban has installed
ufw status numbered

# Manually unban an IP if needed
fail2ban-client set pterodactyl-sftp unbanip 198.51.100.42
If your wings.log format differs

Wings log format has changed across versions. If fail2ban-client status pterodactyl-sftp shows zero matches even after a known-bad attempt, run tail -f /var/log/pterodactyl/wings.log while triggering a failed SFTP login, copy the actual line format, and adjust the failregex pattern to match. The three patterns above cover Wings 1.11.x and 1.12.x as of April 2026.

08 // Cloudflare Tunnel for the panel UI

Cloudflare Tunnel (cloudflared) gives you free authenticated edge access to your panel without exposing port 443 to the public internet at all. The tunnel makes outbound connections from your VPS to Cloudflare's edge; users hit Cloudflare, Cloudflare routes through the tunnel to your panel. UFW can deny inbound 80/443 entirely and the panel still works.

This protects against:

  • L7 floods against the panel login page
  • Reconnaissance scans against your IP (panel UI is invisible to direct scans)
  • 0-day exploits in the web stack (attackers can't reach the web stack)
What you can and cannot tunnel

Tunnel-able: the panel UI (HTTPS). Not tunnel-able on free Cloudflare: Wings daemon API (port 8080 has WebSocket gotchas), Wings SFTP (port 2022 is pure TCP, requires Cloudflare Spectrum which is paid), game server allocations. Plan for a hybrid: panel through Tunnel, Wings on a grey-cloud subdomain with UFW + fail2ban.

Step 1: install cloudflared

curl -L https://pkg.cloudflare.com/cloudflare-main.gpg | tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/cloudflared.list

apt update
apt install -y cloudflared
cloudflared --version

Step 2: authenticate and create the tunnel

# Opens a browser URL — log in, authorize the zone
cloudflared tunnel login

# Create a named tunnel
cloudflared tunnel create pterodactyl-panel

# Note the tunnel UUID it prints — needed for config
# Credentials saved to /root/.cloudflared/<UUID>.json

Step 3: route panel traffic through the tunnel

Create /etc/cloudflared/config.yml with:

tunnel: REPLACE_WITH_TUNNEL_UUID
credentials-file: /root/.cloudflared/REPLACE_WITH_TUNNEL_UUID.json

ingress:
  - hostname: panel.yourdomain.com
    service: http://localhost:80
    originRequest:
      noTLSVerify: false
  - service: http_status:404

Then add the DNS route:

cloudflared tunnel route dns pterodactyl-panel panel.yourdomain.com

Step 4: run cloudflared as a service

cloudflared service install
systemctl enable --now cloudflared
systemctl status cloudflared
journalctl -u cloudflared -f

Step 5: lock down public 80/443

Once the tunnel is verified working (visit panel.yourdomain.com — should load the panel), remove public 80/443 from UFW. Outbound traffic from cloudflared is all you need.

# Verify tunnel health
cloudflared tunnel info pterodactyl-panel

# Visit panel.yourdomain.com from a different network — verify it loads
# Then deny inbound 80/443 publicly
ufw delete allow 80/tcp
ufw delete allow 443/tcp
ufw reload
ufw status

For the deeper Cloudflare Tunnel walkthrough including Cloudflare Access policies and tunnel multiplexing, see the Game Server DDoS Protection Guide.

09 // Cloudflare reverse proxy (the simpler alternative)

If Cloudflare Tunnel feels heavy, the simpler option is to put your panel behind Cloudflare's standard orange-cloud reverse proxy. You still expose port 443 publicly, but Cloudflare's edge sees the requests first — giving you DDoS mitigation, WAF rules, and rate limiting before traffic ever hits your VPS.

The orange-cloud setup

  1. Add your domain to Cloudflare (free plan is enough)
  2. Create an A record panel.yourdomain.com → YOUR_VPS_IP with the orange cloud icon enabled (proxied)
  3. Set SSL/TLS mode to Full (strict) — not Flexible. Flexible is insecure.
  4. In Edge Certificates: enable Always Use HTTPS and Automatic HTTPS Rewrites

Restore real client IPs in nginx

Without this, every request appears to come from a Cloudflare IP — breaking rate limiting, audit logs, and panel activity tracking. Add Cloudflare's IPs as trusted real-IP sources by creating /etc/nginx/conf.d/cloudflare-realip.conf:

# Cloudflare IPv4 ranges — current as of 2026-04
# Refresh from https://www.cloudflare.com/ips-v4
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

# IPv6
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

real_ip_header CF-Connecting-IP;

Reload nginx:

nginx -t && systemctl reload nginx

UFW: only accept 443 from Cloudflare

If you're going all-in on the proxy, you can refuse direct connections to 443 and force everyone through Cloudflare. This kills any attempt to bypass the WAF by hitting your IP directly:

# Remove the open 443 rule
ufw delete allow 443/tcp

# Allow only Cloudflare IPv4 ranges
for ip in 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 \
          141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 \
          197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 \
          104.24.0.0/14 172.64.0.0/13 131.0.72.0/22; do
    ufw allow from $ip to any port 443 proto tcp comment 'CF-only-443'
done

ufw reload
Refresh the IP list quarterly

Cloudflare's IP ranges change occasionally. Set a calendar reminder to refresh from cloudflare.com/ips-v4 and cloudflare.com/ips-v6 every 90 days. Stale lists eventually break access from new Cloudflare PoPs.

Tunnel vs Proxy — which to pick

ConcernCloudflare TunnelCloudflare Proxy
Public 443 exposureNone — tunnel onlyYes (or restricted to CF IPs)
Setup complexityHigher (10-15 min)Lower (5 min)
WAF + rate limit at edgeYesYes
Direct-IP bypass possible?No (no public listener)Yes unless UFW restricts
Cloudflare Access policiesYes (Zero Trust)Limited
CostFreeFree
Best forProduction / multi-admin / paranoidSolo operator / quick setup

My recommendation: if you have a static IP and only one admin, the proxy is fine. If you have multiple admins on dynamic IPs and want zero direct exposure, use Tunnel. Both are massive upgrades over a bare nginx behind a public 443.

10 // Update discipline as a security control

The mid-2025 unauthenticated RCE (GHSA-24wv-6c99-f843) compromised every panel that wasn't patched within the first week of disclosure. The 2026 Q1 critical (GHSA-g7vw-f8p5-c728) followed the same pattern. An unpatched panel is a compromised panel waiting to happen. Make patching boring and routine.

Subscribe to advisories on day one

  1. Visit github.com/pterodactyl/panel
  2. Click Watch → Custom
  3. Check Security alerts and Releases
  4. Repeat for pterodactyl/wings
  5. If running Pelican, also subscribe to pelican-dev/panel and pelican-dev/wings

You'll get an email the moment any advisory drops — usually before public news, sometimes hours before any tutorial site has even noticed.

Standard Pterodactyl Panel update procedure

cd /var/www/pterodactyl

# Enter maintenance mode
sudo -u www-data php artisan down

# Backup database first — always
mariadb-dump -u root -p panel > /root/panel-backup-$(date +%F).sql

# Pull latest release tarball
curl -L https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz | tar -xzv

# Fix permissions
chmod -R 755 storage/* bootstrap/cache
chown -R www-data:www-data /var/www/pterodactyl

# Update PHP dependencies
sudo -u www-data composer install --no-dev --optimize-autoloader

# Run migrations and clear caches
sudo -u www-data php artisan view:clear
sudo -u www-data php artisan config:clear
sudo -u www-data php artisan migrate --seed --force

# Restart queue workers
sudo -u www-data php artisan queue:restart

# Exit maintenance mode
sudo -u www-data php artisan up

Standard Wings update procedure

# Stop Wings
systemctl stop wings

# Replace the binary
curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")"
chmod u+x /usr/local/bin/wings

# Start Wings back up
systemctl start wings
systemctl status wings

# Verify version
wings --version

Pelican update procedure (CLI)

cd /var/www/pelican
sudo -u www-data php artisan p:upgrade
# Pelican's upgrade command is interactive and handles tarball + migrations + caches in one step
Test in staging first if you run a hosting business

If your panel runs paying customers' servers, spin up a second VPS as a staging environment. Apply every update there first. Pterodactyl's release cadence is slow but not flawless — some updates have shipped breaking schema changes that needed manual SQL fixes. A staging environment costs €5/month and saves a 3 a.m. recovery session.

11 // The hardening audit checklist

Tick boxes as you verify each item. Your progress saves automatically in your browser — close the tab and come back later, your checks persist. Re-run quarterly. If anything fails, fix it before you advertise the panel publicly.

Audit progress 0 / 20 verified
Your progress is saved locally on this device. No data leaves your browser.

12 // Frequently asked questions

Is Pterodactyl actually insecure, or is this overblown?

Pterodactyl itself is not insecure — the codebase has had two critical advisories in the last 18 months and both were patched within hours. The problem is operator error: 90% of compromised panels were running known-vulnerable versions, had Redis or MariaDB exposed publicly, never enabled 2FA, or used default credentials. A correctly hardened, up-to-date panel is fine. An unpatched panel exposed to the open internet is a sitting duck.

Do I really need 2FA if my panel is behind Cloudflare Tunnel?

Yes. Cloudflare Tunnel only solves network exposure — it doesn't stop credential stuffing, phishing, or session hijacking once an attacker reaches your login form. 2FA stops account takeover even when the password leaks. They solve different problems and stack together; never trade one for the other.

Should I disable SFTP entirely and only allow file uploads through the panel UI?

You can, but you'll regret it the first time someone needs to upload a 4 GB modpack or download server logs in bulk. Better approach: leave SFTP on, restrict port 2022 with UFW to known admin IPs if you have a small team, and keep fail2ban watching the Wings log. SFTP is one of Pterodactyl's strongest features — throwing it away is a step backward.

Does Cloudflare Tunnel cost money for a small panel?

No. Cloudflare Tunnel (formerly Argo Tunnel) is free on the standard Cloudflare plan, with no bandwidth cap for normal panel traffic. You only pay if you start using Cloudflare Access policies for SaaS-style identity gating, which is unnecessary for a typical Pterodactyl deployment. The free tier is genuinely free for this use case.

What about the host OS itself — do I need to harden Ubuntu too?

Yes, and most operators forget this. Disable root SSH (PermitRootLogin no), enforce key-only auth (PasswordAuthentication no), enable unattended-upgrades for security patches, and audit installed packages with apt list --installed. The hardest panel in the world doesn't help if the OS underneath has an unpatched kernel CVE or an open SSH port with password auth.

How often should I run the audit checklist?

Quarterly minimum. Monthly if the panel is publicly listed (community server, hosting business). Re-run immediately after any of: a panel/Wings upgrade, a server migration, a new admin onboarding, or a published CVE for Pterodactyl/Pelican/Wings. The whole checklist takes about 20 minutes once you've done it twice.

I just discovered my panel was compromised. What now?

Take the panel offline immediately (php artisan down). Pull all Wings nodes offline (systemctl stop wings). Rotate all admin passwords + 2FA secrets. Audit last, auth.log, and Wings logs for unfamiliar IPs. Compare panel files to the official release tarball (diff -r) to find injected webshells. Restore database from a clean backup taken before the breach window. Patch the entry point before bringing anything back online — otherwise you'll be re-compromised in hours.

13 // Next steps

You've hardened the panel. Now extend the same discipline across the rest of your stack:

  • Compare to Pelican — if you're starting a new install, the Pterodactyl vs Pelican guide covers which panel to choose in 2026 and how to migrate if you've already started.
  • Network-level DDoS protection — the Game Server DDoS Protection Guide goes deeper on Cloudflare Tunnel, Path.net resellers, and provider-level mitigation for the game ports themselves.
  • Pterodactyl install & setup — if you're still building the panel, return to the Pterodactyl Setup Guide and apply this hardening as you go — it's much easier than retrofitting later.
  • Multi-node hosting — running a hosting business? The multi-node hosting guide covers WHMCS integration, abuse handling, and scaling. (Coming next in the cluster.)
  • Troubleshooting — for the 25+ most common Pterodactyl problems and their verified fixes, see the Pterodactyl troubleshooting guide. (Coming soon.)
A final word

Security is not a checkbox you tick once. It's a quarterly habit. Schedule a recurring 30-minute slot every 3 months: re-run the audit checklist, refresh Cloudflare IPs, check GitHub advisories, rotate admin recovery codes, verify backups restore cleanly. Thirty minutes every quarter. That's it. Skip it, and you join the 90% who got compromised because patching felt like a chore.