हमने जो भी per-IP control शिप किया, वो एक header से bypass हो जाता था
हमारा account-lockout counter failed logins को पाँच पर रोक देता था। हमारा rate limiter abusive clients को throttle करता था। दोनों client के IP address पर keyed थे, जो कि keying के लिए सबसे obvious चीज़ है। और दोनों पूरी तरह bypassable थे, bypass था एक अकेला HTTP header जो आप खुद set कर सकते थे।
वजह ये है। जब आपकी app किसी reverse proxy के पीछे चलती है, और हमारी nginx के पीछे Kubernetes पर चलती है, तो app को जो TCP connection दिखता है वो user से नहीं आता। वो proxy से आता है। हर request एक ही socket peer address के साथ पहुँचती है: ingress। इसलिए असली client IP को एक header में ले जाना पड़ता है, X-Forwarded-For, जिसे proxy set करता है और app पढ़ती है। ASP.NET Core में ठीक इसी काम के लिए middleware है: वो X-Forwarded-For पढ़ता है और connection का remote IP उसी से बदल देता है, ताकि आपका बाकी code, आपके rate limiter समेत, असली client देखे।
दिक्कत ये है कि X-Forwarded-For बस एक header है। कोई भी भेज सकता है। अगर app उस पर बिना शर्त भरोसा करती है, और हमारी करती थी, चाहे वो किसी भी source से आए, तो उसकी value attacker के control में है। तो attack कोई चालाकी नहीं है, बस एक line है:
curl -H "X-Forwarded-For: 10.0.0.1" https://.../login
curl -H "X-Forwarded-For: 10.0.0.2" https://.../login
curl -H "X-Forwarded-For: 10.0.0.3" https://.../login
हर request पर header बदल दीजिए और app को लगता है कि हर attempt किसी बिल्कुल नए client से आ रही है। उस IP पर keyed lockout counter किसी भी एक value के लिए कभी पाँच तक नहीं पहुँचता। rate limiter की bucket हर बार नई होती है। आप दिन भर password brute force कर सकते हैं, और हमारा हर per-IP defense खुशी-खुशी एक तक गिनता रहता है। जो control ऐसी value पर keyed हो जिसे attacker खुद set करता है, वो control है ही नहीं।
fix सुनने में मामूली लगता है: X-Forwarded-For पर सिर्फ तभी भरोसा करो जब वो आपके अपने proxy से आए। सिद्धांत में मामूली है और अमल में पेचीदा, और ये पेचीदगी Kubernetes में ही बसती है।
आप बस header पढ़ना बंद नहीं कर सकते। ऐसा करेंगे तो हर request ingress से आती दिखेगी, यानी अब आपके सारे clients एक ही IP share करेंगे और per-IP controls उल्टी दिशा में टूट जाएँगे: एक abuser सबके लिए lockout trip कर देगा। आपको X-Forwarded-For पढ़ना ही है, पर उस पर यकीन तभी करना है जब request सचमुच किसी ऐसे proxy से आई हो जिस पर आप भरोसा करते हैं।
ASP.NET का forwarded-headers middleware source के आधार पर भरोसा करता है: known proxy IPs की list, या CIDRs के रूप में दिए गए known proxy networks। naive तरीका है ingress pod का IP pin कर देना। AKS पर वो IP stable नहीं है। ingress pods reschedule और rescale होते रहते हैं, और उनके addresses उनके साथ बदलते रहते हैं, तो pinned IP बासी पड़ जाता है और या तो legitimate traffic तोड़ देता है या, अगर आपने कोई पुराना fallback छोड़ रखा है, तो वही छेद दोबारा खोल देता है। जो stable है वो है वो subnet जिससे pods अपने addresses लेते हैं। तो आप node-subnet CIDR पर भरोसा करते हैं, किसी एक address पर नहीं। हर वो hop जो legitimately header set कर सकता है उसी range में रहता है, और उसके बाहर किसी की बात नहीं मानी जाती।
फिर आती है गिनती। middleware X-Forwarded-For की list को दाएँ से चलता है, एक-एक trusted hop छीलते हुए, और trusted hops के बाद जो बचता है उसे client मान लिया जाता है। एक hop ज़्यादा चल गए तो आप अपने proxy से उतरकर header के उस हिस्से में जा पहुँचते हैं जो attacker ने लिखा था। इसलिए आप जितने hops छीलते हैं वो संख्या ठीक उतनी होनी चाहिए जितने proxies सचमुच उस header में entry जोड़ते हैं, बिल्कुल उतनी। हमारे path में एक है: nginx। उसके आगे खड़ा Azure load balancer layer 4 है। वो TCP forward करता है, HTTP parse नहीं करता, और कोई X-Forwarded-For entry नहीं जोड़ता। तो सही forward limit है एक। default नहीं, 'safe रहने के लिए थोड़े ज़्यादा' भी नहीं। एक, क्योंकि एक ही proxy उस header को छूता है।
यही जोड़ी, node subnet पर भरोसा और ठीक एक hop की छिलाई, पूरा fix है। middleware अब आपको जो client IP थमाता है वो वही value है जो nginx ने असली socket से लिखी थी, और attacker की inject की हुई entries उसके बाईं ओर पड़ी रह जाती हैं, नज़रअंदाज़, क्योंकि हम उन तक पहुँचने से पहले ही चलना बंद कर देते हैं।
transferable हिस्सा ये numbers नहीं हैं, बल्कि ये बात है कि वे आपकी specific topology के numbers हैं। X-Forwarded-For data नहीं है, trust delegation है: उसे पढ़ने का मतलब है 'जिसने भी इसे set किया, मैं उसकी बात मानता हूँ।' वो यकीन आपके request path के exact hops पर pinned होना चाहिए, न उससे चौड़ा न उससे संकरा। भरोसा ज़्यादा चौड़ा रखा तो spoofable है। ज़्यादा संकरा रखा तो आपके सारे clients proxy में सिमटकर एक हो जाते हैं। और गिनती आपके infrastructure से बंधी है, तो जिस दिन आपने सबके आगे एक CDN लगा दिया, आपने एक hop जोड़ दिया, और जो forward limit कल तक सही थी वो अब एक कम है। fix कोई जादुई constant नहीं है। fix ये है: अपने path के proxies गिनिए, उन्हीं पर और सिर्फ उन्हीं पर भरोसा कीजिए, और जब भी path बदले, दोबारा गिनिए।
proxy hops को सही गिनना बेरौनक काम है, गलत होना आसान है, और यही तय करता है कि आपके rate limits का कोई मतलब भी है या नहीं। Authagonal ये हिसाब-किताब आपके लिए रखता है, ताकि आपका lockout client को गिने, किसी header को नहीं।