English
Homelab Ultimate Remote Access Architecture: Cloudflare Tunnel + Nginx Unified Access Layer (Multi-SSH Host Version)

Not just web services—even SSH can be accessed through a unified setup. With the Nginx Stream module + SNI routing, one tunnel and one port let you access any SSH host.
Why is this architecture needed?
If you have already tried Cloudflare Tunnel, you may encounter several very practical issues:
Subdomain proliferation: Every time a new service is added, a subdomain must be added, and DNS records are getting longer and longer.
Protocol restricted:
cloudflaredNative support is primarily for HTTP/HTTPS; TCP protocols such as SSH require additional handling.Multiple SSH Host Dilemma: When there are multiple machines in the Homelab that need to be accessed via SSH, a simple TCP tunnel solution cannot distinguish the target hosts.
The core goal of this architecture is to solve these problems:Through a unified Nginx access layer, implement intelligent routing and unified management for all internal network services (including multiple SSH hosts).
Ultimate Solution Overall Architecture
The core idea of this architecture is:Deploy an Nginx reverse proxy inside the Homelab as the single entry point for all internal services.cloudflared You only need to direct traffic to this Nginx, and Nginx will route the traffic to the specific backend service based on the request characteristics (domain name, path, and protocol).
text
Public network users
│
▼
Cloudflare edge nodes (HTTPS/TLS)
│ (encrypted tunnel)
▼
cloudflared (daemon in Homelab)
│ (Forward traffic to Nginx)
▼
Nginx Unified Access Layer (Core Routing)
│
├── HTTP/HTTPS routing (via location path)
│ ├── https://homelab-esxi.your-domain.com → ESXi (192.168.0.201:443)
│ ├── https://homelab-vcenter.your-domain.com → vCenter (192.168.0.202:443)
│ └── https://gitlab.your-domain.com → GitLab (172.16.0.10:80) [Independent subdomain]
│
└── TCP/SSH routing (via SNI)
├── dev.homelab.your-domain.com → development machine (172.16.0.10:22)
├── k8s.homelab.your-domain.com → K8s master node (172.16.0.20:22)
└── storage.homelab.your-domain.com → Storage node (172.16.0.30:22)💡 Note
Resolve the issue of adding /ui in ESXi and vCenter route rewriting; an independent second-level domain must be used.
Preparations
Before you begin, make sure you have the following conditions:
| condition | Description |
|---|---|
| Cloudflare account | The domain you own is already hosted on Cloudflare. |
| A Linux virtual machine | Used for running cloudflared Daemon processes (e.g., Ubuntu 22.04 LTS) |
| Intranet service information | IP addresses and ports for services such as ESXi and vCenter |
Pre-operation
Log in to your Linux virtual machine and complete the following operations.
1. Configure local HOST DNS resolution
View the static IP of a Linux virtual machine (for example 192.168.0.212), run the following command to write it into /etc/hosts file for use in the Nginx configuration file homelab.local Hostname:
bash
echo "192.168.0.212 homelab.local" | sudo tee -a /etc/hosts2. Installation cloudflared
bash
# Download and install cloudflared
wget -q https://github.com/cloudflare/cloudflared/releases/download/2026.7.2/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
# Verify installation
cloudflared --version3. Install NGINX
bash
sudo apt update
sudo apt install nginx -yPart 1: NGINX-side configuration
1.1 Create the SSL configuration directory and upload the certificate
bash
sudo mkdir -p /etc/nginx/sslUse the Cloudflare Origin CA certificate (.pem) and private key (.key) File uploaded to /etc/nginx/ssl/ Under the directory.
Certificate Description: In Cloudflare Dashboard → SSL/TLS → Origin Server, create a certificate, select "Let Cloudflare generate a private key and CSR", and it is recommended to set the validity period to the maximum (15 years). After downloading, simply upload it to the server.
1.2 Configure Nginx HTTP/HTTPS Routing (Web Service)
We use /etc/nginx/conf.d/ Directory, split configuration files by service. First, delete the default sites-enabled/default, then create a separate configuration file for each service.
bash
sudo rm -f /etc/nginx/sites-enabled/default1.2.1 Unified Entry Configuration:homelab.conf
nginx
# /etc/nginx/conf.d/homelab.conf
server {
listen 443 ssl http2;
server_name homelab.your-domain.com;
ssl_certificate /etc/nginx/ssl/your-domain.com.pem;
ssl_certificate_key /etc/nginx/ssl/your-domain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
access_log /var/log/nginx/homelab-access.log;
error_log /var/log/nginx/homelab-error.log;
# Cloudflare IP address ranges (used to obtain the real client IP)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
# Path routing: differentiate different services by location
# location /esxi/ {
# proxy_pass https://192.168.0.201:443/;
# proxy_ssl_verify off;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# location /vcenter/ {
# proxy_pass https://192.168.0.202:443/;
# proxy_ssl_verify off;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
location / {
return 404;
}
}1.2.2 ESXi Restricted Prefix Domain Configuration homelab-esxi.conf
nginx
# /etc/nginx/conf.d/homelab-esxi.conf
server {
listen 443 ssl http2;
server_name homelab-esxi.your-domain.com;
ssl_certificate /etc/nginx/ssl/your-domain.com.pem;
ssl_certificate_key /etc/nginx/ssl/your-domain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
access_log /var/log/nginx/esxi-access.log;
error_log /var/log/nginx/esxi-error.log;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
location / {
proxy_pass https://192.168.0.201;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}1.2.3 vCenter Qualified Prefix Domain Configuration homelab-vcenter.conf
nginx
# /etc/nginx/conf.d/homelab-vcenter.conf
server {
listen 443 ssl http2;
server_name homelab-vcenter.your-domain.com;
ssl_certificate /etc/nginx/ssl/your-domain.com.pem;
ssl_certificate_key /etc/nginx/ssl/your-domain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
access_log /var/log/nginx/vcenter-access.log;
error_log /var/log/nginx/vcenter-error.log;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
location / {
proxy_pass https://192.168.0.202;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}1.2.4 GitLab Dedicated Domain Configuration:gitlab.conf
nginx
# /etc/nginx/conf.d/gitlab.conf
server {
listen 443 ssl http2;
server_name gitlab.your-domain.com;
ssl_certificate /etc/nginx/ssl/your-domain.com.pem;
ssl_certificate_key /etc/nginx/ssl/your-domain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
access_log /var/log/nginx/gitlab-access.log;
error_log /var/log/nginx/gitlab-error.log;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
location / {
proxy_pass http://172.16.0.10:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}1.2.5 default.conf(Default 404 rule)
nginx
# /etc/nginx/conf.d/default.conf
server {
listen 443 ssl http2 default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/your-domain.com.pem;
ssl_certificate_key /etc/nginx/ssl/your-domain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
return 404;
}1.2.6 http-redirect.conf(HTTP forced redirect to HTTPS)
nginx
# /etc/nginx/conf.d/http-redirect.conf
server {
listen 80;
server_name homelab.your-domain.com gitlab.your-domain.com;
return 301 https://$host$request_uri;
}1.3 Configure Nginx TCP/SSH multi-host routing (core)
Edit /etc/nginx/nginx.conf, at events Add Stream configuration after the module:
nginx
stream {
map $ssl_preread_server_name $ssh_backend {
dev.homelab.your-domain.com 172.16.0.10:22;
k8s.homelab.your-domain.com 172.16.0.20:22;
storage.homelab.your-domain.com 172.16.0.30:22;
default 172.16.0.10:22;
}
server {
listen 2222;
proxy_pass $ssh_backend;
proxy_connect_timeout 1s;
proxy_timeout 3s;
ssl_preread on;
}
}After configuration is complete, execute sudo nginx -t Check whether the configuration syntax is correct, then execute sudo systemctl reload nginx Reload the configuration.
Part 2: cloudflared-side configuration
2.1 Authenticate and bind the domain name
bash
cloudflared tunnel loginAfter the command is executed, it will return an authorization link. Open the link in a browser, select your Cloudflare domain, and complete the authorization.
Prompt: After successful authorization,cloudflared will generate one in your virtual machine cert.pem Certificate file, used for subsequent identity verification.
2.2 Creating a Tunnel
bash
cloudflared tunnel create homelabAfter execution, it will generate a Tunnel UUID and a credentials file (such as <Tunnel UUID>.json), please keep it safe.
2.3 Configure Tunnel Routing Rules
Create ~/.cloudflared/config.yml Configuration file:
yaml
tunnel: <your Tunnel UUID>
credentials-file: /root/.cloudflared/<Tunnel UUID>.json
ingress:
- hostname: homelab.your-domain.com
service: https://homelab.local:443
originRequest:
noTLSVerify: true
httpHostHeader: homelab.your-domain.com
- hostname: homelab-esxi.your-domain.com
service: https://homelab.local:443
originRequest:
noTLSVerify: true
httpHostHeader: homelab-esxi.your-domain.com
- hostname: homelab-vcenter.your-domain.com
service: https://homelab.local:443
originRequest:
noTLSVerify: true
httpHostHeader: homelab-vcenter.your-domain.com
- hostname: gitlab.your-domain.com
service: https://homelab.local:443
originRequest:
noTLSVerify: true
httpHostHeader: gitlab.your-domain.com
- hostname: ssh.your-domain.com
service: tcp://homelab.local:2222
- service: http_status:404ingress Parameter description
| parameter | type | Required | Description |
|---|---|---|---|
hostname | string | Yes (except for default rules) | Matched external domain names. Supports exact matching or wildcards. |
service | string | Yes | Forwarding target address, in the format 协议://地址:端口. Support http://,https://,tcp://,unix:// or http_status:code. |
originRequest | object | No | Define the request behavior when forwarding. |
originRequest.noTLSVerify | bool | No (suggestion true) | Whether to skip verification of the backend service TLS certificate. |
originRequest.httpHostHeader | string | No | in the override forwarding request Host Head. |
originRequest.originServerName | string | No | Specify the SNI of the backend server. |
originRequest.tcpKeepAlive | int | No | Set the TCP Keep-Alive probe interval (seconds). |
2.4 Configure DNS Records
bash
cloudflared tunnel route dns homelab homelab.your-domain.com
cloudflared tunnel route dns homelab homelab-esxi.your-domain.com
cloudflared tunnel route dns homelab homelab-vcenter.your-domain.com
cloudflared tunnel route dns homelab gitlab.your-domain.com
cloudflared tunnel route dns homelab ssh.your-domain.comDesign Description:homelab.your-domain.com As the unified entry point for all Web services, through paths (such as /esxi,/vcenter) to distinguish backend services. GitLab and SSH retain separate subdomains due to protocol or management needs. This design preserves flexibility for special services while maintaining subdomain convergence.
Part 3: Load-related Services
3.1 Start Nginx
bash
sudo systemctl start nginx
sudo systemctl enable nginx3.2 Startup cloudflared tunnel
bash
cloudflared tunnel --loglevel=debug run homelab💡 Note
This method is the terminal mode used during testing. After testing is complete, you must manage the service by using the Cloudflare service; you can refer to it.
3.3 Verify HTTP/HTTPS access
bash
curl -k https://homelab-esxi.your-domain.com
curl -k https://homelab-vcenter.your-domain.com
curl -k https://gitlab.your-domain.com3.4 Verify SSH Multi-Host Access
bash
# Access the development machine
ssh -o ProxyCommand="cloudflared access ssh --hostname %h" [email protected]
# Access the K8s master node
ssh -o ProxyCommand="cloudflared access ssh --hostname %h" [email protected]
# Access storage node
ssh -o ProxyCommand="cloudflared access ssh --hostname %h" [email protected]If you feel you have to type it every time ProxyCommand It's very troublesome; can be at ~/.ssh/config Add configuration in:
ssh-config
Host *.homelab.your-domain.com
ProxyCommand cloudflared access ssh --hostname %hAfter that, you only need to execute ssh [email protected] That's all.
Part 4: Managing the cloudflared Service in a Production Environment
When testing intranet penetration, we can use the following commands for debugging.
bash
cloudflared tunnel --loglevel=debug run homelabAfter debugging is complete, we need to manage cloudflared through systemd. cloudflared provides a service subcommand.
bash
cloudflared service helpThe specific command information is as follows.
NAME:
cloudflared service - Manages the cloudflared system service
USAGE:
cloudflared service [global options] command [command options] [arguments...]
COMMANDS:
install Install cloudflared as a system service
uninstall Uninstall the cloudflared service
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help (default: false)4.1 Generate the cloudflared systemd service definition file
To generate the cloudflared systemd service definition file, you only need to run the following command.
bash
cloudflared service installThe specific execution logs are as follows
2026-07-22T17:09:05Z INF Using Systemd
2026-07-22T17:09:07Z INF Linux service for cloudflared installed successfullyIt will be at /etc/systemd/system Generate one named cloudflared.service service definition file
ini
[Unit]
Description=Cloudflare Tunnel client
After=network-online.target
Wants=network-online.target
[Service]
TimeoutStartSec=15
Type=notify
ExecStart=/usr/bin/cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target4.2 Verify the cloudflared service status
Execute in the previous step cloudflared service install At that time, the cloudflared package will generate a systemd service definition file and start cloudflared Service, we can run a command to verify.
bash
systemctl status cloudflaredWill output service status information similar to the following.
● cloudflared.service - Cloudflare Tunnel client
Loaded: loaded (/etc/systemd/system/cloudflared.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2026-07-22 17:09:07 UTC; 4min 34s ago
Main PID: 36946 (cloudflared)
Tasks: 10 (limit: 9383)
Memory: 15.7M
CPU: 1.438s
CGroup: /system.slice/cloudflared.service
└─36946 /usr/bin/cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF | SUMMARY: Environment is healthy. cloudflared will use 'quic' as p>
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF +------------------------------------------------------------------->
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF precheck component="DNS Resolution" details="DNS Resolved successful>
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF precheck component="DNS Resolution" details="DNS Resolved successful>
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF precheck component="UDP Connectivity" details="QUIC connection succe>
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF precheck component="UDP Connectivity" details="QUIC connection succe>
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF precheck component="TCP Connectivity" details="HTTP/2 connection suc>
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF precheck component="TCP Connectivity" details="HTTP/2 connection suc>
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF precheck component="Cloudflare API" details="API is reachable" run_i>
Jul 22 17:09:13 cloudflared cloudflared[36946]: 2026-07-22T17:09:13Z INF precheck complete hard_fail=false run_id=a55be9f4-82cb-4244-a61e-c3a>
lines 1-20/20 (END)Moreover, it has already been set to start automatically at boot. This way, even if your service restarts, the cloudflared service will be brought up by the systemd daemon—isn't that incredibly smooth! Now you can happily do intranet penetration. Are you happy?~
Configuration File Structure Overview
text
/etc/nginx/
├── conf.d/
│ ├── homelab.conf # Unified entry path routing (core)
│ ├── homelab-esxi.conf # exsi
│ ├── homelab-vcenter.conf # vcenter
│ ├── gitlab.conf # GitLab dedicated subdomain
│ ├── default.conf # Default 404 rule
│ └── http-redirect.conf # Force HTTP to HTTPS redirect
├── ssl/
│ ├── your-domain.com.pem
│ └── your-domain.com.key
└── nginx.conf # Main configuration file (including Stream module configuration)Summary of Architectural Advantages
| Characteristics | Description |
|---|---|
| Subdomain convergence | Web services uniformly use homelab.your-domain.com, differentiate services by path |
| Multiple SSH host support | Through Nginx Stream + SNI routing, a single port supports unlimited SSH targets. |
| Unified access layer | All traffic is centralized through Nginx, with unified configuration |
| Highly scalable | To add a new service, you only need to add it in Nginx location block |
| Clear configuration | Each service has an independent configuration file for easy maintenance. |
Brief Description of Working Principle
HTTP/HTTPS path routing:
User access
https://homelab.your-domain.com/esxi/Cloudflare forwards via tunnel to
cloudflaredcloudflaredForward to NginxNginx according to
location /esxi/Proxy to ESXi backend
SSH SNI routing:
SSH client connection
dev.homelab.your-domain.comcloudflaredForward TCP traffic to Nginx's2222portNginx through
ssl_prereadRead SNI, based onmapForward rules to the corresponding backend
Details to pay attention to
Trailing slash in path:
proxy_passA trailing slash in the URL will affect URL rewriting; please ensure the configuration is correct.Nginx SSL Certificate: Use a Cloudflare Origin CA certificate, upload it to
/etc/nginx/ssl/Table of contents.Cloudflare IP address ranges: Must remain consistent with Cloudflare's official IP list; regular updates are recommended.
Local DNS resolution:
/etc/hostsdefined inhomelab.localPoints to the Nginx internal IP.Stream module location: Stream configuration can only be placed in
nginx.confIn the main configuration file.
Conclusion
This architecture essentially builds, inside the Homelab, a Enterprise-grade reverse proxy access layer + TCP intelligent routing layer. It makes your intranet services no longer isolated, but instead provides external access through a unified entry point. When you add new services in the future, you only need to add one in the Nginx configuration location Rules: keep the entire process simple and controllable.
Related documents
First in the series:Homelab: From Zero to One Hands-On Series
Basic Chapter:Building a Homelab from Scratch (Part 1): Creating an ESXi 8.0 Bootable USB Drive in macOS
Operations Chapter:Homelab Abnormal Power Outage Repair Record: From Midnight Shutdown to vCenter Rebirth
Basic Remote Access:The Most Elegant Homelab Remote Access Solution: Cloudflare Tunnel Zero Trust Practice
Appendix: Related IP Address Reference
ESXi management address:
https://192.168.0.201vCenter management address:
https://192.168.0.202Nginx unified access layer:
https://homelab.local(Homelab internal access)SSH target host example:
Development machine:
172.16.0.10:22K8s master node:
172.16.0.20:22Storage node:
172.16.0.30:22
