WSG is a small SSH/SFTP gateway for Pterodactyl Wings nodes.
Put one public SFTP endpoint in front of many Wings nodes, then route each login to the correct node. This is useful when the nodes are reachable over private infrastructure such as Tailscale, WireGuard, a private VLAN, or another reverse-tunnel/backbone network.
The gateway does not replace Wings authentication. Clients still log in with the normal Pterodactyl SFTP username and password. WSG forwards the connection to the selected Wings node and lets Wings decide whether the login is valid.
Internet
|
| sftp.example.com:2022
v
WSG gateway
|
| private network / VPN / tunnel backbone
|
+-- wings-atlanta:2022
+-- wings-boston:2022
+-- wings-frankfurt:2022
A common deployment is:
- Run WSG on one public edge server.
- Connect all Wings nodes and the WSG server to the same private network, for example Tailscale.
- Expose only WSG to users.
- Keep Wings SFTP ports private or firewall them to the backbone.
- Point the panel/user-facing SFTP hostname at WSG.
This gives you one stable SFTP endpoint while keeping node-to-node connectivity off the public internet.
WSG expects Pterodactyl-style SFTP usernames:
username.serveruuid
Example:
admin.a1b2c3d4
The suffix is the first 8 hex characters of the server UUID. WSG uses it to find the target node. The password is still checked by Wings.
The published image is available from GitHub Container Registry:
docker pull ghcr.io/langduamc/wsg:latestRun it with a mounted config and persistent host key directory:
docker run -d \
--name wsg \
--restart unless-stopped \
-p 2022:2022 \
-v $(pwd)/gateway.toml:/app/gateway.toml:ro \
-v $(pwd)/data:/app/data \
ghcr.io/langduamc/wsg:latest \
wsg /app/gateway.tomlUse a fixed image tag in production if you do not want automatic updates when latest changes:
docker pull ghcr.io/langduamc/wsg:maincargo build --release
./target/release/wsg gateway.tomlRequirements when running outside Docker:
- Rust 1.85 or newer
ssh-keygenfrom OpenSSH onPATH- TCP access from WSG to each Wings node SFTP port
Install and join Tailscale on the WSG server and every Wings node.
On the WSG server:
sudo tailscale upOn each Wings node:
sudo tailscale upUse the node's Tailscale DNS name or Tailscale IP in gateway.toml:
[node.a1b2c3d4]
sftp = "wings-atlanta.tailnet-name.ts.net:2022"
[node.e5f6a7b8]
sftp = "100.92.10.44:2022"Firewall suggestion:
- Public internet can reach WSG on
2022. - WSG can reach Wings nodes on their SFTP port over Tailscale.
- Public internet cannot reach Wings SFTP directly.
If nodes sit behind NAT, terminate your tunnel/backbone so WSG can dial a stable private address for each Wings SFTP service.
Examples of valid sftp values:
[node.a1b2c3d4]
sftp = "10.10.0.12:2022"
[node.e5f6a7b8]
sftp = "boston.internal.example:2022"WSG only needs normal TCP connectivity to the backend SFTP address.
WSG has two ways to map a server UUID suffix to a Wings node.
Use this when you want explicit routing in gateway.toml and do not want WSG to call the panel API.
In static mode, each [node.<server_uuid_prefix>] key should be the 8-character server UUID suffix that users place after the username. The api value still needs to exist in the TOML file, but it is not used when api_key is empty.
api = "https://panel.example.com"
api_key = ""
[listen]
bind = "0.0.0.0"
port = 2022
[ssh]
host_key_path = "data/host_key"
max_auth_tries = 6
auth_rejection_time_secs = 3
[node.a1b2c3d4]
sftp = "wings-atlanta.tailnet-name.ts.net:2022"
[node.e5f6a7b8]
sftp = "wings-boston.tailnet-name.ts.net:2022"
[logging]
level = "info"
[rate_limit]
connections_per_sec = 5.0
auth_per_minute = 30
burst_size = 10Static mode is selected when api_key is empty.
Use this when WSG should resolve the server UUID through the Pterodactyl Application API.
api = "https://panel.example.com"
api_key = "ptla_your_application_api_key"
[listen]
bind = "0.0.0.0"
port = 2022
[ssh]
host_key_path = "data/host_key"
max_auth_tries = 6
auth_rejection_time_secs = 3
[cache]
capacity = 5000
ttl_secs = 60
[logging]
level = "info"
[rate_limit]
connections_per_sec = 5.0
auth_per_minute = 30
burst_size = 10The panel API key must be an Application API key that can read servers and node data.
| Key | Default | Description |
|---|---|---|
api |
required | Panel base URL. Used in Panel API mode. |
api_key |
"" |
Application API key. Empty enables static node mode. |
[listen].bind |
"0.0.0.0" |
Address WSG listens on. |
[listen].port |
2022 |
Public SFTP port served by WSG. |
[ssh].host_key_path |
"data/host_key" |
SSH host key path. Generated on first start if missing. |
[ssh].max_auth_tries |
6 |
Maximum SSH auth attempts per connection. |
[ssh].auth_rejection_time_secs |
3 |
Delay before rejecting failed auth attempts. |
[node.<server_uuid_prefix>].sftp |
none | Backend Wings SFTP address for static mode. The table key should be the same 8-character suffix used in the login username. |
[cache].capacity |
5000 |
Panel lookup cache size. |
[cache].ttl_secs |
60 |
Panel lookup cache TTL in seconds. |
[logging].level |
"info" |
Log filter, for example info, debug, or wsg=debug. |
[rate_limit].connections_per_sec |
5.0 |
Per-IP connection rate limit. |
[rate_limit].auth_per_minute |
30 |
Per-IP and username auth attempt limit. |
[rate_limit].burst_size |
10 |
Token bucket burst size. |
- Persist
/app/dataor the configured host key path. If the host key changes, SFTP clients will see a host key warning. - Put WSG behind your normal firewall/security group. Only expose the SFTP port you intend users to access.
- Backend
sftpaddresses should be reachable from WSG before starting the service. - Public key login is passed through to Wings as SSH public key/none auth. Whether it works depends on your Wings setup.
- Shell, exec, and PTY requests are rejected. This gateway is for SFTP.
MIT