Reverse Proxy Arkasında Gerçek İstemci IP'si: X-Forwarded-For, Güvenilir Vekiller ve Sahte Başlıklar
Bir vekilin arkasındaki uygulama istemcinin adresini yalnızca başlıktan öğrenir. X-Forwarded-For'u sağdan okumak, güvenilir vekilleri adresle tanımlamak, nginx, Express ve Laravel ayarları, PROXY protokolü ve sahte başlıkla oran sınırını aşmanın yolları.
Uygulamanın önüne bir yük dengeleyici, CDN ya da reverse proxy koyduğunuz anda TCP bağlantısının karşı ucundaki adres artık ziyaretçinin değil, o vekilin adresidir. İstemcinin gerçek adresi uygulamaya ancak bir HTTP başlığıyla taşınır. Oran sınırlama, giriş denemesi kilidi, denetim kaydı, ülke bazlı kurallar ve IP izin listeleri hep bu değere bakar. Burada iki ayrı hata yapılır ve ikisi de ilk bakışta çalışıyor gibi görünür: ya herkes vekilin adresiyle görünür ve tek bir saldırganın tetiklediği kilit bütün kullanıcıları dışarıda bırakır, ya da uygulama istemcinin kendi yazdığı bir başlığa inanır ve saldırgan istediği adresle gelir.
Hangi başlık ne taşır
X-Forwarded-For fiilî standarttır. Değeri virgülle ayrılmış bir listedir ve her vekil, bağlantıyı kimden aldıysa o adresi listenin sonuna ekler. İstemci, iki vekil ve uygulamadan oluşan bir zincirde uygulamanın gördüğü başlık istemci, vekil1 olur; son vekilin adresi başlıkta değil, TCP bağlantısının kaynak adresindedir.
X-Real-IP tek bir adres taşır; nginx dünyasından gelen bir alışkanlıktır, standart değildir. RFC 7239'daki Forwarded başlığı aynı bilgiyi yapılandırılmış biçimde taşır (Forwarded: for=198.51.100.7;proto=https); IPv6 adresleri köşeli parantez ve tırnak içinde yazılır (for="[2001:db8::17]") ve onu destekleyen yazılım hâlâ azdır. CDN'lerin kendi başlıkları da vardır: Cloudflare CF-Connecting-IP, Fastly Fastly-Client-IP gönderir, Akamai tarafında True-Client-IP yaygındır. Bu başlıklar CDN kenarının yazdığı tek bir değer taşır, ama yalnızca isteğin gerçekten o CDN'den geldiği doğrulanmışsa bir anlamı vardır.
Dördüncü katmanda çalışan yük dengeleyicilerin eline HTTP başlığı geçmez. Orada PROXY protokolü kullanılır: dengeleyici TCP bağlantısının en başına istemci adresini içeren kısa bir başlık ekler, arka uç onu okuduktan sonra bağlantının geri kalanını normal işler.
Doğru okuma: sağdan sola
En soldaki değer istemcinin kontrolündedir. Herkes isteğine X-Forwarded-For: 203.0.113.66 ekleyebilir ve vekiller bu değeri silmeden arkasına kendi gördükleri adresi yazar. Bu yüzden başlık soldan değil, sağdan okunur:
- Bağlantının kaynak adresine bakın. Güvendiğiniz bir vekil değilse istemci odur ve başlık tamamen yok sayılır.
- Güvendiğiniz bir vekilse başlıktaki en sağdaki adrese geçin.
- O adres de güvenilir vekil listesindeyse bir sola kayın. Güvenilir olmayan ilk adres istemcidir.
Bir örnek: istemci 198.51.100.7 isteğine sahte bir X-Forwarded-For: 203.0.113.66 ekliyor. Vekil 192.0.2.10 bunu uygulamaya 203.0.113.66, 198.51.100.7 olarak iletiyor. Uygulama bağlantıyı 192.0.2.10'dan alıyor ve bu adres güvenilir; en sağdaki 198.51.100.7 güvenilir değil, demek ki istemci odur. İlk değeri alan bir kod 203.0.113.66 derdi ve her istekte başka bir adres yazan saldırgan oran sınırına hiç takılmazdı.
Güvenilir vekilleri sayıyla değil adresle tanımlayın. "Sağdan bir atlama geç" kuralı zincirde tam olarak bir vekil olduğu sürece doğrudur; önüne bir CDN eklendiği gün atlama sayısı değişir ve yapılandırmayı güncellemeyi kimse hatırlamaz. Atlama sayısı, biri zincirin bir halkasını atlayıp doğrudan içerideki dengeleyiciye bağlandığında da yanlış adrese güvenir. Adres listesi ise yanlış olduğunda daha gürültülü bozulur: herkes CDN'in adresiyle görünür ve bu hemen fark edilir.
Yapılandırma: kenar, ara katman, uygulama
İnternete bakan ilk vekilin istemciden gelen X-Forwarded-For değerini koruma zorunluluğu yoktur. Önünde başka bir vekil yoksa en sağlamı başlığı kendi gördüğü adresle ezmektir. nginx'te:
proxy_set_header X-Forwarded-For $remote_addr;
İç katmanlarda ise ekleme yapılır ($proxy_add_x_forwarded_for). nginx'in kendisi bir vekilin arkasındaysa istemci adresini realip modülü çözer:
set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
real_ip_recursive on olmadan nginx başlıktaki son adresi alır; açıkken sağdan sola yürür ve güvenilir listede olmayan ilk adresi seçer. Bundan sonra $remote_addr gerçek istemciyi gösterir; log satırları, limit_req ve allow/deny kuralları doğru adresle çalışır.
Uygulama çatıları da aynı kararı ister ve varsayılanları tehlikeli olabilir. Express'te app.set('trust proxy', true) başlıktaki en soldaki adresi istemci sayar, yani tam olarak sahteciliğe açık olan değeri. Bunun yerine adres verin:
app.set('trust proxy', ['loopback', '192.0.2.0/24'])
Laravel'de trustProxies(at: '*') "beni çağıran her zaman vekildir" anlamına gelir. Uygulama gerçekten tek bir vekilin arkasındaysa doğru sonucu verir; ama uygulamaya doğrudan ulaşılabiliyorsa çağıran istemcinin kendisidir ve başlığa ne yazdıysa olduğu gibi kabul edilir. Zincirde iki vekil varsa da istemci olarak dıştaki vekilin adresi döner. Adresleri açıkça yazın:
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: ['192.0.2.0/24']);
})
Hazır "gerçek IP" ara katmanlarını da okuyun: bazıları başlığa koşulsuz güvenip ilk değeri alır ve bunu belgelerinde tek bir uyarı cümlesiyle geçer.
Dördüncü katman: adresi hiç kaybetmemek
TCP seviyesinde çalışan bir dengeleyici (bulut sağlayıcıların ağ yük dengeleyicileri, HAProxy'nin mode tcp kipi) TLS'i açmadığı için başlık ekleyemez. PROXY protokolünün iki tarafta birden açılması gerekir; yalnız birinde açıksa arka uç bağlantının başındaki veriyi HTTP sanır ve her istek bozulur. HAProxy'de sunucu satırına send-proxy-v2, nginx'te listen 443 ssl proxy_protocol; ile birlikte real_ip_header proxy_protocol; eklenir.
Kubernetes'te LoadBalancer ya da NodePort tipindeki bir Service, varsayılan externalTrafficPolicy: Cluster ile trafiği başka bir düğüme aktarırken kaynak adresi düğümün adresiyle değiştirir (SNAT). İstemci adresini korumak için externalTrafficPolicy: Local kullanılır. Bedeli, trafiğin yalnızca o servisin podu bulunan düğümlere gitmesi ve yükün düğümler arasında eşit dağılmamasıdır.
Tuzaklar
Origin'e doğrudan erişim. Güvenilir liste CDN aralıklarını içeriyor ve kusursuz çalışıyor olabilir; ama sunucunuza CDN'i atlayarak doğrudan bağlanmak mümkünse WAF ve oran sınırları da atlanır. Güvenlik duvarında yalnızca CDN aralıklarına izin verin ya da kenar ile origin arasında karşılıklı TLS (Cloudflare'de Authenticated Origin Pulls) veya bir tünel kullanın.
Bayatlayan aralıklar. CDN'ler adres bloklarını yayımlar (Cloudflare için https://www.cloudflare.com/ips-v4 ve ips-v6) ve zaman zaman yenilerini ekler. Listeyi bir kez elle yazarsanız yeni kenarlardan gelen ziyaretçiler CDN'in adresiyle görünür. Bu bir güvenlik açığı değildir, ama oran sınırları ve kilitler o kenarın arkasındaki herkes için ortak olur. Listeyi zamanlanmış bir işle çekin, değiştiğinde alarm verin.
Fazla geniş güven. "Özel ağdan gelen her şey vekildir" kuralı pratiktir, ama aynı ağdaki her konteyner, her pod ve her yan servis artık istediği istemci adresini yazabilir. Kubernetes'te pod ağının tamamına güvenmek, kümedeki herhangi bir podun başlık sahteciliği yapabilmesi demektir. Güveni yalnızca gerçekten vekil olan adreslere verin.
IPv6 ve eşlenmiş adresler. Çift yığınlı dinleyen bir sunucu IPv4 istemcisini ::ffff:198.51.100.7 biçiminde görebilir ve düz IPv4 ile yazılmış bir güven listesiyle karşılaştırma tutmaz. Bazı dengeleyiciler adresin sonuna port ekler. Ayrıştırmadan önce normalleştirin. IPv6 istemcileri tek bir adres yerine çoğunlukla bütün bir /64 bloğu kullanır; oran sınırını tek adrese bağlamak, adresini her istekte değiştiren birine kapıyı açık bırakır.
Aynı başlık birden çok satırda. HTTP aynı adlı başlığın birden çok satırda gelmesine izin verir ve doğru okuma onları sırasıyla birleştirmektir. Yalnızca ilk satırı okuyan bir kod, saldırganın kendi eklediği satırı görür.
Yalnız adres değil. X-Forwarded-Proto ve X-Forwarded-Host de aynı güven kuralına tabidir. Uygulama parola sıfırlama bağlantısını X-Forwarded-Host üzerinden kuruyor ve bu başlık süzülmüyorsa, saldırgan bağlantısında kendi alan adı bulunan bir sıfırlama e-postası ürettirebilir.
Kayıt. Log satırına çözülmüş istemci adresinin yanında bağlantı adresini ve ham başlığı da yazın. Bir şey ters gittiğinde hangi katmanın neyi eklediğini ancak böyle görürsünüz.
Başlığa hiç bakmamanız gereken durumlar
Önünüzde vekil yoksa yönlendirme başlıklarını hiç okumayın; onları okuyan her kod satırı sahteciliğe açılan bir kapıdır. Ayrıca IP adresi zayıf bir kimliktir: operatörlerin ve kurumların ortak NAT'ı arkasında binlerce kişi aynı adresi paylaşır, VPN'ler ve konut vekilleri adresi dakikada bir değiştirir. Gerçek istemci adresini doğru çözmek oran sınırının ve denetim kaydının doğru çalışması için şarttır, ama kimlik doğrulamanın yerini tutmaz. IP izin listesini tek savunma olarak değil, ikinci bir katman olarak kullanın.
Kısa kontrol listesi
Zincirdeki her vekili adresiyle bilin ve uygulamaya yalnızca onlara güvenmesini söyleyin. Başlığı sağdan okuyun ve güvenilir olmayan ilk adreste durun. İnternete bakan ilk katman gelen değeri ezsin. Origin'e yalnızca vekillerinizden ulaşılabilsin, CDN aralıkları kendiliğinden güncellensin. Dördüncü katmanda PROXY protokolünü iki uçta birden açın, Kubernetes'te kaynak adresin nerede kaybolduğunu ölçün. Son olarak kendi sunucunuza sahte bir X-Forwarded-For ile istek atın ve logda hangi adresin göründüğüne bakın; o tek istek, yapılandırmanın doğru olup olmadığını herhangi bir belgeden daha hızlı söyler.