September 27, 2026

Cloudflare Tunnel Lebih Dalam.

Cara kerja cloudflared di balik layar: koneksi outbound, connector dan replica, aturan ingress di config.yml, tunnel remotely-managed vs locally-managed, DNS route, menjalankan sebagai service, Cloudflare Access, SSH lewat tunnel, dan jebakan-jebakannya.

Di catatan Cloudflare Tunnel aku sudah menulis langkah praktisnya: login, create, route dns, run. Empat perintah itu cukup untuk membuat satu aplikasi localhost bisa diakses dari internet. Tapi begitu tunnel dipakai sungguhan, misalnya untuk home server dengan belasan service, pertanyaan yang muncul berubah: kenapa bisa jalan tanpa buka port? apa yang terjadi kalau server mati? bagaimana satu tunnel melayani banyak domain? bagaimana mengamankan panel admin?

Catatan ini mencoba menjawab pertanyaan-pertanyaan itu.

Masalah yang Diselesaikan

Cara klasik mengekspos service dari rumah:

Internet ──▶ IP publik router ──(port forward 443)──▶ server di LAN

Masalahnya:

  • Banyak ISP rumahan memakai CGNAT, jadi router tidak punya IP publik sendiri dan port forwarding mustahil.
  • IP publik rumahan biasanya dinamis.
  • Port yang terbuka terlihat oleh semua orang; bot pemindai akan menemukannya dalam hitungan jam.

Cloudflare Tunnel membalik arah koneksinya:

Browser ──▶ Cloudflare edge ◀══(koneksi outbound dari server)══ cloudflared ──▶ service lokal

Server tidak menerima koneksi masuk sama sekali. cloudflared yang membuka koneksi keluar ke Cloudflare, lalu request dari pengguna “dititipkan” lewat koneksi yang sudah terbuka itu. Router cukup mengizinkan trafik keluar, yang memang sudah diizinkan hampir semua jaringan.

Anatomi: Tunnel, Connector, Replica

Tiga istilah ini sering tertukar:

IstilahArti
TunnelObjek logis di akun Cloudflare, punya nama dan UUID. Hostname publik diarahkan ke tunnel ini.
ConnectorSatu proses cloudflared yang sedang terhubung atas nama tunnel tersebut.
ReplicaConnector tambahan untuk tunnel yang sama, biasanya di host lain, demi redundansi.

Apa yang Dilakukan cloudflared Saat Start

  1. Membaca kredensial tunnel (file JSON kredensial atau token).
  2. Membuka empat koneksi outbound ke empat server berbeda di minimal dua data center Cloudflare. Jadi satu connector saja sudah punya redundansi di sisi Cloudflare.
  3. Koneksi dibuat ke port 7844, memakai QUIC (UDP) secara default, dengan HTTP/2 (TCP) sebagai alternatif. Protokol bisa dipaksa dengan --protocol quic|http2.
  4. Setelah terdaftar, Cloudflare tahu bahwa trafik untuk tunnel itu bisa dikirim lewat koneksi-koneksi tersebut.
  5. Setiap request yang masuk dicocokkan dengan aturan ingress, lalu diteruskan ke service lokal yang sesuai.

Implikasi untuk firewall : yang perlu diizinkan hanya outbound TCP/UDP 7844 (ditambah 443 untuk fitur opsional seperti cek update). Tidak ada port inbound. Kalau jaringan kantor memblokir UDP, tunnel akan jatuh ke HTTP/2; kalau port 7844 diblokir total, tunnel tidak bisa tersambung sama sekali.

Replica dan High Availability

Satu connector sudah tahan terhadap gangguan data center Cloudflare, tapi tidak tahan terhadap host-nya sendiri mati. Untuk itu ada replica: jalankan cloudflared untuk tunnel yang sama di host lain.

  • Batasnya 25 replica (100 koneksi) per tunnel.
  • Replica bukan load balancer. Tidak ada round-robin atau hash. Request dikirim ke replica terdekat secara geografis dari data center yang menerima request, dan pindah ke replica lain kalau yang itu gagal.
  • Semua replica memakai konfigurasi rute yang sama, jadi semua host harus bisa menjangkau service yang dirujuk. Kalau ingress menunjuk http://localhost:8080, service itu harus ada di setiap host replica.
  • Kalau cloudflared berjalan sebagai service, hanya satu instance per host.

