Wings implements multiple layers of security through filesystem permissions, user isolation, and container sandboxing. This page covers how Wings manages access control and security boundaries.
System User Configuration
Wings creates and manages a dedicated system user for running server processes.
Pterodactyl User
By default, Wings uses the pterodactyl user:
# config.yml
system:
username: pterodactyl
Configuration structure:
// config/config.go:145-174
type SystemConfiguration struct {
Username string `default:"pterodactyl" yaml:"username"`
User struct {
Rootless struct {
Enabled bool `yaml:"enabled" default:"false"`
ContainerUID int `yaml:"container_uid" default:"0"`
ContainerGID int `yaml:"container_gid" default:"0"`
} `yaml:"rootless"`
Uid int `yaml:"uid"`
Gid int `yaml:"gid"`
} `yaml:"user"`
}
Automatic User Creation
Wings automatically creates the pterodactyl user on startup:
// config/config.go:487-549
func EnsurePterodactylUser() error {
log.WithField("username", _config.System.Username).Info("checking for pterodactyl system user")
u, err := user.Lookup(_config.System.Username)
if err != nil {
if _, ok := err.(user.UnknownUserError); !ok {
return err
}
} else {
_config.System.User.Uid = system.MustInt(u.Uid)
_config.System.User.Gid = system.MustInt(u.Gid)
return nil
}
command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", _config.System.Username)
// Alpine Linux compatibility
if strings.HasPrefix(sysName, "alpine") {
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", _config.System.Username)
if _, err := exec.Command("addgroup", "-S", _config.System.Username).Output(); err != nil {
return err
}
}
split := strings.Split(command, " ")
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
return err
}
u, err = user.Lookup(_config.System.Username)
if err != nil {
return err
}
_config.System.User.Uid = system.MustInt(u.Uid)
_config.System.User.Gid = system.MustInt(u.Gid)
return nil
}
User Properties:
- System account:
--system flag
- No home directory: Prevents login
- No shell:
/usr/sbin/nologin for security
- Dedicated group: Same name as user
Manual User Configuration
You can use an existing user:
system:
username: myuser
user:
uid: 1001
gid: 1001
Filesystem Permissions
Wings automatically manages file ownership and permissions for server files.
Directory Structure
Wings creates directories with restricted permissions:
// config/config.go:629-686
func ConfigureDirectories() error {
root := _config.System.RootDirectory
if err := os.MkdirAll(root, 0o700); err != nil {
return err
}
if err := os.MkdirAll(_config.System.Data, 0o700); err != nil {
return err
}
if err := os.MkdirAll(_config.System.ArchiveDirectory, 0o700); err != nil {
return err
}
if err := os.MkdirAll(_config.System.BackupDirectory, 0o700); err != nil {
return err
}
return nil
}
Default Directories:
/var/lib/pterodactyl (0700) - Root directory
/var/lib/pterodactyl/volumes (0700) - Server data
/var/lib/pterodactyl/archives (0700) - Server transfers
/var/lib/pterodactyl/backups (0700) - Local backups
/tmp/pterodactyl (0700) - Temporary files
Automatic Chown
Wings automatically sets correct ownership on files:
// server/filesystem/filesystem.go:133-136
func (fs *Filesystem) chownFile(name string) error {
uid := config.Get().System.User.Uid
gid := config.Get().System.User.Gid
return fs.unixFS.Lchown(name, uid, gid)
}
Recursive chown for directories:
// server/filesystem/filesystem.go (Chown method)
func (fs *Filesystem) Chown(p string) error {
uid := config.Get().System.User.Uid
gid := config.Get().System.User.Gid
// Chown the initial path
if err := fs.unixFS.Lchownat(dirfd, name, uid, gid); err != nil {
return errors.Wrap(err, "server/filesystem: chown: failed to chown path")
}
// Walk and chown all files
return walkFn()
}
SFTP File Operations
SFTP operations automatically set ownership:
// sftp/handler.go:124-137
func (h *Handler) Filewrite(request *sftp.Request) (io.WriterAt, error) {
f, err := h.fs.Touch(request.Filepath, os.O_RDWR|os.O_TRUNC)
if err != nil {
return nil, sftp.ErrSSHFxFailure
}
// Chown may or may not have been called in touch, so always do it
_ = h.fs.Chown(request.Filepath)
return f, nil
}
See sftp/handler.go:139-257 for all SFTP operations.
Permission Checking
Wings can verify permissions on boot:
system:
check_permissions_on_boot: true # Default
Configuration:
// config/config.go:238
CheckPermissionsOnBoot bool `default:"true" yaml:"check_permissions_on_boot"`
Disabling permission checks improves boot performance but may cause permission issues if files are modified externally.
File Mode Defaults
- Files: 0644 (rw-r—r—)
- Directories: 0755 (rwxr-xr-x)
- SFTP forced directory mode: 0755
From sftp/handler.go:163-166:
mode := request.Attributes().FileMode().Perm()
if request.Attributes().FileMode().IsDir() {
mode = 0o755 // Force directories to 0755
}
Container Isolation
Wings runs each server in an isolated Docker container with security restrictions.
Container User Mapping
Containers run as the pterodactyl user:
// environment/docker/container.go:190-195
if cfg.System.User.Rootless.Enabled {
conf.User = fmt.Sprintf("%d:%d", cfg.System.User.Rootless.ContainerUID, cfg.System.User.Rootless.ContainerGID)
} else {
conf.User = strconv.Itoa(cfg.System.User.Uid) + ":" + strconv.Itoa(cfg.System.User.Gid)
}
Standard Mode:
- Container runs as
pterodactyl user (typically UID 988)
- Files are owned by
pterodactyl on host
- Processes inside container run as same UID
Rootless Mode:
- Container runs as current user
- No privileged operations required
- Better security isolation
User Namespace Remapping
Wings supports Docker user namespace remapping:
docker:
userns_mode: "" # Use daemon default
# userns_mode: "host" # Disable remapping for Wings
Configuration:
// config/config_docker.go:83-89
// Sets the user namespace mode for the container when user namespace remapping
// option is enabled. If the value is blank, the daemon's user namespace remapping
// configuration is used. If the value is "host", then the pterodactyl containers
// are started with user namespace remapping disabled.
UsernsMode string `default:"" json:"userns_mode" yaml:"userns_mode"`
Container Security Options
Process Limits
Prevent fork bombs:
docker:
container_pid_limit: 512 # Default
// config/config_docker.go:62-66
// ContainerPidLimit sets the total number of processes that can be active in
// a container at any given moment. This is a security concern in shared-hosting
// environments where a malicious process could create enough processes to cause
// the host node to run out of available pids and crash.
ContainerPidLimit int64 `default:"512" json:"container_pid_limit" yaml:"container_pid_limit"`
Network Isolation
Each container gets an isolated network:
docker:
network:
name: pterodactyl_nw
enable_icc: true # Inter-container communication
is_internal: false
Tmpfs Size Limits
docker:
tmpfs_size: 100 # MB
// config/config_docker.go:57-60
// TmpfsSize specifies the size for the /tmp directory mounted into containers.
// Docker utilizes the host's system memory for this value.
TmpfsSize uint `default:"100" json:"tmpfs_size" yaml:"tmpfs_size"`
Rootless Mode
Run Wings without root privileges:
system:
user:
rootless:
enabled: true
container_uid: 0 # Run as root inside container
container_gid: 0
Benefits:
- Wings runs as non-root user
- Better security isolation
- Reduced attack surface
Requirements:
- Docker configured for rootless mode
- Proper UID/GID mapping
- No privileged operations
See config/config.go:501-511 for rootless user handling.
SFTP Permissions
SFTP access is controlled through Panel-issued permissions.
Permission Model
// sftp/handler.go:20-26
const (
PermissionFileRead = "file.read"
PermissionFileReadContent = "file.read-content"
PermissionFileCreate = "file.create"
PermissionFileUpdate = "file.update"
PermissionFileDelete = "file.delete"
)
Permission Checking
// sftp/handler.go:289-303
func (h *Handler) can(permission string) bool {
if h.server.IsSuspended() {
return false
}
for _, p := range h.permissions {
// Match specific permission or wildcard
if p == permission || p == "*" {
return true
}
}
return false
}
Read-Only Mode
Force all SFTP connections to read-only:
system:
sftp:
read_only: true
Implementation in sftp/handler.go:94-96:
func (h *Handler) Filewrite(request *sftp.Request) (io.WriterAt, error) {
if h.ro {
return nil, sftp.ErrSSHFxOpUnsupported
}
// ...
}
SFTP Permission Examples
Read Files:
PermissionFileRead // List files
PermissionFileReadContent // Read file contents
Modify Files:
PermissionFileCreate // Create new files/directories
PermissionFileUpdate // Modify existing files, chmod, rename
PermissionFileDelete // Delete files/directories
Admin Access:
Container Passwd Files
Wings can generate custom passwd/group files for containers:
system:
passwd:
enabled: false # Default
directory: /run/wings/etc
Generated files:
// config/config.go:553-581
func ConfigurePasswd() error {
// Generate /etc/group
v := []byte(fmt.Sprintf(
`root:x:0:
container:x:%d:
nogroup:x:65534:`,
_config.System.User.Gid,
))
if err := os.WriteFile(filepath.Join(passwd.Directory, "group"), v, 0o644); err != nil {
return err
}
// Generate /etc/passwd
v = []byte(fmt.Sprintf(
`root:x:0:0::/root:/bin/sh
container:x:%d:%d::/home/container:/bin/sh
nobody:x:65534:65534::/var/empty:/bin/sh
`,
_config.System.User.Uid,
_config.System.User.Gid,
))
if err := os.WriteFile(filepath.Join(passwd.Directory, "passwd"), v, 0o644); err != nil {
return err
}
return nil
}
This is useful for applications that require specific passwd entries but should be used carefully as it’s mounted into all containers.
Security Best Practices
System User Security
- Never use root: Always run Wings as a dedicated user
- No login shell: Use
/usr/sbin/nologin or /bin/false
- System account: Use
--system flag when creating users
- Minimal permissions: Restrict UID/GID access to server files only
File Permission Hardening
- Restrict root directory:
sudo chmod 700 /var/lib/pterodactyl
sudo chown pterodactyl:pterodactyl /var/lib/pterodactyl
- Verify ownership:
find /var/lib/pterodactyl/volumes -type f ! -user pterodactyl
- Enable boot checks:
system:
check_permissions_on_boot: true
Container Isolation
- Enable PID limits:
docker:
container_pid_limit: 512
- Restrict network access:
docker:
network:
enable_icc: false # Disable inter-container communication
-
Consider rootless mode for enhanced security
-
Use user namespace remapping in Docker daemon
SFTP Hardening
- Enable read-only when appropriate:
system:
sftp:
read_only: true
-
Use key-based authentication instead of passwords
-
Limit SFTP permissions in Panel to minimum required
-
Monitor SFTP access via server activity logs
Directory Isolation
Wings uses path validation to prevent directory traversal:
- All file operations validate against server root
- Symlinks are restricted to server directory
- No access to host filesystem outside server root
See server/filesystem/ for path safety implementations.
Troubleshooting
Permission Denied Errors
Check file ownership:
ls -la /var/lib/pterodactyl/volumes/server-uuid/
Fix ownership:
sudo chown -R pterodactyl:pterodactyl /var/lib/pterodactyl/volumes/server-uuid/
Container User Mismatch
Verify Wings configuration:
grep -A 5 "user:" /etc/pterodactyl/config.yml
Check system user:
SFTP Access Denied
Check server suspension:
- Suspended servers deny all SFTP access
Verify permissions in Panel:
- User must have appropriate file permissions
Check read-only mode:
system:
sftp:
read_only: false # Allow writes