← All posts

वह एडमिशन वेबहुक जिसने हाँ कहा और फिर भी हमें ठप कर दिया

Authagonal·July 27, 2026

हम एक ऑथ सेवा चलाते हैं, इसलिए यह सवाल "जो कंटेनर अभी शुरू हुआ है क्या वह सचमुच वही कंटेनर है जो हमने बनाया था?" कोई अकादमिक सवाल नहीं है। हमारा जवाब था हस्ताक्षर: CI में हर इमेज पर हस्ताक्षर करो, डिप्लॉय से पहले सत्यापित करो, और फिर, एक दूसरी परत के रूप में, क्लस्टर में एक वेबहुक लगाओ ताकि हाथ से टाइप किया गया kubectl भी कुछ बिना-हस्ताक्षर वाला न चला सके। यह एक मंगलवार दोपहर को लगाया गया। शाम तक dev क्लस्टर पर हमारे ऑथ Deployment ने 2,242 ReplicaSet जमा कर लिए थे, लगभग हर तीन सेकंड में एक नया बना रहा था, Available=False रिपोर्ट कर रहा था, और कुछ भी सर्व नहीं कर रहा था।

स्पष्ट परिकल्पना यह है कि नया वेबहुक हमारी इमेज को अस्वीकार कर रहा था। ऐसा नहीं था। इसके लॉग, हर अनुरोध के लिए, allowed: true कह रहे थे। इसने वह सब कुछ एडमिट किया जो हमने उसे भेजा, पूरी शाम भर, जबकि जिस Deployment को यह एडमिट कर रहा था वह बिखर रहा था।

जो परत हम जोड़ रहे थे

पाइपलाइन पहले से ही हस्ताक्षर और सत्यापन करती थी। हर इमेज को बिल्ड-टाइम पर एक कीलेस हस्ताक्षर मिलता है, जो GitHub Actions OIDC पहचान से बंधा होता है, और डिप्लॉय जॉब उस पहचान के विरुद्ध सत्यापन चलाता है, इससे पहले कि कुछ भी क्लस्टर तक पहुँचे। वह गेट fail-closed है और वही असली नियंत्रण है।

क्लस्टर-साइड परत गहराई में रक्षा है: sigstore का policy-controller, जो एक एडमिशन वेबहुक के रूप में चलता है, दो पॉलिसियाँ रखते हुए। एक कहती है कि हमारे अपने रजिस्ट्री पथ से मेल खाने वाली इमेज पर हमारी वर्कफ़्लो पहचान से एक कीलेस हस्ताक्षर होना चाहिए। दूसरी एक कैच-ऑल है जो बाकी सब कुछ पास होने देती है, क्योंकि "कोई पॉलिसी मेल नहीं खाई" का मतलब है अस्वीकार, और कैच-ऑल के बिना क्लस्टर अपने Vault एजेंट, अपने CSI ड्राइवर, और हर वह sidecar खो देता है जिसे उसने नहीं बनाया। इंस्टॉल करो, नेमस्पेस को लेबल करो, हो गया। लेबल ही स्विच है: कोई लेबल नहीं, कोई प्रवर्तन नहीं।

पहली आउटेज, जो उबाऊ थी

इसे failurePolicy: Fail और चार्ट के डिफ़ॉल्ट दस सेकंड टाइमआउट के साथ चालू करने पर dev लगभग तुरंत ठप हो गया, ठीक उसी तरह जैसे हर कोई उम्मीद करता है कि एक एडमिशन वेबहुक आपको ठप कर देगा। एक ठंडे कंट्रोलर को किसी पहले न देखे गए हस्ताक्षर को सत्यापित करने के लिए Fulcio और Rekor तक पहुँचना पड़ता है। ठंडे होने पर, वह काम दस सेकंड में पूरा नहीं होता। Fail-closed और चूकी हुई समय-सीमा का मतलब है कि पॉड निर्माण अस्वीकार कर दिया जाता है, जिसका मतलब है कि रोलआउट पॉड्स को रख नहीं सकता, जिसका मतलब है कि सेवा के पास कोई रेप्लिका नहीं है।

वह फेल्योर मोड अच्छी तरह प्रलेखित है और समाधान भी वही प्रलेखित वाला है: वेबहुक टाइमआउट को तीस सेकंड तक बढ़ाओ और failurePolicy: Ignore सेट करो। Ignore हार मान लेने जैसा लगता है, और एकल-परत डिज़ाइन में यह होता भी। हमारे में पाइपलाइन गेट fail-closed प्राथमिक है, और क्लस्टर परत उसे पकड़ने के लिए मौजूद है जो पाइपलाइन से कभी गुज़रा ही नहीं। एक ऐसा वेबहुक जो क्लस्टर को कभी ठप नहीं कर सकता, हमारे लिए उस वेबहुक से ज़्यादा मूल्यवान है जो अंतिम एक प्रतिशत को पकड़ता है, क्योंकि वह अंतिम एक प्रतिशत पहले ही अपस्ट्रीम पकड़ा जा चुका है।

