Skip to content

Security: howdeploy/Xrayebator

Security

docs/security.md

Security

← Back to README · Русский · 简体中文

Sections: Service account and permissions · Subscription security · SSH access to the VPS · Desktop GUI credentials


Service account and permissions

Xray runs as the system user xray. The drop-in /etc/systemd/system/xray.service.d/security.conf sets User=xray and narrows the capabilities to CAP_NET_BIND_SERVICE, which is enough for low ports. The manager state is deliberately outside the service account's write boundary.

The ownership model is:

Path or class Owner and access
/usr/local/etc/xray/ and its state directories Mostly root-owned and not service-writable; generated metadata such as .server_country may be xray-owned
config.json, profiles and migration markers Normally root:root and readable by Xray as required; a rollback helper may set the live config to root:xray with mode 0640
Reality and VLESS private key files root:root, mode 0600
public key files and generated metadata Readable where Xray or the subscription handler needs them; ownership can differ for generated metadata
/usr/local/etc/xray/scripts/ and /usr/local/bin/xrayebator* root:root, executable manager scripts protected from service-account replacement
/var/log/xray/ writable by the Xray runtime account for service logs

The service account can read the configuration it needs, but cannot replace executable manager scripts, markers or private keys. Most state/config files are root-protected, not universally immutable or necessarily root:root; do not treat .server_country as a private key or as a permission boundary. Backups are root-owned and kept separately from the live files. Do not put unrelated secrets in the Xray state directory.

Runtime configuration changes made by xrayebator use a backup, validated atomic write and the safe_restart_xray transaction. This scope is limited to runtime mutations owned by xrayebator. Install and update have separate validation, restart and rollback paths and are not guaranteed to be identical. After either lifecycle update, the operator should verify Xray, DNS and the subscription endpoint/service rather than assuming that a successful script exit proves every path is healthy.

Subscription security

The subscription URL is a bearer credential. It is not public, but anyone holding the full URL can download the route list and the token-protected subscription resources. The desktop GUI therefore masks the token (…) before the URL can appear in either console — the deployment log and the import wizard console; passwords and private-key bytes are never written there.

Already handled server-side:

  • a 32-character hexadecimal token generated by openssl rand -hex 16;
  • /sub/ without a valid token returns an identical 404;
  • a profile with no live routes returns 410 and serves no routes; a partially stale multi-route profile can still return its remaining live routes with 200;
  • an expired or disabled profile also returns 410 Gone (Profile expired or disabled) and serves no routes at all, even before the client refreshes its subscription;
  • nginx adds Cache-Control: no-store and rate-limits the subscription location;
  • the root path and paths outside /sub/ return 404;
  • Revoke rotates sub_token, so the old URL stops working; a full revocation (profile-revoke --full) also rotates the uuid in every inbound of the profile, so already-downloaded configurations stop connecting.

Left to the operator:

  • do not post the subscription_url in public chats or issue trackers;
  • press Revoke immediately after a leak;
  • treat every saved vless:// link as a credential too;
  • never hand a local-only URL to an external client;
  • do not host third-party panels or proxies on the same domain without understanding the nginx configuration.

Multi-protocol backend security

Optional backends (Hysteria 2, AmneziaWG) follow the same trust model as the Xray service, with per-backend refinements:

Path or class Owner and access
/usr/local/etc/xrayebator/backends.json root:root 644 — deliberately secret-free (ports, versions, flags only); the xray service account reads it from the subscription handler
/usr/local/etc/xrayebator/backends/hysteria/ root:hysteria 750; server.yaml and certificates 0640 root:hysteria — the service must read them, nothing else touches them
/usr/local/etc/xrayebator/backends/awg/ root:root 700; awg0.conf and server-params.json 0600 — server private keys and peer secrets
Hysteria placeholder credential _xrayebator_placeholder with a random password in a 0600 file; exists only so the auth map is never empty, not used by any issued client
certbot renewal deploy-hook /etc/letsencrypt/renewal-hooks/deploy/xrayebator-hysteria.sh, root-owned 0755; copies renewed certificates into the backend dir and restarts the backend
/etc/amnezia/amneziawg/awg0.conf a symlink to the managed file — the distro awg-quick@awg0 unit reads through it; no second copy of the config exists

Hysteria runs as the dedicated hysteria user with AmbientCapabilities=CAP_NET_BIND_SERVICE and NoNewPrivileges=true; the QUIC buffer sysctls live in /etc/sysctl.d/99-xrayebator-hysteria.conf. AmneziaWG requires net.ipv4.ip_forward=1 (its own sysctl file) and adds MASQUERADE/FORWARD rules through the interface config's PostUp/PostDown, scoped to the backend's subnet. Backend services are installed only on explicit operator action and removed with the corresponding *-uninstall command; uninstall.sh is not involved.

SSH access to the VPS

Xrayebator can be installed straight from root, but a dedicated user with narrowly scoped sudo is the better practice.

On the server:

adduser <username>
usermod -aG sudo <username>
su - <username>

On your own machine:

ssh-keygen -t ed25519 -C <your_email@example.com>
ssh-copy-id <username>@<server_ip>

