Ekspos Layanan Self-Hosted dengan Aman via Cloudflare Tunnel

Foto oleh Chi-Hung Lin via Wikimedia Commons (CC BY-SA 3.0)
Tidak. Daemon cloudflared hanya membuat koneksi keluar ke edge Cloudflare, jadi router dan firewall Anda tetap tertutup penuh di sisi masuk. Inilah alasan ia bekerja di belakang carrier-grade NAT di mana port forwarding tidak mungkin, dan mengapa aman menjalankannya di VPS yang diperketat tanpa port publik yang terikat.
Tunnel menentukan bagaimana trafik mencapai layanan Anda — ia adalah transport yang membawa permintaan dari edge Cloudflare ke origin Anda tanpa port terbuka. Access menentukan siapa yang boleh lewat, menambahkan pemeriksaan login berbasis identitas di edge sebelum permintaan apa pun dirutekan ke tunnel. Anda sering memakai keduanya bersama: tunnel untuk konektivitas, Access untuk autentikasi.
Penyebab paling umum adalah aturan catch-all yang hilang. Daftar ingress Anda harus diakhiri dengan aturan yang tidak punya hostname, misalnya yang mengembalikan status HTTP 404. Cloudflare mengevaluasi aturan dari atas ke bawah dan mewajibkan fallback terakhir itu, jadi jika daemon tidak mau boot, pastikan entri terakhir tidak punya kunci hostname.
Bisa. Buat tunnel di dashboard Zero Trust dan ia memberi Anda token konektor yang sudah berisi identitas tunnel dan konfigurasi routing. Berikan token itu ke image resmi cloudflare/cloudflared melalui variabel environment TUNNEL_TOKEN dan jalankan perintah tunnel — tidak ada file konfigurasi atau JSON kredensial yang perlu di-mount.
Tidak sebagai aplikasi HTTP publik. Hostname publik Cloudflare Tunnel dibuat untuk layanan HTTP, dan database tidak boleh dipublikasikan seperti itu. Untuk akses database gunakan routing jaringan privat Cloudflare dengan klien WARP, atau simpan database di jaringan internal yang dijangkau tunnel hanya untuk kontainer aplikasi, jangan pernah sebagai rute publik.

Foto oleh Chi-Hung Lin via Wikimedia Commons (CC BY-SA 3.0)
Ringkasan Utama
Cloudflare Tunnel menjalankan daemon kecil bernama cloudflared yang menghubungi Cloudflare secara keluar, jadi Anda mengekspos layanan homelab atau VPS tanpa satu pun port masuk terbuka. Anda memetakan hostname ke layanan lokal di konfigurasi ingress, lalu menempatkan kebijakan Zero Trust Access di depan untuk mewajibkan login berbasis identitas sebelum siapa pun mencapai aplikasi.
Selama bertahun-tahun jawaban standar untuk mengakses server rumah adalah port forwarding: membuka lubang di router, mengekspos port 443, dan berharap konfigurasi reverse proxy serta TLS Anda tidak pernah bermasalah. Di koneksi rumahan Anda sering tidak bisa melakukannya sama sekali, karena ISP berada di belakang carrier-grade NAT dan tidak pernah memberi Anda IP publik yang bisa dirutekan. Di VPS Anda bisa membuka port, tetapi setiap port terbuka adalah layar login yang bisa digempur oleh seluruh internet.
Cloudflare Tunnel membalik arahnya. Alih-alih internet menghubungi Anda, sebuah agen ringan menghubungi edge Cloudflare secara keluar dan menjaga koneksi persisten tetap terbuka. Trafik untuk hostname Anda tiba di Cloudflare, dikirim melalui tunnel keluar yang sudah ada itu, dan baru kemudian mencapai layanan Anda. Firewall Anda tetap tertutup penuh di sisi masuk. Saya menjalankan ini di depan beberapa dashboard internal dan sebuah layanan di VPS saya, dan diam-diam ini menggantikan setiap setup reverse-proxy-plus-open-port yang dulu harus saya awasi.
Daemon-nya, cloudflared, membangun beberapa koneksi keluar ke pusat data Cloudflare terdekat melalui QUIC (UDP 7844) atau HTTPS. Karena ini koneksi keluar yang sudah diizinkan NAT dan firewall Anda, tidak ada yang perlu dibuka. Ketika permintaan mengenai hostname Anda, Cloudflare merutekannya ke dalam tunnel dan cloudflared mem-proxy-kannya ke alamat lokal seperti localhost pada suatu port. Layanan origin tidak pernah melihat internet publik secara langsung, dan port-nya tidak pernah terikat ke antarmuka publik.
Jalur klasik menggunakan CLI untuk autentikasi, membuat tunnel bernama, dan menghasilkan file kredensial. Autentikasi membuka browser sehingga Anda bisa memilih zona mana yang dimiliki tunnel. Membuat tunnel menulis file kredensial JSON yang namanya adalah UUID tunnel — perlakukan file itu seperti kata sandi.
# Install on Debian/Ubuntu (official Cloudflare repo)
sudo mkdir -p /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
https://pkg.cloudflare.com/cloudflared any main" \
| sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install -y cloudflared
# Authenticate (opens a browser to pick your zone)
cloudflared tunnel login
# Create a named tunnel -> writes ~/.cloudflared/<UUID>.json
cloudflared tunnel create homelab
# Route a DNS hostname to this tunnel
cloudflared tunnel route dns homelab grafana.example.comLebih baik gunakan metode berbasis token untuk kontainer dan setup cepat. Di dashboard bagian Zero Trust, buat tunnel dan ia memberi Anda satu token konektor — tanpa file kredensial yang harus dipindah-pindah, dan ingress rules bisa diedit di UI. Metode config-file CLI di bawah lebih baik jika Anda ingin aturan routing berada di version control.
Ketika Anda mengelola tunnel secara lokal, sebuah file konfigurasi memetakan setiap hostname publik ke layanan lokal. Cloudflare mengevaluasi ingress rules dari atas ke bawah dan berhenti pada kecocokan pertama, jadi urutan penting: taruh hostname spesifik lebih dulu dan wildcard luas terakhir. Aturan yang benar-benar sering menjebak orang adalah yang terakhir.
# ~/.cloudflared/config.yml
tunnel: 6ff42ae2-765d-4adf-8112-31c55c1551ef
credentials-file: /home/matthews/.cloudflared/6ff42ae2-...json
ingress:
# Specific hostnames first
- hostname: grafana.example.com
service: http://localhost:3000
- hostname: uptime.example.com
service: http://localhost:3001
# A whole app behind one subdomain tree
- hostname: "*.lab.example.com"
service: http://localhost:8080
# REQUIRED catch-all — must be last, no hostname
- service: http_status:404Aturan ingress terakhir harus berupa catch-all tanpa hostname, misalnya mengembalikan status HTTP 404. Tanpa itu cloudflared menolak untuk memulai dan mencetak error validasi. Ini menjebak hampir semua orang pada konfigurasi pertama mereka — jika daemon tidak mau boot, periksa apakah aturan terakhir tidak punya kunci hostname.
Setelah konfigurasi tervalidasi, jalankan sebagai service agar bertahan saat reboot. Di host systemd, cloudflared memasang unit service untuk Anda; di Docker, Anda mengarahkan kontainer ke routing yang sama tetapi memberikan token konektor alih-alih config yang di-mount.
Di VPS saya semuanya Docker Compose, jadi tunnel hanyalah satu service tambahan. Metode token paling bersih di sini — token konektor membawa identitas tunnel dan konfigurasi routing-nya, jadi kontainer tidak perlu apa pun yang di-mount. Simpan token di file environment, jangan pernah di-hardcode di file compose yang Anda commit.
# docker-compose.yml
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
# No ports published — that is the whole point.
networks:
- web
networks:
web:
external: true
# .env (git-ignored)
# TUNNEL_TOKEN=eyJhIjoiTOKEN...Taruh cloudflared di jaringan Docker yang sama dengan aplikasi yang di-proxy-nya, lalu arahkan ingress ke nama kontainer alih-alih localhost — misalnya http://grafana:3000. Kini tidak ada kontainer aplikasi Anda yang perlu port terpublikasi juga; tunnel menjangkaunya lewat jaringan Docker internal dan host tidak punya apa pun yang mendengarkan secara publik.
Tunnel saja tetap mempublikasikan layanan Anda ke siapa pun yang tahu URL-nya. Untuk apa pun yang tidak dimaksudkan sepenuhnya publik, lapisi dengan Cloudflare Access di atasnya. Access berada di edge dan memaksa setiap permintaan melewati pemeriksaan identitas sebelum pernah dirutekan ke tunnel Anda. Semua aplikasi Access bersifat deny by default — pengguna harus mencocokkan kebijakan Allow terlebih dulu — jadi orang asing tanpa autentikasi tidak pernah mencapai origin Anda sama sekali.
Di luar allow-list email sederhana, kebijakan Access dapat membatasi berdasarkan keanggotaan grup IdP, negara, postur perangkat, dan durasi sesi. Untuk layanan yang harus menerima panggilan programatik, tambahkan service token agar skrip bisa berautentikasi dengan sepasang header alih-alih login browser. Model mentalnya sederhana: tunnel menentukan bagaimana trafik mencapai mesin Anda, dan Access menentukan siapa yang boleh mengirimnya.
| Layanan | Pendekatan | Alasan |
|---|---|---|
| Dashboard internal (Grafana, uptime, Portainer) | Tunnel plus allow-list Access | Bersifat privat; gerbang identitas menjaganya tetap milik Anda. |
| Blog publik atau situs marketing | Tunnel saja, tanpa Access | Ditujukan untuk semua orang; Access akan memblokir pengunjung asli. |
| SSH ke host | Tunnel plus Access dengan sesi pendek | Menjangkau SSH tanpa port 22 terbuka ke dunia. |
| Port database (Postgres, Redis) | Rute jaringan privat, bukan hostname publik | Jangan pernah mengekspos DB sebagai aplikasi HTTP; gunakan WARP atau routing privat. |
| Apa pun yang menangani pembayaran atau PII | Access plus IdP ketat dan postur perangkat | Auth di edge adalah kontrol, bukan pengganti keamanan tingkat aplikasi. |
Trade-off yang layak disebut: Anda merutekan trafik melalui Cloudflare dan mempercayai edge mereka untuk menterminasi TLS. Untuk homelab dan sebagian besar proyek sampingan itu kesepakatan yang adil demi kenyamanan dan firewall yang tertutup. Jika Anda tidak bisa menerima pihak ketiga menterminasi TLS, alternatif self-hosted seperti VPN WireGuard atau reverse SSH tunnel biasa menjaga semuanya di tangan Anda dengan biaya setup lebih banyak dan tanpa filter edge. Bagi saya, di jalur rumahan di belakang CGNAT dan VPS yang ingin saya kunci, Cloudflare Tunnel plus Access adalah setup yang benar-benar terus saya jalankan.