हमने वह 17:40 पर शिप किया। इसने पहली आउटेज को पूरी तरह ठीक कर दिया। इसने दूसरी आउटेज भी बना दी, और यही वह हिस्सा है जो पढ़ने लायक है।

म्यूटेटिंग, सिर्फ़ वैलिडेटिंग नहीं

हर कोई एक एडमिशन वेबहुक को एक बाउंसर की तरह सोचता है: वह ऑब्जेक्ट का निरीक्षण करता है और हाँ या ना लौटाता है। policy-controller सिर्फ़ इतना ही नहीं है। यह एक म्यूटेटिंग वेबहुक है, और यह जिसे म्यूटेट करता है वही चीज़ है जिसे उसने अभी सत्यापित किया था। जब यह एक ऐसे pod spec को एडमिट करता है जो किसी इमेज को tag के ज़रिए संदर्भित करता है, तो यह उस संदर्भ को फिर से लिखता है ताकि उसमें वह digest शामिल हो जिसे उसने हल किया और जाँचा। myregistry.io/authagonal-auth:abc123 अंदर जाता है। myregistry.io/authagonal-auth:abc123@sha256:... बाहर आता है।

यह सचमुच एक अच्छा विचार है। एक tag एक परिवर्तनशील पॉइंटर है, इसलिए एक tag को सत्यापित करना और फिर kubelet को उसे बाद में दोबारा हल करने देना एक ऐसी खिड़की छोड़ देता है जहाँ दोनों भिन्न हो सकते हैं। एडमिशन के समय digest को पिन कर देना उसे बंद कर देता है। जो ऑब्जेक्ट चलता है वही ऑब्जेक्ट है जिसे सत्यापित किया गया था।

अब इसे इस बात के साथ रखो कि एक Deployment कैसे काम करता है। Deployment कंट्रोलर आपके pod टेम्पलेट का हैश निकालता है, और वही हैश उस ReplicaSet की पहचान करता है जो pods का मालिक है। यह उस टेम्पलेट का हैश निकालता है जिसे आपने घोषित किया। वेबहुक उस ReplicaSet पर टेम्पलेट को फिर से लिखता है जिसे वह एडमिट करता है। तो अगर आपके Deployment का टेम्पलेट :abc123 कहता है, तो उसका अपना चाइल्ड ReplicaSet :abc123@sha256:... कहता है, और दोनों अब सहमत नहीं होते।

कंट्रोलर रीकंसाइल करता है, टेम्पलेट का हैश निकालता है, उस हैश वाले ReplicaSet को खोजता है, और एक ऐसा पाता है जिसका टेम्पलेट अलग है। Kubernetes में इसका ठीक एक ही मतलब होता है: एक हैश टकराव, दो अलग-अलग टेम्पलेट एक ही हैश पर आ गिरना। कंट्रोलर टकरावों को उसी तरह संभालता है जैसे उसे संभालना चाहिए। यह collisionCount को बढ़ाता है, जो हैश को विचलित करता है, और एक नया ReplicaSet बनाता है। तीन सेकंड बाद यह फिर से रीकंसाइल करता है, और नया ReplicaSet भी म्यूटेट हो चुका होता है। लूप अभिसरित नहीं होता, क्योंकि जिस असहमति को यह सुलझाने की कोशिश कर रहा है उसे वेबहुक हर बार फिर से बना देता है जब भी यह कोशिश करता है।

दो हज़ार दो सौ बयालीस ReplicaSet यही है कि एक अनंत लूप कैसा दिखता है जब आप उसे सुबह के बजाय शाम को पकड़ते हैं।

सिर्फ़ एक Deployment ही क्यों टूटा

उस नेमस्पेस में चार वर्कलोड चलते हैं। एक स्पाइरल में चला गया। तीन पूरी तरह ठीक थे, और यही वह विवरण है जिसने हमें गलत जगह देखते रखा, क्योंकि एक व्यवस्थागत ग़लत-कॉन्फ़िगरेशन को चयनात्मक नहीं होना चाहिए।

failurePolicy: Ignore इसकी वजह है। जब वेबहुक धीमा या ठंडा होता है और एक अनुरोध का समय समाप्त हो जाता है, तो Ignore का मतलब है कि ऑब्जेक्ट बिना म्यूटेट हुए एडमिट कर लिया जाता है। कोई दिया गया apply digest बेक्ड-इन के साथ वापस आया या उसी कोरे tag के साथ जिसके साथ वह अंदर गया था, यह इस पर निर्भर करता था कि वेबहुक ने समय पर जवाब दिया या नहीं। तीन स्वस्थ Deployment तब लागू किए गए थे जब यह गर्म था, इसलिए उनके टेम्पलेट पहले से ही digest ले जा रहे थे, और वेबहुक की पुनर्लेखन एक no-op थी जो पहले से मौजूद चीज़ से मेल खाती थी। जो स्पाइरल में गया वह तब लागू किया गया था जब वेबहुक ठंडा था, उसने अपना कोरा tag टेम्पलेट में बनाए रखा, और फिर उसका हर चाइल्ड ReplicaSet उसके नीचे से म्यूटेट कर दिया गया।

