← Back to README · Русский · 简体中文
Sections: Service account and permissions · Subscription security · SSH access to the VPS · Desktop GUI credentials
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.
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 identical404;- a profile with no live routes returns
410and serves no routes; a partially stale multi-route profile can still return its remaining live routes with200; - 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-storeand rate-limits the subscription location; - the root path and paths outside
/sub/return404; Revokerotatessub_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_urlin public chats or issue trackers; - press
Revokeimmediately 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.
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.
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_configPermitRootLogin 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
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
fail2banand enables thesshdjail — journald backend,maxretry = 4withinfindtime = 10m,bantime = 30m, plusbantime.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.confwithMaxAuthTries 3andLoginGraceTime 30— fewer attempts inside one connection and less time to complete them; - validates the drop-in with
sshd -tbefore applying it and reloadsssh(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.
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.