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:
| Istilah | Arti |
|---|---|
| Tunnel | Objek logis di akun Cloudflare, punya nama dan UUID. Hostname publik diarahkan ke tunnel ini. |
| Connector | Satu proses cloudflared yang sedang terhubung atas nama tunnel tersebut. |
| Replica | Connector tambahan untuk tunnel yang sama, biasanya di host lain, demi redundansi. |
Apa yang Dilakukan cloudflared Saat Start
- Membaca kredensial tunnel (file JSON kredensial atau token).
- Membuka empat koneksi outbound ke empat server berbeda di minimal dua data center Cloudflare. Jadi satu connector saja sudah punya redundansi di sisi Cloudflare.
- Koneksi dibuat ke port 7844, memakai QUIC (UDP) secara default, dengan HTTP/2 (TCP) sebagai alternatif. Protokol bisa dipaksa dengan
--protocol quic|http2. - Setelah terdaftar, Cloudflare tahu bahwa trafik untuk tunnel itu bisa dikirim lewat koneksi-koneksi tersebut.
- 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
cloudflaredberjalan 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-managed | Remotely-managed | |
|---|---|---|
| Dibuat dengan | cloudflared tunnel create | Dashboard Zero Trust atau API |
| Konfigurasi ingress | File config.yml di server | Disimpan di Cloudflare |
| Kredensial di server | cert.pem + <UUID>.json | Satu token |
| Menjalankan | cloudflared tunnel run <nama> | cloudflared tunnel run --token <TOKEN> |
| Ubah rute | Edit file, restart | Dashboard/API, tanpa sentuh server |
| Cocok untuk | GitOps, config di repo, eksperimen CLI | Tim, banyak host, kelola dari satu tempat |
File-file pada mode locally-managed punya peran berbeda:
cert.pem: sertifikat hasilcloudflared 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. hostnamemendukung wildcard di depan (*.example.com), tapi tidak di tengah hostname.pathadalah 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,cloudflaredmenolak konfigurasinya.
Jenis service yang umum:
| Service | Kegunaan |
|---|---|
http://host:port, https://host:port | Aplikasi web |
ssh://host:22 | SSH (butuh cloudflared di sisi client) |
tcp://host:port | TCP generik, misal database |
rdp://host:3389 | Remote desktop |
http_status:404 | Balas status tetap, untuk catch-all |
hello_world | Halaman 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:
| Opsi | Default | Kapan dipakai |
|---|---|---|
noTLSVerify | false | Origin HTTPS dengan sertifikat self-signed (Proxmox, router, NAS) |
originServerName | kosong | Origin HTTPS yang sertifikatnya untuk nama tertentu (SNI) |
caPool | kosong | Lebih aman dari noTLSVerify: percayai CA internal tertentu |
httpHostHeader | kosong | Origin yang memilih virtual host berdasarkan header Host |
connectTimeout | 30s | Origin lambat menerima koneksi |
keepAliveTimeout | 1m30s | Durasi koneksi idle ke origin disimpan |
http2Origin | false | Origin yang mendukung HTTP/2 (misal gRPC) |
access | kosong | Wajibkan 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:
| Action | Fungsi |
|---|---|
| Allow | Izinkan pengguna yang memenuhi kriteria (setelah login) |
| Block | Tolak pengguna tertentu |
| Bypass | Matikan pemeriksaan Access (tanpa log; hindari untuk hal sensitif) |
| Service Auth | Autentikasi 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
| Gejala | Penyebab umum |
|---|---|
| Error 1033 di browser | Tunnel tidak punya connector aktif (service mati, kredensial salah, port 7844 diblokir) |
| Error 502 Bad Gateway | Connector hidup, tapi service origin tidak menjawab (salah port, service mati, localhost salah konteks di dalam Docker) |
| Proxmox/NAS error TLS | Origin HTTPS self-signed; pakai noTLSVerify atau caPool |
| Situs yang terbuka salah | Origin multi-vhost butuh httpHostHeader |
| Hostname malah 404 | DNS ada, tapi aturan ingress untuk hostname itu tidak ada atau urutannya kalah dengan aturan lain |
Edit config.yml tidak berefek | Tunnel remotely-managed, atau service belum di-restart, atau service membaca file config lain |
localhost tidak bisa diakses dari container cloudflared | Di dalam container, localhost adalah container itu sendiri; pakai nama service Docker atau IP host |
| Upload file besar gagal | Batas ukuran body request di plan Cloudflare tetap berlaku (100 MB di plan Free/Pro) |
| Request lama berakhir 524 | Batas waktu respons origin di proxy Cloudflare (100 detik secara default); pindahkan proses lama ke background job |
| Panel admin bisa dibuka siapa saja | Tunnel tanpa Cloudflare Access |
| Tunnel tidak bisa dihapus | Masih 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.