- Shell 53.7%
- Nix 46.3%
A small NixOS flake for a personal censorship-circumvention proxy on a Linode Nanode: Xray VLESS+Reality on 443/8443/2053, BBR, zram swap, and sops-nix for service secrets. Host-specific details (IP, pinned SSH host key, SSH key) live in an admin-only sops file, and the scripts read them from there, so the repo carries no addresses or key material in the clear. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| hosts/nixproxy | ||
| scripts | ||
| secrets | ||
| .gitignore | ||
| .sops.yaml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
linode-nix-vless-proxy
A small NixOS (26.05) VPS on Akamai/Linode running an Xray VLESS+Reality server, as a personal censorship-circumvention proxy. It's meant to be cheap (a 1 GB Nanode, $5/mo), reproducible from one flake, and safe to keep in a public repo: every secret and every host-specific detail is encrypted with sops + age.
Linode has no first-party NixOS image, so the install goes through a custom installer disk booted in rescue mode, following Akamai's "Install and Configure NixOS on a Linode" guide with adjusted sizes.
Layout
| Path | What it is |
|---|---|
flake.nix, hosts/nixproxy/ |
The NixOS config. xray.nix is the proxy. |
secrets/xray.yaml |
UUID, Reality private key, short id. Encrypted to you and the server (sops-nix decrypts it on the host). |
secrets/server.yaml |
Server IP, pinned SSH host key, SSH private key. Encrypted to you only; the server can't read it. |
scripts/ |
Provisioning, ssh, client config and a connectivity test. They read secrets/server.yaml, so nothing host-specific is hard-coded. |
client/ |
Generated client material. Gitignored. |
Disk layout on a Nanode (25600 MB):
| Disk | Size | Role |
|---|---|---|
| installer | 1792 MB | NixOS minimal ISO written with dd |
| swap | 512 MB | swap |
| nixos | 23296 MB | root filesystem |
Two config profiles: installer (direct-disk, root /dev/sdc) and boot (GRUB 2, root
/dev/sda), with all Linode boot helpers disabled.
Setting up your own
You need sops, age, linode-cli (with a token) and ssh. Nix isn't needed locally.
-
Make an age key if you don't have one:
age-keygen -o ~/Library/Application\ Support/sops/age/keys.txt(on Linux:~/.config/sops/age/keys.txt). Put the public key in.sops.yamlasadmin. -
Run
scripts/01-provision.sh, then create the two profiles it describes, boot the installer and install the flake. Take the server's age key from its SSH host key (ssh-keyscan <server-ip> | ssh-to-age) and add it to.sops.yamlasnixproxy. -
Create the secrets (
sops secrets/xray.yaml,sops secrets/server.yaml):# secrets/xray.yaml (xray uuid / xray x25519 / openssl rand -hex 8) xray: uuid: ... private_key: ... short_id: ... # secrets/server.yaml server: ip: <server-ip> known_hosts: "<server-ip> ssh-ed25519 AAAA..." ssh_key: | -----BEGIN OPENSSH PRIVATE KEY----- ... -
Put the matching public key in
hosts/nixproxy/configuration.nixunderopenssh.authorizedKeys.keys, and change thedougusername if you like. -
scripts/ssh.shnow connects with no further setup.scripts/client-config.shbuilds the client JSON and avless://share link from the files inclient/(uuid.txt,reality-public-key.txt,short-id.txt).
Findings: port 443 is filtered from some China networks
From one China WiFi, nc to the server on :443 timed out 5/5 while :22 connected 5/5, and a packet
capture on the server showed the :443 SYNs never arrived (one connection that did arrive stalled
partway through the server's TLS flight). The server side was clean: no cloud firewall, host
firewall open, no rate limits. So the drop is on the path, and it's specific to the port.
Fix: the same Reality inbound also listens on 8443 and 2053 (ports in hosts/nixproxy/xray.nix).
Over :8443 a speedtest through the tunnel gave about 10 Mbit/s down, 7.8 up, 256-328 ms.
That said, it's not a clean fix. On :8443 the tunnel runs fine for 10-20 minutes, then slows to a crawl and eventually passes nothing. Reconnecting sometimes helps, and switching ports helped once. This looks like per-flow throttling on the path rather than a server problem, but it isn't confirmed yet. Xray also warns that non-443 Reality ports may draw extra attention, so once one port is confirmed stable, drop the others.
Clients must use the alternate port: edit the share link's port before importing.
Deploying changes
The flake lives on the server at ~/nixproxy (no local nix). Sync only the files you changed,
since the server's flake.lock is the source of truth (it carries sops-nix); pull it back with
rsync after a build. Then:
scripts/ssh.sh 'cd nixproxy && sudo nixos-rebuild switch --flake .#nixproxy'
scripts/ssh.sh 'sudo systemctl restart xray'
The explicit restart is needed when only the rendered secret JSON changes, because the unit itself
is unchanged and NixOS won't restart it. nixos-rebuild build first is a safe way to check a
change evaluates.
Limitations and things to revisit
- Encrypted secrets in a public repo are permanent. Anyone can copy
secrets/*.yamland the history forever, so their safety rests entirely on your age private key. Back it up, and if it ever leaks, rotate the Xray UUID/keys and the SSH key, not just the age key. - The IP isn't a secret, only less casual. It's in every client's share link and visible to anyone on the path. Keeping it out of the repo just stops drive-by scanning from the README.
- 1 GB is tight for rebuilds. The running system uses ~280 MB, but the live installer and flake evaluation together thrashed a 1 GB machine during the original install. If a rebuild hangs, add a swapfile or resize up for the rebuild (billing is hourly).
- Consider a domain for the client address so a blocked IP can be rerolled by changing one A record. Never use your own domain as the Reality SNI/dest.
- The SSH key was a bootstrap key. Once you pick a permanent one, remove the bootstrap key from the
config and drop
security.sudo.wheelNeedsPassword = falsewith it.