VLESS + TLS: a basic working config
Sometimes you don't need stealth but honest TLS on your own domain — behind a CDN, or where compatibility matters more than masquerading as someone else's site. Below is a working config, cert issuance, and an honest talk about where this variant gets spotted. Fill in your domain and email in the constructor above, copy the rest.
This material is about engineering your own infrastructure and is educational in nature. Complying with the laws of your own jurisdiction is your responsibility.
Why honest TLS at all, if Reality exists
I'll say it up front, no illusions: as Russian-grade stealth it's weaker than Reality. Your SNI is visible in the clear, and it can be cut off by a domain list. Reality is more resilient here, and if a node lives under harsh DPI — use it.
But honest TLS has its own niche, which is why I keep it around. It plays nice with CDNs — behind Cloudflare and similar fronts you need exactly a genuine certificate on your own domain; donor-style Reality won't ride in there. It can fall back to a real site: whoever pokes it without the right client lands on a normal page, not on emptiness. And it's plainly simple — no x25519 keys, no shortId, no donor. People take it for compatibility, not for masquerade.
Step 1 — issue the certificate
The domain must point via an A record to the node's IP. Port 80 must be free during issuance (Let's Encrypt's HTTP-01 validation knocks on it).
apt -y install certbot
certbot certonly --standalone -d your-domain.com -m admin@your-domain.com --agree-tos --non-interactiveThe cert will land in /etc/letsencrypt/live/your-domain.com/ — there's fullchain.pem and privkey.pem, and those go into the config.
A gotcha from the field: on Russian hosting, HTTP-01 sometimes times out ("Timeout after connect"). This is often transient — just repeat the command once or twice; usually it goes through on the second try. If it consistently fails, check that the domain is on the "grey cloud" (DNS only, not behind the Cloudflare proxy) and that :80 is open in the firewall, or switch to DNS-01 without port 80.
Step 2 — the full config profile
This is the complete Xray profile for the panel: log / inbounds / outbounds / routing. Paste it whole; the constructor will substitute the values:
{
"log": { "loglevel": "none" },
"inbounds": [
{
"tag": "vless-tls",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [],
"decryption": "none",
"fallbacks": [{ "dest": "8080" }]
},
"sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] },
"streamSettings": {
"network": "raw",
"security": "tls",
"tlsSettings": {
"serverName": "your-domain.com",
"minVersion": "1.2",
"alpn": ["h2", "http/1.1"],
"certificates": [
{
"certificateFile": "/etc/letsencrypt/live/your-domain.com/fullchain.pem",
"keyFile": "/etc/letsencrypt/live/your-domain.com/privkey.pem"
}
]
}
}
}
],
"outbounds": [
{ "tag": "DIRECT", "protocol": "freedom" },
{ "tag": "BLOCK", "protocol": "blackhole" }
],
"routing": { "rules": [
{ "ip": ["geoip:private"], "outboundTag": "BLOCK" },
{ "domain": ["geosite:category-ads-all"], "outboundTag": "BLOCK" },
{ "protocol": ["bittorrent"], "outboundTag": "BLOCK" }
]}
}Breakdown of the key parts:
network: raw— this is TCP (in newer Xray builds the transport is calledraw; the old nametcpis also valid).clients: []don't touch it — the panel will populate the users from squads itself.fallbacks → dest 8080— all "stray" traffic without a proper client goes to a real site or an nginx listening on:8080. Poke it with a browser and you'll see a normal page, not "something on 443". Keep a plausible site there.alpn: ["h2", "http/1.1"]— align it with what the CDN serves if you put it behind a front.- routing cuts private networks, ads, and torrents — the last one is mandatory, otherwise you'll get an abuse notice from the host.
Step 3 — settings on the Host
The config itself contains no clients — those come from the panel. Connection settings are set on the host:
Address : your-domain.com
Port : 443
SNI / Host : your-domain.com
ALPN : h2, http/1.1
flow : (empty)
fingerprint : firefoxNote: flow is empty. Vision is a Reality-TCP feature; there's none of it here. And set fingerprint to firefox, not chrome — chrome is a giveaway and in places knocks the node over.
Gotchas worth knowing in advance
- SNI is visible. Your domain shows up in the ClientHello in plaintext. Under harsh Russian DPI it can be cut off by a list. If it starts getting cut off — move to Reality/Selfsteal; that's precisely the problem Reality solves.
- The cert lives 90 days. Put
certbot renewin cron and be sure to restart the node after renewal — otherwise Xray keeps serving the old cert from memory, and clients drop off silently. - Behind a CDN people usually take something else. Honest TLS sits behind a CDN, but for fronting XHTTP is usually handier — it's designed for HTTP transport and finicky CDNs. That's covered in the XHTTP articles.
The mechanics of why an open SNI is a weakness, and how Reality gets around it, I covered in the Reality theory piece. Here's the working minimum for the case where you need exactly honest TLS.
Next guide XHTTP transport: when and why → ↗ Article unclear or something off? Message me and I will help or fix it. @notrealvpn →