Skip to content

Secure On-Premise Harbor Deployment with Cloudflare Tunnel and NGINX (Proxmox + VM)

This guide details how to deploy a self-hosted Harbor container registry inside a private VM and expose it securely through Cloudflare Tunnel using NGINX running on a Proxmox host.


Architecture Overview

Browser / Docker Client
        │  HTTPS (TLS)

Cloudflare Edge Network
        │  (secure tunnel)

cloudflared (on Proxmox host)
        │  forwards HTTPS → localhost:443

NGINX (on Proxmox)
        │  HTTPS termination (Origin CA)

Harbor VM (192.168.1.182:8030)
        HTTP backend

No public ports exposed — all traffic securely tunnels through Cloudflare.


1. Harbor VM Configuration

File: /opt/harbor/harbor/harbor.yml

yaml
hostname: 192.168.1.182
http:
  port: 8030
external_url: https://harbor-registry.cloudmateria.com

Rebuild Harbor configuration:

bash
cd /opt/harbor/harbor
sudo ./prepare
sudo docker-compose down -v
sudo docker-compose up -d

Test locally:

bash
curl http://192.168.1.182:8030

Should return Harbor HTML.


2. NGINX Reverse Proxy (Proxmox Host)

File: /etc/nginx/sites-enabled/harbor-registry.cloudmateria.com

nginx
server {
    listen 443 ssl;
    server_name harbor-registry.cloudmateria.com;

    ssl_certificate /etc/nginx/ssl/cloudmateria.pem;
    ssl_certificate_key /etc/nginx/ssl/cloudmateria.key;


    location / {
        proxy_pass http://192.168.1.182:8030;
        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 https;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }
}

Restart and validate:

bash
sudo nginx -t
sudo systemctl reload nginx

Test locally:

bash
curl -vk https://127.0.0.1 --resolve harbor-registry.cloudmateria.com:443:127.0.0.1

Should show Harbor HTML output.


3. Cloudflare Tunnel Configuration

File: /etc/cloudflared/config.yml

yaml
tunnel: harbor-tunnel
credentials-file: /root/.cloudflared/<UUID>.json

ingress:
  - hostname: harbor-registry.cloudmateria.com
    service: https://127.0.0.1:443
    originRequest:
      noTLSVerify: true
  - service: http_status:404

Restart Cloudflared:

bash
sudo systemctl restart cloudflared

Verify DNS route:

bash
cloudflared tunnel route dns harbor-tunnel harbor-registry.cloudmateria.com

This creates a CNAME in Cloudflare → <UUID>.cfargotunnel.com.


4. Verification Commands

TestCommandExpected
Local Harborcurl http://192.168.1.182:8030Harbor HTML
Local HTTPScurl https://127.0.0.1Harbor HTML
Public HTTPScurl https://harbor-registry.cloudmateria.comHarbor HTML
Browserhttps://harbor-registry.cloudmateria.comHarbor login page

5. Security Highlights

LayerEncryptionNotes
Browser → Cloudflare✅ Full HTTPSTLS 1.3 Edge encryption
Cloudflare → Proxmox (cloudflared)✅ HTTPS TunnelEncrypted via Argo Tunnel
NGINX → Harbor⚙️ HTTP (internal only)Can be upgraded to HTTPS via Origin CA

6. Optional: Enable End-to-End TLS (Cloudflare → Harbor)

If you prefer full encryption:

  1. Generate a Cloudflare Origin CA certificate.
  2. Copy cert/key to Harbor VM (/data/cert/).
  3. Edit Harbor’s config:
    yaml
    https:
      port: 443
      certificate: /data/cert/origin.crt
      private_key: /data/cert/origin.key
    
  4. Update Cloudflared config:
    yaml
    service: https://192.168.1.182:443
    originRequest:
      noTLSVerify: true
    

Rebuild Harbor and restart services.


7. Final Results

  • Secure public access at https://harbor-registry.cloudmateria.com
  • No exposed ports
  • Harbor fully functional behind Cloudflare
  • Supports Docker login, push, and K8s image pulls

Summary Table

ComponentRoleAddressProtocol
Harbor VMRegistry service192.168.1.182:8030HTTP
NGINXReverse proxy127.0.0.1:443HTTPS
CloudflaredSecure tunnel to Cloudflarelocalhost:443HTTPS
Public DomainExternal accessharbor-registry.cloudmateria.comHTTPS

Troubleshooting

SymptomCauseFix
502 Bad GatewayCloudflared → NGINX protocol mismatchSet service: https://127.0.0.1:443
400 Bad RequestSent HTTP to HTTPS portSame fix as above
./login redirectMissing external_url in harbor.ymlAdd full domain and rebuild Harbor
Wrong app loadsAnother NGINX default server takes precedenceUse listen 127.0.0.1:443 ssl http2;

End Result: You now have a private Harbor registry, securely accessible worldwide through Cloudflare, with no direct exposure to the internet — perfect for use with Kubernetes, CI/CD pipelines, or private image distribution.