तो ट्रिगर एक रेस थी, हर apply पर तय होने वाली, जिसे चारों में से कोई भी किसी भी डिप्लॉय पर हार सकता था। Ignore ने बग पैदा नहीं किया। इसने एक नियतात्मक बग को एक रुक-रुक कर आने वाले बग में बदल दिया और उसे तीन स्वस्थ वर्कलोड के पीछे छिपा दिया।

समाधान एक स्थिर बिंदु है

सहज प्रवृत्ति यह है कि वेबहुक को म्यूटेट करने से रोका जाए। बेहतर प्रवृत्ति: उसे म्यूटेट करने के लिए कुछ भी न दो। अगर जो pod टेम्पलेट हम जमा करते हैं वह पहले से ही ठीक वही है जो वेबहुक बनाता, तो पुनर्लेखन कुछ नहीं बदलता, हैश स्थिर रहता है, और लूप शुरू नहीं हो सकता।

तो डिप्लॉय जॉब अब लागू करने से पहले खुद ही digest को हल करता है। हर इमेज के लिए यह रजिस्ट्री से पूछता है कि tag इस समय किसकी ओर इशारा कर रहा है, फिर पूरी तरह पिन किए गए संदर्भ को overlay में लिखता है:

digest=$(az acr repository show --name "$acr" \
  --image "authagonal-${img}:${tag}" --query digest -o tsv)
kustomize edit set image "${ref}=${ref}:${tag}@${digest}"

जो शिप होता है वह है :tag@sha256:..., tag और digest एक साथ। tag मानवीय पठनीयता के लिए रहता है, digest वही है जो वास्तव में हल होता है। वेबहुक इसे सत्यापित करता है, फिर से लिखने के लिए कुछ नहीं पाता, और ऑब्जेक्ट को अपरिवर्तित लौटा देता है। collisionCount तब से समतल रहा है।

इसकी पूँछ में एक छोटा जाल है। चूँकि digest CI लिखता है, इसलिए overlay को लैपटॉप से हाथ से लागू करने पर बदले में बेस मैनिफेस्ट का प्लेसहोल्डर tag भेजा जाता है, और पॉलिसी अब सही ढंग से "must be an image digest" कहकर उसे अस्वीकार कर देती है। क्लस्टर आपको सच बता रहा है: वह चीज़ जो आपने अभी टाइप की थी वह कभी सत्यापित ही नहीं हुई थी।

एक म्यूटेटिंग वेबहुक एक दूसरा लेखक है, चेकपॉइंट नहीं

एक म्यूटेटिंग एडमिशन वेबहुक एक चेकपॉइंट नहीं है। यह एक कंट्रोल लूप के भीतर एक लेखक है जो पहले से ही तुलना कर रहा है कि आपने क्या माँगा बनाम क्या मौजूद है। एक ही फ़ील्ड के बारे में अलग-अलग राय रखने वाले दो लेखक कोई पॉलिसी सवाल नहीं है, यह एक वितरित प्रणाली का सवाल है, और जो एक चीज़ इसे सुरक्षित बनाती है वह है इडेम्पोटेंस: जिस अवस्था को आप घोषित करते हैं उसे म्यूटेशन का एक स्थिर बिंदु होना चाहिए, ताकि उस पर म्यूटेशन लागू करने से वही फिर से बने।

वह पुनर्रचना sigstore से आगे तक सामान्य होती है। कोई भी चीज़ जो एडमिशन के समय आपके spec को फिर से लिखती है, चाहे वह sidecar इंजेक्ट करे, डिफ़ॉल्ट रिसोर्स लिमिट जोड़े, या इमेज संदर्भों को सामान्यीकृत करे, उसी स्थिति में है, और वही सवाल लागू होता है। अगर आप अपने खुद के मैनिफेस्ट को इस चीज़ से गुज़ारें, तो क्या आपको अपना खुद का मैनिफेस्ट वापस मिलेगा? अगर नहीं, तो कोई चीज़ उस अंतर को बार-बार नोटिस करती रहेगी। वह धैर्यवान होगी, और वह आपसे कहीं ज़्यादा तेज़ होगी।

और छोटा सबक, वह जिसे हमें सचमुच उस रात भुलाना पड़ा: allowed: true का मतलब यह नहीं है कि वेबहुक समस्या नहीं है। हमने एक घंटा उन लॉग को एक अलीबाई की तरह मानने में बिताया। वेबहुक पूरे समय सच बोल रहा था। हम उससे गलत सवाल पूछ रहे थे, क्योंकि हमने उसे हमेशा सिर्फ़ ऐसी चीज़ के रूप में सोचा था जो ना कहती है।

अगर आप चाहते हैं कि आपका आइडेंटिटी प्रोवाइडर अपनी सप्लाई चेन पहले से ही सुलझी हुई लेकर आए, तो Authagonal एक होस्टेड ऑथ सेवा है जिसकी इमेज CI में साइन की जाती हैं, डिप्लॉय से पहले सत्यापित की जाती हैं, और एडमिशन के बिंदु पर digest द्वारा पिन की जाती हैं, यह एक ऐसा वाक्य है जिसे हम लिख सकते हैं क्योंकि हमने इसे कमाने के लिए पहले ही एक शाम गँवा दी।