Cek connector yang aktif:

cloudflared tunnel info home-server

Di home lab satu laptop, replica sering tidak relevan. Tapi pola yang berguna: jalankan satu connector di host Proxmox dan satu lagi di VM terpisah, supaya me-restart salah satunya tidak memutus akses.

Locally-Managed vs Remotely-Managed

Ada dua cara menyimpan konfigurasi tunnel, dan ini sumber kebingungan terbesar.

Locally-managedRemotely-managed
Dibuat dengancloudflared tunnel createDashboard Zero Trust atau API
Konfigurasi ingressFile config.yml di serverDisimpan di Cloudflare
Kredensial di servercert.pem + <UUID>.jsonSatu token
Menjalankancloudflared tunnel run <nama>cloudflared tunnel run --token <TOKEN>
Ubah ruteEdit file, restartDashboard/API, tanpa sentuh server
Cocok untukGitOps, config di repo, eksperimen CLITim, banyak host, kelola dari satu tempat

File-file pada mode locally-managed punya peran berbeda:

  • cert.pem: sertifikat hasil cloudflared tunnel login. Memberi izin untuk mengelola tunnel di zona tersebut (create, delete, route dns). Tidak dibutuhkan hanya untuk menjalankan tunnel.
  • <UUID>.json: kredensial tunnel. Inilah “kunci” yang membuat connector boleh mengaku sebagai tunnel itu. Perlakukan seperti private key.
  • config.yml: aturan ingress dan opsi lain.

Untuk server produksi, prinsip least privilege berlaku: host yang hanya menjalankan tunnel cukup memegang file JSON kredensial (atau token), tidak perlu cert.pem.

Catatan penting: pada tunnel remotely-managed, aturan ingress datang dari Cloudflare. Mengedit config.yml lokal tidak mengubah rute. Ini sering bikin orang mengedit file berkali-kali tanpa hasil.

config.yml dan Aturan Ingress

Contoh konfigurasi untuk home lab dengan beberapa service:

tunnel: 00000000-0000-0000-0000-000000000000
credentials-file: /etc/cloudflared/00000000-0000-0000-0000-000000000000.json

ingress:
  # Web UI Proxmox: HTTPS self-signed di port 8006
  - hostname: pve.example.com
    service: https://localhost:8006
    originRequest:
      noTLSVerify: true

  # Aplikasi di VM Docker lain di LAN
  - hostname: app.example.com
    service: http://10.10.0.21:3000

  # API dengan path terpisah ke service berbeda
  - hostname: example.com
    path: ^/api/
    service: http://10.10.0.22:8080
  - hostname: example.com
    service: http://10.10.0.22:80

  # SSH (dipakai bersama Cloudflare Access, lihat di bawah)
  - hostname: ssh.example.com
    service: ssh://localhost:22

  # Wildcard untuk preview environment
  - hostname: "*.preview.example.com"
    service: http://10.10.0.30:80

  # Catch-all WAJIB ada di akhir
  - service: http_status:404

Cara Pencocokan

  • Aturan dievaluasi dari atas ke bawah, dan aturan pertama yang cocok yang dipakai. Urutan penting: aturan spesifik (path: ^/api/) harus di atas aturan umum untuk hostname yang sama.
  • hostname mendukung wildcard di depan (*.example.com), tapi tidak di tengah hostname.
  • path adalah regex (sintaks Go). ^/api/ cocok dengan /api/users; tanpa ^, pola akan cocok di posisi mana pun.
  • Aturan terakhir harus catch-all (tanpa hostname/path). Tanpa itu, cloudflared menolak konfigurasinya.

Jenis service yang umum:

ServiceKegunaan
http://host:port, https://host:portAplikasi web
ssh://host:22SSH (butuh cloudflared di sisi client)
tcp://host:portTCP generik, misal database
rdp://host:3389Remote desktop
http_status:404Balas status tetap, untuk catch-all
hello_worldHalaman uji bawaan

Karena cloudflared bertindak sebagai reverse proxy, service bisa berada di mana saja yang terjangkau dari host tersebut: localhost, IP VM lain di bridge Proxmox, atau nama container di network Docker yang sama.

Validasi Sebelum Restart

Dua perintah yang menghemat banyak waktu:

# Cek sintaks dan struktur
cloudflared tunnel ingress validate

# Uji URL mana yang akan cocok dengan aturan mana
cloudflared tunnel ingress rule https://example.com/api/users

originRequest: Menyetel Koneksi ke Origin

originRequest bisa diletakkan di level atas (berlaku global) atau per aturan (menimpa yang global). Opsi yang paling sering dibutuhkan:

OpsiDefaultKapan dipakai
noTLSVerifyfalseOrigin HTTPS dengan sertifikat self-signed (Proxmox, router, NAS)
originServerNamekosongOrigin HTTPS yang sertifikatnya untuk nama tertentu (SNI)
caPoolkosongLebih aman dari noTLSVerify: percayai CA internal tertentu
httpHostHeaderkosongOrigin yang memilih virtual host berdasarkan header Host
connectTimeout30sOrigin lambat menerima koneksi
keepAliveTimeout1m30sDurasi koneksi idle ke origin disimpan
http2OriginfalseOrigin yang mendukung HTTP/2 (misal gRPC)
accesskosongWajibkan dan validasi JWT Cloudflare Access sebelum proxy

httpHostHeader penting untuk kasus seperti Nginx/Apache/Laravel Herd yang melayani banyak situs di satu port: tanpa header Host yang benar, yang terbuka adalah situs default.

DNS Route: Bagaimana Domain Menemukan Tunnel

cloudflared tunnel route dns <tunnel> app.example.com sebenarnya hanya membuat satu CNAME yang di-proxy:

app.example.com  CNAME  <UUID>.cfargotunnel.com   (proxied, awan oranye)

Domain <UUID>.cfargotunnel.com hanya bermakna di dalam jaringan Cloudflare dan tidak me-resolve ke IP publik untuk dunia luar. Konsekuensinya:

  • Record harus proxied (awan oranye). Kalau DNS-only, tidak akan berfungsi.
  • Karena hanya CNAME, kamu bisa membuatnya lewat dashboard atau API DNS biasa, tidak harus lewat cloudflared.
  • Punya DNS record tidak otomatis membuat trafik diterima: tetap harus ada aturan ingress untuk hostname itu. Kalau tidak ada, request jatuh ke catch-all (biasanya 404).
  • Menghapus tunnel tidak menghapus CNAME-nya. Record yatim ini perlu dibersihkan manual, seperti di catatan fundamental.

Satu hal yang perlu diwaspadai: route dns membuat record di zona yang terkait dengan cert.pem hasil login. Kalau kamu punya beberapa domain dan login dengan zona yang salah, record bisa tercipta di zona yang tidak diharapkan (misalnya menjadi app.example.com.zonalain.com). Selalu cek hasilnya di dashboard DNS, atau buat CNAME-nya langsung lewat dashboard/API.

Menjalankan sebagai Service

cloudflared tunnel run di terminal akan mati saat terminal ditutup atau server reboot. Untuk pemakaian permanen, pasang sebagai service sistem .

Locally-managed (Linux, systemd):

# Konfigurasi minimal wajib berisi `tunnel` dan `credentials-file`
sudo cloudflared --config /home/<user>/.cloudflared/config.yml service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

Perhatikan jebakan sudo: saat memakai sudo, $HOME menunjuk ke /root, sehingga cloudflared tidak menemukan config di home user. Karena itu path config diberikan eksplisit dengan --config. Setiap kali config.yml diubah, service perlu di-restart (systemctl restart cloudflared). Kalau ragu file config mana yang dipakai service, lihat unit-nya dengan systemctl cat cloudflared.

