Skip to content

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

Diagram for creating an ESXi 8.0 bootable USB drive

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:cloudflared Native 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:

conditionDescription
Cloudflare accountThe domain you own is already hosted on Cloudflare.
A Linux virtual machineUsed for running cloudflared Daemon processes (e.g., Ubuntu 22.04 LTS)
Intranet service informationIP 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/hosts

2. 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 --version

3. Install NGINX ​

bash
sudo apt update
sudo apt install nginx -y

Part 1: NGINX-side configuration ​

1.1 Create the SSL configuration directory and upload the certificate ​

bash
sudo mkdir -p /etc/nginx/ssl

Use 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/default

1.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 login

After 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 homelab

After 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:404

ingress Parameter description ​

parametertypeRequiredDescription
hostnamestringYes (except for default rules)Matched external domain names. Supports exact matching or wildcards.
servicestringYesForwarding target address, in the format 协议://地址:端口. Support http://,https://,tcp://,unix:// or http_status:code.
originRequestobjectNoDefine the request behavior when forwarding.
originRequest.noTLSVerifyboolNo (suggestion true)Whether to skip verification of the backend service TLS certificate.
originRequest.httpHostHeaderstringNoin the override forwarding request Host Head.
originRequest.originServerNamestringNoSpecify the SNI of the backend server.
originRequest.tcpKeepAliveintNoSet 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.com

Design 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.

3.1 Start Nginx ​

bash
sudo systemctl start nginx
sudo systemctl enable nginx

3.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.com

3.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 %h

After 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 homelab

After debugging is complete, we need to manage cloudflared through systemd. cloudflared provides a service subcommand.

bash
cloudflared service help

The 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 install

The 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 successfully

It 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.target

4.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 cloudflared

Will 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 ​

CharacteristicsDescription
Subdomain convergenceWeb services uniformly use homelab.your-domain.com, differentiate services by path
Multiple SSH host supportThrough Nginx Stream + SNI routing, a single port supports unlimited SSH targets.
Unified access layerAll traffic is centralized through Nginx, with unified configuration
Highly scalableTo add a new service, you only need to add it in Nginx location block
Clear configurationEach 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 cloudflared

  • cloudflared Forward to Nginx

  • Nginx according to location /esxi/ Proxy to ESXi backend

SSH SNI routing:

  • SSH client connection dev.homelab.your-domain.com

  • cloudflared Forward TCP traffic to Nginx's 2222 port

  • Nginx through ssl_preread Read SNI, based on map Forward rules to the corresponding backend

Details to pay attention to ​

  • Trailing slash in path:proxy_pass A 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/hosts defined in homelab.local Points to the Nginx internal IP.

  • Stream module location: Stream configuration can only be placed in nginx.conf In 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.

Appendix: Related IP Address Reference

  • ESXi management address:https://192.168.0.201

  • vCenter management address:https://192.168.0.202

  • Nginx unified access layer:https://homelab.local(Homelab internal access)

  • SSH target host example:

  • Development machine:172.16.0.10:22

  • K8s master node:172.16.0.20:22

  • Storage node:172.16.0.30:22

最后更新2026/11/07 14:16
如果你觉得这篇文章有帮助,或者想聊聊技术、工作,欢迎通过下面方式联系我:
contact fishfinal