Then log in as <username>@<server_ip>. Once key-based login works, disable passwords and optionally forbid root login:

sudo nano /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Losing the SSH keys means losing access to the server. Confirm that key-based login works before disabling passwords.

A keep-alive helps the SSH session survive VPN reconfiguration:

ClientAliveInterval 60
ClientAliveCountMax 120
TCPKeepAlive yes

Brute-force protection (fail2ban)

A public VPS with an open SSH port is scanned continuously. Measured on a live deployment (2026-10-23): 9 953 failed logins in 24 hours from 115 addresses — six of them in 109.160.32.0/24 with 1 197 attempts each — while root was allowed to authenticate by password.

sudo xrayebator ssh-protect --enable closes that gap without locking the operator out:

  • installs fail2ban and enables the sshd jail — journald backend, maxretry = 4 within findtime = 10m, bantime = 30m, plus bantime.increment (repeat offenders escalate up to a day); every knob sits inside the [sshd] section, so a pre-existing fail2ban setup with other jails keeps its own defaults;
  • writes /etc/ssh/sshd_config.d/99-xrayebator-hardening.conf with MaxAuthTries 3 and LoginGraceTime 30 — fewer attempts inside one connection and less time to complete them;
  • validates the drop-in with sshd -t before applying it and reloads ssh (not restart), so open sessions — including the one the GUI is working over — survive.

The address from which the install or deploy was run is exempt from bans for 24 hours: it is written into the ignoreip line together with # xrayebator-ignore-until: <epoch>, and xrayebator-ssh-ignoreip.timer (every 30 minutes, and 10 minutes after boot) calls ssh-protect --prune-ignore, which removes the entry once the term is over. A run of typos therefore cannot lock the operator — or the GUI, which connects from the same address — out for 30 minutes, while everyone else is still filtered. The exemption is reported by ssh-protect --status as ignore_ip / ignore_until; the loopback baseline ignoreip = 127.0.0.1/8 ::1 stays permanently. This is a command-line/JSON detail: the desktop GUI does not display it.

Password authentication is deliberately not disabled. The desktop app manages the server as root over an SSH password (authMethod: password); switching to key-only is a separate operator decision, and the path is described above in this file. If you do switch to keys, keep fail2ban: it costs nothing and removes the log noise.

The same operation is available as menu item 15, as the Security and TLS tab in the GUI, and automatically: install.sh and quickstart enable it on every deployment. On hosts without apt or systemd the command reports supported: false and changes nothing.

To release your own address after several typos:

sudo xrayebator ssh-protect --unban <IP>

Without SSH access the ban expires on its own (30 minutes, longer for repeat offenders), and the hosting provider's console always remains.

Desktop GUI credentials

The active Electron GUI supports SSH passwords and private keys, with direct-root or sudo execution. A private key selected through the native Electron file dialog is read by the main process and stored in the operating-system keychain via keytar (Windows Credential Manager, macOS Keychain, Linux Secret Service), so the same key can be reused for later operations and after app restarts without selecting the file again. When the OS keychain is unavailable there is deliberately no plaintext fallback: the key survives only in main-process memory for the current session and the UI warns that re-selection will be required after restart.

The SSH login password and a selected private key are persisted to the operating-system keychain after successful authentication — the password only after the first successful sign-in, the key when it is picked through the native file dialog — and are reused for later operations and after app restarts. When the OS keychain is unavailable there is deliberately no plaintext fallback: the key survives only in main-process memory for the current session and the UI warns that re-selection will be required after restart.

A distinct sudo password and an encrypted key's passphrase are never persisted; they are requested again in each session. Credentials and password values never cross the preload boundary — the renderer receives only non-secret credential ids and the display key name.

The GUI does persist the server metadata needed to return to a server, including the host, SSH port, username, authentication method, privilege mode, credential ids, display key name and installation diagnostics, plus preferences, the subscription_url, fetched vless:// links, the Hysteria 2 links, AWG client configs, the Clash/sing-box links, per-profile expiry dates and the SHA-256 SSH host-key pin — the latter four groups exist so the Keys page paints instantly instead of waiting for an SSH round-trip. The subscription URL, VLESS links, Hysteria 2 links and AWG configs are bearer credentials, so protect the local Electron application data and revoke the subscription (Server settings) if they leak; a full revocation rotates the client key as well, so already-downloaded configs stop working. Removing the last server card that references a credential deletes that keychain entry; entries shared with another card are kept.

Existing and imported servers can also be managed read-only: importing over SSH recognizes Xrayebator installations only and never reconfigures a server implicitly. An email is optional during deployment: without one, Certbot registers the ACME account with --register-unsafely-without-email, so renewal notices and account recovery are unavailable — both consequences are shown in the UI before deploying.

SSH host keys use trust on first successful authentication. The fingerprint is then pinned; any later mismatch fails closed before commands are executed. After an intentional VPS reinstall, explicitly reset the pin in Server Settings and confirm the new key on the next successful connection.

See Electron Desktop GUI for the complete Electron boundary, command adapters and packaging details.

There aren't any published security advisories