Remotely-managed:

sudo cloudflared service install <TUNNEL_TOKEN>

Token disematkan ke definisi service. Perlakukan token ini seperti password: siapa pun yang memegangnya bisa menjalankan connector untuk tunnel itu.

macOS (misal untuk Mac mini atau laptop dev) memakai launchd lewat cloudflared service install yang sama. Mac Service Admin punya halaman khusus untuk mengelola tunnel dari browser: login, membuat dan menghapus tunnel, route DNS, dan mengedit aturan ingress, jadi tidak perlu mengingat semua perintah di atas.

Mengelola Lewat API

Tunnel remotely-managed bisa diatur penuh lewat API Cloudflare dengan API token berizin Cloudflare Tunnel: Edit dan DNS: Edit:

# Membuat tunnel (config disimpan di Cloudflare)
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel" \
  --request POST \
  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  --json '{"name": "home-server", "config_src": "cloudflare"}'

# Mengganti aturan ingress
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID/configurations" \
  --request PUT \
  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  --json '{"config": {"ingress": [
      {"hostname": "app.example.com", "service": "http://localhost:8001"},
      {"service": "http_status:404"}
  ]}}'

Setelah itu tinggal buat CNAME ke <tunnel_id>.cfargotunnel.com (proxied). Pola ini cocok untuk panel yang ingin mengekspos VM baru secara otomatis, misalnya sebagai langkah terakhir provisioning di catatan Proxmox VE API & Otomasi VM .

Cloudflare Access: Tunnel Bukan Autentikasi

Ini poin keamanan paling penting: tunnel hanya menyembunyikan origin, tidak membatasi siapa yang boleh masuk. Begitu pve.example.com terhubung ke tunnel, siapa pun di internet bisa membuka halaman login Proxmox itu. Keamanannya kembali bergantung pada password aplikasi.

Untuk panel admin, pasang Cloudflare Access (bagian dari Zero Trust) di depannya. Access berdiri di edge Cloudflare: pengguna harus lolos kebijakan (login lewat email OTP, Google, GitHub, dll.) sebelum request diteruskan ke tunnel.

Struktur Policy

Setiap policy punya action dan rule:

ActionFungsi
AllowIzinkan pengguna yang memenuhi kriteria (setelah login)
BlockTolak pengguna tertentu
BypassMatikan pemeriksaan Access (tanpa log; hindari untuk hal sensitif)
Service AuthAutentikasi non-manusia: service token atau mTLS, tanpa login IdP

Urutan evaluasinya: Service Auth → Bypass → Allow → Block, dan begitu pengguna cocok dengan Allow atau Block, evaluasi berhenti.

Rule menggabungkan kriteria dengan logika:

  • Include: OR. Cocok salah satu sudah cukup.
  • Require: AND. Semua harus terpenuhi.
  • Exclude: NOT. Kalau cocok, dikeluarkan.

Contoh policy untuk panel Proxmox:

Aplikasi : pve.example.com
Policy   : Allow
  Include : Emails → [email protected]
  Require : Country → Indonesia

Kesalahan umum: memakai Include: Everyone dengan harapan login saja sudah cukup. Itu berarti siapa pun yang bisa login lewat metode apa pun (termasuk OTP ke email sembarang) diizinkan.

Service Token untuk Mesin

Untuk skrip atau CI yang perlu mengakses aplikasi di balik Access, buat service token dan policy Service Auth. Client mengirim dua header:

CF-Access-Client-Id: <client-id>.access
CF-Access-Client-Secret: <client-secret>

Validasi JWT di Origin

Setelah lolos Access, Cloudflare menyertakan JWT di header Cf-Access-Jwt-Assertion (dan cookie CF_Authorization di browser). Origin yang paranoid bisa memvalidasi JWT ini, sehingga request yang entah bagaimana melewati jalur lain tetap ditolak. Cara termudah adalah opsi access di originRequest:

- hostname: pve.example.com
  service: https://localhost:8006
  originRequest:
    noTLSVerify: true
    access:
      required: true
      teamName: nama-tim-zero-trust
      audTag:
        - <AUD-tag-aplikasi>

Dengan ini cloudflared sendiri menolak request tanpa JWT Access yang valid sebelum diteruskan ke Proxmox.

SSH Lewat Tunnel

SSH bukan HTTP, jadi browser tidak bisa langsung memakainya. Ada dua pendekatan.

1. cloudflared di sisi client. Server punya aturan ingress ssh://localhost:22, dan client memakai cloudflared sebagai ProxyCommand:

# ~/.ssh/config
Host ssh.example.com
  ProxyCommand /opt/homebrew/bin/cloudflared access ssh --hostname %h

(Path cloudflared berbeda per OS; di macOS cek dengan brew --prefix cloudflared.)

Lalu cukup ssh [email protected]. Kalau hostname dilindungi Access, browser akan terbuka untuk login terlebih dahulu, baru koneksi SSH diteruskan. Autentikasi SSH (key) tetap berlaku setelahnya, jadi ada dua lapis.

2. Browser-rendered SSH. Access bisa me-render terminal SSH langsung di browser untuk aplikasi self-hosted, berguna saat berada di perangkat yang tidak bisa memasang cloudflared.

Pola yang sama berlaku untuk TCP lain: service: tcp://localhost:5432 di server, lalu cloudflared access tcp --hostname db.example.com --url localhost:5432 di client untuk membuat listener lokal.

Pitfall yang Sering Ditemui

GejalaPenyebab umum
Error 1033 di browserTunnel tidak punya connector aktif (service mati, kredensial salah, port 7844 diblokir)
Error 502 Bad GatewayConnector hidup, tapi service origin tidak menjawab (salah port, service mati, localhost salah konteks di dalam Docker)
Proxmox/NAS error TLSOrigin HTTPS self-signed; pakai noTLSVerify atau caPool
Situs yang terbuka salahOrigin multi-vhost butuh httpHostHeader
Hostname malah 404DNS ada, tapi aturan ingress untuk hostname itu tidak ada atau urutannya kalah dengan aturan lain
Edit config.yml tidak berefekTunnel remotely-managed, atau service belum di-restart, atau service membaca file config lain
localhost tidak bisa diakses dari container cloudflaredDi dalam container, localhost adalah container itu sendiri; pakai nama service Docker atau IP host
Upload file besar gagalBatas ukuran body request di plan Cloudflare tetap berlaku (100 MB di plan Free/Pro)
Request lama berakhir 524Batas waktu respons origin di proxy Cloudflare (100 detik secara default); pindahkan proses lama ke background job
Panel admin bisa dibuka siapa sajaTunnel tanpa Cloudflare Access
Tunnel tidak bisa dihapusMasih ada connector aktif; hentikan dulu atau pakai cloudflared tunnel cleanup

Satu catatan dari home lab -ku: sebelum menyalahkan Cloudflare, uji dulu service dari host tempat cloudflared berjalan (curl -v http://10.10.0.21:3000). Sebagian besar masalah “tunnel error” ternyata masalah jaringan antara cloudflared dan origin, bukan antara Cloudflare dan cloudflared.

Penutup

Kalau disederhanakan, Cloudflare Tunnel adalah reverse proxy yang koneksinya dibalik: cloudflared menelepon keluar, lalu Cloudflare memakai sambungan itu untuk mengirim request masuk. Dari model itu, hampir semua perilakunya jadi masuk akal: kenapa tidak butuh port terbuka, kenapa 1033 berarti connector mati, kenapa DNS cukup berupa CNAME, dan kenapa aturan ingress bekerja seperti virtual host di Nginx.

Yang tidak boleh dilupakan: menyembunyikan origin bukan sama dengan mengamankannya. Pasangkan tunnel dengan Cloudflare Access untuk apa pun yang bersifat admin.

Referensi:

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments