Ana SayfaBlogCloudflare Tunnel ile Homelab Servislerini Disari Actim: 90 Gunluk Notlarim
Security & Compliance

Cloudflare Tunnel ile Homelab Servislerini Disari Actim: 90 Gunluk Notlarim

Emre Ferit Aslantas1 Haziran 202613 dkMakale
cloudflare-tunnel homelab zero-trust self-hosted reverse-proxy security
Sponsored

Mart ayinin sonunda evdeki Nginx + Let's Encrypt + DDNS kombinasyonumu Cloudflare Tunnel ile degistirdim. Karari aceleci aldim, sebep de basitti: ISS'im CGNAT'a gecmis ve port forwarding'im sessizce kirilmisti. Iki haftadir webhook'larim disaridan calismiyordu ve sebebini fark etmem 11 gun aldi. Sinir oldum, baska bir yol aradim, Cloudflare Tunnel'da karar kildim.

Bu yazi o 90 gunde ne yaptigimi, neyin calistigini, neyin can sikici oldugunu ve bugun bilseydim hangi karari farkli verecegimi anlatiyor. Birebir kopya bir tutorial degil — kendi 7 servisli setup'imin dogal halini paylasiyorum.

Neden Port Forwarding'i Birakmistim

Setup'im su sekildeydi: ev IP'm No-IP uzerinden bir DDNS adina baglaniyor, modem 80/443'u Proxmox uzerindeki Nginx LXC'sine forward ediyor, Nginx de Let's Encrypt sertifikalariyla 4-5 servis publish ediyordu. Calisiyordu, ama uc problem birikti:

  1. CGNAT kabusu — ISS musteri hizmetlerini iki kez aradiktan sonra public IP'mi geri vermediler. "Aboneliginiz fiber'a tasinmali" dediler, fiyatlandirmasi sacmaydi.
  2. Modem rebootlari — haftada 1-2 kez modem yeniden basliyordu, DDNS guncellemesi bazen 6 dakika gecikiyordu. Webhook receiver'larim icin bu yeterince uzun bir kapanma.
  3. 80/443'u disari acmak — fail2ban kuruluydu, ama Nginx access log'larinda gunde ~3000 abuse denemesi goruyordum. Cogu zararsiz tarayici, ama her log satiri bana "buyuk ihtimalle bir gun bir guvenlik aciginla bunlarin biri tutar" hissi veriyordu.

Cloudflare Tunnel bu uc problemi de teorik olarak cozuyor: dis IP'ye ihtiyac yok, port acmak yok, Cloudflare'in WAF'i onunde duruyor.

Setup: 90 Dakikadan Az

Daemon'i kurmak hizliydi. Mac Mini'mde brew uzerinden:

brew install cloudflared
cloudflared tunnel login
cloudflared tunnel create homelab-main

Tunnel credential dosyasi ~/.cloudflared/<tunnel-id>.json olarak duser. Ben bunu Vault'tan cikartip dogrudan LaunchAgent'imin secret dizinine kopyalamayi tercih ettim, cunku Mac Mini'mde zaten Vault kuruluydu.

Config dosyasi gercekten basit:

# ~/.cloudflared/config.yml
tunnel: 9f4a7b22-...-redacted
credentials-file: /Users/efa/.cloudflared/9f4a7b22-...-redacted.json

ingress:
  - hostname: dashboard.aslantas.dev
    service: http://localhost:3000
  - hostname: api.aslantas.dev
    service: http://localhost:8080
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
  - hostname: mcp.aslantas.dev
    service: http://192.168.1.106:7000
  - hostname: status.aslantas.dev
    service: http://192.168.1.106:3001
  - service: http_status:404

DNS kayitlarini elle eklemedim, cloudflared tunnel route dns homelab-main dashboard.aslantas.dev komutu Cloudflare DNS panelinde otomatik CNAME yaratiyor. Bu ergonomik ayrinti zamanla bana cok zaman kazandirdi — Terraform'u soktum, son 30 gunde 4 yeni servis ekleme islemini commit ile yaptim.

LaunchAgent kismi:

<!-- ~/Library/LaunchAgents/com.cloudflare.cloudflared.plist -->
<plist version="1.0">
<dict>
  <key>Label</key><string>com.cloudflare.cloudflared</string>
  <key>ProgramArguments</key>
  <array>
    <string>/opt/homebrew/bin/cloudflared</string>
    <string>tunnel</string>
    <string>run</string>
    <string>homelab-main</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>/Users/efa/.cloudflared/stdout.log</string>
  <key>StandardErrorPath</key><string>/Users/efa/.cloudflared/stderr.log</string>
</dict>
</plist>

launchctl load ... ve servis ayaktaydi. Toplam: 90 dakikanin altinda. Eski Nginx config'imi devre disi birakmak yaklasik 20 dakika daha aldi, cunku Let's Encrypt yenileme cron'unu temizleyip dokumantasyonumu guncellemem gerekti.

Latency: Iyi, Ama Kac ms Pahasina?

Gercek olcumlerimle bunu birakayim. Test sirasinda kullanilan endpoint: kendi efa-agent'imin /healthz endpoint'i, bos JSON donen 200 yanit. Mac Mini -> Cloudflare edge (FRA) -> client (Istanbul home, ayni evden, mobil 4G uzerinden test).

| Senaryo | Median (p50) | p95 | |---|---|---| | Eski setup (port forwarding + Nginx) | 28 ms | 95 ms | | Cloudflare Tunnel | 62 ms | 140 ms | | Cloudflare Tunnel (warm connection) | 48 ms | 110 ms |

Yani ~30-40 ms ekstra latency. Cogu uygulamam icin bu kabul edilebilir. Webhook'lar, dashboard'lar, RAG sorgulari icin fark hissedilmiyor. Ama yerel LLM streaming response'larinda ilk token'in 60ms gec gelmesi su yapilanmis terminal arayuzunde belirgindi.

Cloudflare'in tunnel daemon'i bir cold connection acmaya 200-300ms civari ekleyebiliyor; bu yuzden HTTP keep-alive ile devamli ayakta tutulan baglantilar p50'yi belirgin sekilde dusuruyor. SearXNG ve dashboard gibi static-heavy servislerde fark gozardi edilebilir.

WebSocket ve Server-Sent Events Hikayesi

Ilk hafta her sey gulluk gulistanlikti. Sonra efa-agent'imdaki streaming chat endpoint'imi (Server-Sent Events) test ettigimde sasirtici bir davranis gordum: yanitlar gerciyor, ama yaklasik 100 saniye sonra connection sessizce kopuyordu. Browser konsoldan "EventSource error" disinda bir sey gormuyordum.

Sebep: Cloudflare Free plan'da HTTP/2 idle timeout'u 100 saniye. SSE sirasinda data akiyor olsa bile, eger paketler arasinda 100s'den uzun bir bosluk varsa, Cloudflare baglantiyi kesiyor. LLM responselari uzun bir thinking phase varsa bunu kolayca asabiliyor.

Cozumum cirkin ama isliyor: agent backend'inde 30 saniyede bir SSE keep-alive ping atiyorum:

# agent/streaming.py (simplified)
async def stream_response(...):
    last_send = time.monotonic()
    async for chunk in llm_stream:
        yield f"data: {chunk}\n\n"
        last_send = time.monotonic()
        # heartbeat to keep Cloudflare connection alive
        if time.monotonic() - last_send > 30:
            yield ": keepalive\n\n"

Cloudflare dokumantasyonu bu davranisi gomulu bir bolumde yaziyor, ama production'a almadan once goremedim. Eger LLM streaming, log tail, ya da long-running webhook callback'leriniz varsa, keep-alive ping disiplini sart.

Cloudflare Access: Vaktin Yarisini Bunu Anlamaya Verdim

Tunnel'in en parlak kismi cloudflare access policy'leri. Public bir DNS adina sahibim ama "sadece kendi gmail adresimden ya da kendi GitHub hesabimdan giris" diye onune bir application koyabiliyorum.

# terraform/cloudflare_access.tf
resource "cloudflare_zero_trust_access_application" "mcp" {
  zone_id          = var.zone_id
  name             = "MCP Hub"
  domain           = "mcp.aslantas.dev"
  session_duration = "24h"
  type             = "self_hosted"
}

resource "cloudflare_zero_trust_access_policy" "mcp_me_only" {
  application_id = cloudflare_zero_trust_access_application.mcp.id
  zone_id        = var.zone_id
  name           = "Only me"
  decision       = "allow"
  precedence     = 1

  include {
    email = ["emreferitaslantas@gmail.com"]
  }
}

Bu policy aktif oldugunda mcp.aslantas.dev'e gittigimde Cloudflare beni bir login screen'e atiyor, Google OAuth ile dogrulayinca tunnel'in arkasindaki servise ulasiyorum. 24 saatlik session cookie. Hicbir sey servis tarafinda degismeden auth bir katman geride.

Ilk haftalarda bunu kullanmadim, "zaten kimse adresleri bilmiyor" dedim. Yanilmisim — crt.sh'ye girip kendi domainim icin sertifika seffafligi log'larina baktigimda butun subdomain'lerimi okuyabildim. Ondan sonra hassas servisleri (Vaultwarden, MCP Hub, agent dashboard) Access arkasina aldim. Yalniz public faydali olanlari (kisisel status sayfasi, public dokumantasyon) actim.

Kismen Basarisiz: Cloudflare Tunnel'da Birakmadigim Iki Servis

Her seyi Tunnel'a tasimadim. Iki istisnayi ozellikle anlatmak istiyorum cunku karar matrisi yararli olabilir.

1. Plex/Jellyfin: Hala Yok, Ama Olsa Da Tunnel Disinda Olurdu

Cloudflare Free plan'i mp4, mkv, vs. gibi non-HTML icerikleri proxy etmenizi yasakliyor. Aslinda yasaklamiyor, gizli bir 100MB upload limit ve "bandwidth-heavy non-website use" madde 2.8 vardi sartlarda. Bir media server'i Tunnel uzerinden yayinlamak Cloudflare'in TOS'una takilir. Bu yuzden kendi media setup'imi tamamen LAN icinde tuttum; gerektigi zaman Tailscale uzerinden erisiyorum. Cloudflare Tunnel + media = TOS riski.

2. SSH Erisimi: Tailscale'i Korudum

cloudflared access ssh ile SSH'i tunnel uzerinden expose edebiliyorsunuz. Denedim, calisti, ama hem konfigurasyon hem de client-side cloudflared baglantisi gunluk akisima ekstra friksiyon getirdi. Tailscale zaten kuruluydu ve ssh efa-mac calisiyordu. SSH gibi az kullanilan, kritik bir erisim icin sade WireGuard tunnel'ina guvenmek bence daha akilli — bir tane daha vendor dependency eklemek istemedim.

90 Gunluk Beklenmeyen Faydalar

Bunlari planlamamistim, kendiliginden geldi:

  • Daemon auto-restart: Mac Mini birkac kez (3 kez, manuel macOS update sonrasi) yeniden basti. cloudflared LaunchAgent her seferinde tunnel'i 8-12 saniye icinde geri ayaga kaldirdi. Uptime Kuma'da bu sureler "1m incident" olarak goruluyor, eskiden 6-10 dakikalik DDNS gecikmesinin yerini aldi.
  • Analytics free: Cloudflare panelinden son 30 gunde her hostname icin gercek request grafigini gorebiliyorum. mcp.aslantas.dev gunde ortalama ~1200 request aliyor, dashboard ~250. Bunu Nginx access loglarini parse ederek yapmak GoAccess kurmama gerek birakmazdi.
  • WAF kurallari: Cloudflare'in default managed rules'u son 90 gunde 47.000+ "suspicious" request'i benim sunucuma ulasmadan blokladi. Cogu zararsiz tarayici trafigi, ama 80/443'u kendi yapan ortamlarda fail2ban'in yapamayacagi seyleri free plan'da hediye olarak aliyorsunuz.

Sonuc: 3-5 Madde Net Cikti

  • Cloudflare Tunnel + Access ev sunuculari icin guvenlik ve operasyonel kolaylik acisindan port forwarding'i acik ara geride birakiyor. Static IP yokken ya da CGNAT arkasindaysaniz, baska secenek aramaya gerek yok.
  • Latency pahasi ~30-40 ms median, ~50 ms p95. Bircok kullanima uygun, ama gercek-zamanli (oyun, sesli iletisim, dusuk-latency LLM streaming) icin bu fark hissedilir.
  • SSE/WebSocket icin keep-alive sart. Cloudflare 100 saniye idle timeout uyguluyor, uzun streaming sirasinda heartbeat goz ardi edilemez.
  • Media server'lari koymayin — TOS'a takilir. Tailscale veya self-hosted reverse proxy daha temiz.
  • Terraform'la yonetmek 4. servisten itibaren kendini amorti ediyor. Manuel DNS + Access policy kurmak bir servis icin 2 dakika, 7 servis icin can sikici. cloudflare provider olgun, sorunsuz.

Bir sonraki yazimda muhtemelen Tailscale Funnel'i Cloudflare Tunnel ile yan yana koyup, hangisinin hangi sceneye yakistirdigimi anlatacagim. Su an icin: bu gecisi yaparken yasadigim "neden daha once yapmadim" anini gormussem, port forwarding'i baska bir homelab kuran arkadasima da onermem.

Sponsored

Haftalık DevOps Bülteni

Yeni tool incelemeleri, karşılaştırmalar ve DevOps trendleri haftada bir kutuna gelsin.