2026 年 7 月 3 日,阿爾巴尼亞的 .al 網域在同一天裡失守、恢復、又用另一種方式失守——起點只是一次照表操課的 DNSSEC 金鑰輪替,root 裡的 DS 記錄卻始終沒跟上,讓整條信任鏈斷在同一個環節。
一次失敗的 DNSSEC rollover 讓 .al 全掛,EDE 33 告訴你驗證被繞過
金鑰輪替(key rollover)在 DNSSEC 維運裡通常枯燥到不會被拿出來講:發布新金鑰、等舊金鑰的簽章過期、撤掉舊金鑰,三個步驟照順序做完,沒有人會注意到。這次阿爾巴尼亞的國家通訊管理局 AKEP 對 .al 的操作,卻在同一天裡製造出三種不同形式的失敗——先是全網 .al 驗證失敗,接著操作方讓狀況更糟,最後靠 Cloudflare 動用一項存在多年、卻很少被討論的機制才把解析拉回來。拉回來的那筆回應,跟正常驗證過的回應長得一模一樣,直到 1.1.1.1 加上一個新的錯誤碼,這件事才變得可以被看見。
一次「例行」的 DNSSEC 金鑰輪替,為什麼一天內讓整個 .al 消失
DNSSEC 的信任鏈是嚴格階層式的:root zone 手上有一筆 DS 記錄,內容是子區(這裡是 .al)DNSKEY 的雜湊值;子區自己發布 DNSKEY,再用它簽署底下每一筆記錄的 RRSIG。驗證器要往上追完這條鏈,才會判定一筆答案可信。Cloudflare 在事後發布的說明裡,把這條規則講得很白:「A break anywhere in that chain, such as a DS record pointing to a key that no longer exists, causes validation to fail for everything below it.」(鏈上任何一處斷裂——比如一筆 DS 記錄指向一把已經不存在的金鑰——都會讓底下所有東西的驗證失敗。)這句話本身沒什麼稀奇,稀奇的是它接下來被完整驗證了一次。
大約 14:15 UTC,AKEP 對 .al 發布了新的 DNSKEY,同時停止提供舊金鑰;那把舊金鑰的 id 是 26319。問題出在 root 那一側:root zone 裡的 DS 記錄依然指著 id=26319 這把已經停用的金鑰,沒有同步更新。驗證器抓到 root 的 DS,去 .al 目前發布的 DNSKEY 集合裡找對應的簽章金鑰,卻找不到任何一把跟這筆 DS 對得上——鏈在 root 到 .al 這一段直接斷裂。因為 DNSSEC 的判定是全有全無,不存在「部分網域出問題」這種中間狀態:一旦這一環對不上,.al 底下每一筆記錄的驗證都同時失敗,不管那個網域自己的簽章其實有沒有問題。
這裡沒有任何一方的簽章本身出錯——.al 底下每個網域自己配發的 RRSIG,理論上都還是用當時有效的金鑰簽出來的。壞掉的不是簽名,是簽名跟簽名之上那一層之間的對應關係:驗證器不會去問「這個簽名對不對」,而是先問「你手上這把簽名用的金鑰,有沒有被上一層蓋章承認」。DS 記錄就是那個蓋章,蓋章的對象換了,底下的簽名再正確也沒有意義。
從一台正在做驗證的 resolver 的角度看,這個斷裂具體長成這樣:它先向 root 問到 .al 的 DS,再去 .al 拿當前的 DNSKEY 集合,接著要在集合裡找出一把公鑰,它的雜湊剛好等於 DS 記錄裡記的那個值。14:15 UTC 之後,這一步永遠失敗——DS 裡存的是 id=26319 的雜湊,而 .al 已經把這把金鑰撤下,集合裡剩下的新金鑰算出來的雜湊跟 DS 對不上。信任鏈在這裡斷開,驗證器沒辦法證明 .al 的 DNSKEY 是被 root 授權的,也就沒辦法信任底下任何一筆用它簽出來的記錄。
按 DNSSEC 的設計,這種驗證失敗不能被悄悄忽略。規範不允許驗證器把「算不過」降級成「無法驗證但照樣回答」,唯一合規的反應是拒絕,對外回一個 SERVFAIL。麻煩的是,坐在使用者那端的 stub resolver 沒有能力分辨 SERVFAIL 是因為簽章鏈斷了、還是權威伺服器當機、還是網路不通,它看到的只是「這個網域現在拿不到答案」。於是 .al 底下每一個網站,在使用者眼裡都變成一種講不出原因的下線。一次金鑰輪替的手滑,就這樣被 DNSSEC 的正確失敗機制放大成整個 TLD 的靜默失守。
這件事的影響範圍,用一個數字就能定位:.al 在 Cloudflare Radar 的 TLD 排名是第 191 名。不是流量最前段的巨型後綴,但也遠不是沒人用的冷門 TLD——阿爾巴尼亞的政府、銀行與媒體網域都掛在 .al 底下。鏈一斷,受波及的不是幾個測試網域,而是一個國家的公共與金融入口,在接下來幾個小時裡,對全世界所有照規範驗證的 resolver 同時失去蹤影。
這正是接下來要拆的第一層:斷點不是隨機的、也不是攻擊者刻意製造的,就是這一筆本該同步更新、卻沒有更新的 DS 記錄。要看清楚這句話的分量,得先把信任鏈的四個環節攤開來看——root 本身、.al 的 DS、.al 的 DNSKEY、底下每個網域自己的簽章——哪一環先斷,後面全部連帶遭殃。
點任一層看細節 · 4 個環節
.al 信任鏈的四個環節
點卡片或層看細節
L1 · 為什麼不用驗證
全球所有支援 DNSSEC 驗證的 resolver,出廠就內建 root zone 的公鑰——這是整條信任鏈唯一不用被誰證明的一環,往下每一層都靠這一把錨定。
L2 · 這次卡住的環節
DS 記錄是 root zone 裡對應 .al 的一筆雜湊值,指向 .al 應該用哪一把 DNSKEY 簽章。Cloudflare 原文講得直接:「A break anywhere in that chain, such as a DS record pointing to a key that no longer exists, causes validation to fail for everything below it.」14:15 UTC,AKEP 換上新 DNSKEY、停用舊金鑰之後,root 裡的 DS 依然指著 id=26319 這把已經停用的金鑰——這正是原文點名的斷點型態。
L3 · 17:00 UTC 之後更糟
DNSKEY 集合裡至少要有一把跟 root DS 對得上的金鑰,DS 才有東西可以驗證。17:00 UTC,AKEP 把剛發布的新 DNSKEY 也撤了,舊金鑰又沒恢復——zone 裡沒有任何 DNSKEY,而 root 的 DS 仍在等一把已經不存在的金鑰,兩頭都對不上。
L4 · 簽章本身沒有問題也一樣失敗
每個 .al 底下的網域(例如 google.al)所帶的 A 記錄,都各自配有一份 RRSIG。驗證器要往上追完 root 到 DS 到 DNSKEY 這條鏈,才會放行這份簽章。鏈斷在 L2 或 L3 的任何一步,L4 的簽章就算完全正確,也一樣被判定無法驗證——這就是為什麼問題不是幾個 .al 網域出錯,而是全部一起失敗。
假設操作方會馬上把舊金鑰接回去——他們反而讓狀況更糟
照直覺,發現 DS 對不上舊金鑰之後,最快的修法應該是把舊金鑰放回 DNSKEY 集合、等 root 的 DS 追上來,或者反過來把 DS 換成新金鑰的雜湊——兩條路都能讓鏈重新接上。AKEP 實際做的是第三種:大約 17:00 UTC,他們把剛發布的新 DNSKEY 也撤了,舊金鑰依然沒有恢復。這個時間點,.al 的 DNSKEY 集合是空的,而 root 的 DS 還在等一把三小時前就已經停用、現在整個 zone 裡完全找不到的金鑰。驗證沒有變好,只是換了一種對不上的方式——第一次是「DS 指的金鑰不再服務」,第二次是「DS 指的金鑰從來沒被任何 DNSKEY 記錄涵蓋」,兩種都會讓驗證器判定失敗,差別只在對不上的細節。
下面這個時間軸,把 14:15 UTC 到隔天之間,root DS、.al 的 DNSKEY、驗證結果,以及 1.1.1.1 回應裡會帶什麼 EDE 訊號,按時間點攤開。拖曳滑桿可以看到:狀況不是一路變好,而是先斷、再斷、被 Cloudflare 硬接、最後由 AKEP 自己拆掉整條鏈。
拖曳滑桿看 5 個時間點 · 14:15 到隔天
- DS 記錄
- DNSKEY
- 驗證結果
- EDE
- Cloudflare 動作
拖曳滑桿重播事件時間軸,對照每個時間點的 DS、DNSKEY、驗證結果與 EDE 訊號
14:15 換鑰失敗,17:00 更糟;17:15 Cloudflare 套 NTA 恢復解析但停止驗證,19:15 root 撤 DS,.al 變成未簽章。
兩次操作都沒有解決 root 端跟 .al 端對不上的根本問題,而全球驗證器沒有辦法自己等——凡是照規範走的驗證器,遇到斷鏈只有一種合規反應:拒絕這筆回應。真正把解析拉回來的,不是 AKEP 的下一步操作,是 Cloudflare 在 resolver 端做的一個選擇。
換句話說,「操作方一定會盡快把狀態修正回去」這個假設,本身就假設了修正的方向是唯一、而且是朝著恢復信任鏈前進的。這次的兩步操作提醒的是另一件事:在事故現場,把 DNSKEY 集合清空,跟把 DS 記錄指到正確金鑰,兩者對操作者來說可能是同一張待辦清單上不分先後的兩個步驟,但對驗證器而言,前者只是把斷裂的位置換了個地方,不是往修好的方向走。
Cloudflare 的解法不是修好驗證,是先關掉驗證
Cloudflare 沒辦法從外部修好 AKEP 的 DS 記錄,甚至聯絡不上對方。原文寫得直接:「We received no response, in part because the operator's contact addresses were themselves under .al, making them unreachable during the outage.」(沒收到回覆,部分原因是對方的聯絡位址本身就掛在 .al 底下,這場事故期間根本連不上。)出事的網域,連拿來通報這次事故的信箱都連不上,只能先靠自己的手段止血。
Cloudflare 選的手段是對 .al 套用 Negative Trust Anchor(NTA,RFC 7646),17:15 UTC 全面推給所有 1.1.1.1 使用者——距離鏈斷掉大約三小時。原文的說法是:「We applied the NTA for .al and rolled it out to all 1.1.1.1 users by 17:15 UTC, roughly three hours after the chain broke.」RFC 7646 對 NTA 本身的定義很明確:套用之後,「the validator treats any upstream responses as if the zone is unsigned and does not set the Authentic Data (AD) bit in responses it sends to clients」——驗證器把這個 zone 當成沒簽章,不會在回應裡設 AD 位元。換句話說,NTA 換來的不是「修好驗證」,而是「暫時不驗證」。
這個代價,Cloudflare 自己講得很清楚:「a Negative Trust Anchor suspends DNSSEC validation, which means .al domains were no longer protected against DNS spoofing for the duration.」RFC 7646 用另一句話講同一件事:「End-to-end DNSSEC validation will be disabled during the time that an NTA is used.」好消息是 NTA 的範圍可以精準限定——「This NTA can potentially be implemented at any level within the chain of trust and would stop validation from that point in the chain down」——所以 Cloudflare 這次動的只是 .al 這一段,不影響其他任何 zone 的驗證。但 .al 底下的每一筆查詢,在 NTA 生效的這段期間,都失去了原本該有的加密保證:解析器選擇讓使用者能查到網站,代價是不再保證那筆答案沒被竄改。
往回看 RFC 7646,NTA 從被定義出來的那天起就是一根臨時拐杖,不是常備設施。它設想的情境正是這一種:某個 zone 的簽章因為維運失誤而壞掉,錯不在你這台 resolver,但你得替使用者決定要不要繼續把這些回應擋在門外。RFC 還點出一個容易被忽略的副作用——「One side effect of implementing an NTA is that it may break client applications that assume that a domain is signed and expect an AD bit in the response.」(實作 NTA 的一個副作用是,可能讓那些假設某網域一定有簽章、預期回應裡帶 AD 位元的用戶端程式出錯。)因為 NTA 會讓驗證器停止在回應裡設 AD 位元,任何靠這個位元判斷「這筆答案驗過了」的下游程式,會突然收到一個沒有 AD 位元、外觀卻正常的答案。所以 NTA 該被當成用完就撤的止血手段,而不是讓一個 zone 長期泡在未驗證狀態裡——這也是為什麼 Cloudflare 一等到 .al 的 DS 被移除,隔天就把 NTA 收掉。
這筆回應到底有沒有被驗證過?在 EDE 33 之前,你根本看不出來
NTA 這個機制本身不是新東西,RFC 7646 早就定義好。它一直以來的問題是:啟用之後,從外面看,一筆走 NTA 覆寫出來的回應,跟一筆真的驗證通過的回應長得一模一樣。Cloudflare 原文把這個落差講得很白:「a response served under an NTA looked identical to a fully validated one.」你查一筆網域,拿到一個看起來正常的 A 記錄,resolver 沒有告訴你這筆答案有沒有真的被驗證過——監控工具也一樣看不出來,除非你剛好知道有人在某個時間點對某個 zone 套了 NTA。
這正是 Extended DNS Errors(EDE,RFC 8914)想解決的問題類型。RFC 8914 本身的定位是:「This document specifies a mechanism to extend DNS errors to provide additional information about the cause of an error.」它把「為什麼」補進原本只有粗粒度狀態碼的回應裡。EDE 9 對應 DNSKEY Missing;EDE 33 是新加的一格,對應 Negative Trust Anchor,由 Quad9 的 Babak Farrokhi 提出 Internet-Draft、Cloudflare 加入共同作者,IANA 已經分配好編號。
點按鈕切換前後對照 · 2 種畫面
「a response served under an NTA looked identical to a fully validated one.」——同一筆回應,看不出有沒有被驗證過。
同一筆 google.al 回應,切換看 EDE 33 出現前後的差異
沒有 EDE 33 之前,NTA 覆寫的回應看起來跟已驗證的一模一樣;現在同一筆回應會多帶 EDE 9 與 33,直接寫明沒驗證、為什麼沒驗證。
這次事故留下的一筆實例,長這樣:
$ kdig @1.1.1.1 google.al
;; ANSWER SECTION:
google.al. 300 IN A 142.251.142.196
;; EDE: 9 (DNSKEY Missing): 'no SEP matching the DS found for al.'
;; EDE: 33 (Negative Trust Anchor): 'a Negative Trust Anchor has been applied for this query (see RFC 7646)'
google.al 查回來的是正常的 NOERROR 加一筆 A 記錄,但兩個 EDE 欄位把底下發生的事講清楚:EDE 9 說「no SEP matching the DS found for al.」,找不到跟 DS 對得上的簽章金鑰;EDE 33 說「a Negative Trust Anchor has been applied for this query (see RFC 7646)」,這筆查詢是靠 NTA 放行的。同一個答案,多了兩行,操作者跟監控工具第一次能直接從回應裡讀出「這筆沒被驗證,而且是因為什麼沒被驗證」。目前 Knot 專案的 kdig 已經可以照名稱顯示 EDE 33,Unbound 那邊的 pull request 還在審查中,討論則持續在 IETF DNSOP 工作組進行。任何自己維運 resolver、或者只是消費 1.1.1.1 記錄檔的團隊,往後都可以把 EDE 33 當成一個可以搜尋、可以拉報表的訊號,用來確認自己解析的哪些 zone 正處在被覆寫的狀態——這是合理的推測延伸,原文本身沒有把話說到這麼細,但方向是 EDE 機制本來就打算做到的事。
這兩個 EDE 其實各自回答不同的問題,湊在一起才有意義。EDE 9(DNSKEY Missing)講的是底層那個原始故障——.al 的簽章鏈為什麼壞了,因為找不到跟 DS 對得上的 SEP 金鑰。EDE 33(Negative Trust Anchor)講的是 resolver 對這個故障做了什麼——它套了一個 NTA,決定放行這筆本來會被拒絕的回應。只看 EDE 9,你知道 .al 的 DNSSEC 出事了,卻不明白為什麼還是拿得到答案;只看 EDE 33,你知道 resolver 繞過了驗證,卻不知道它在繞過什麼。兩個並列,才拼得出完整因果:底層壞在哪、上層又替你做了什麼決定。RFC 8914 定義的 INFO-CODE 就是為這種場合設計的——「This 16-bit value, encoded in network most significant bit (MSB) byte order, provides the additional context for the RESPONSE-CODE of the DNS message.」一個 16 位元的補充脈絡,掛在原本粗粒度的 RESPONSE-CODE 旁邊,把「發生什麼」和「為什麼」拆成兩格分別講清楚。
19:15 UTC 之後,.al 自己也做了同一種取捨
大約 19:15 UTC,AKEP 把 DS 記錄整個從 root zone 移除。原文的說法很直接:「Without a DS record, every .al domain is unable to use DNSSEC protections.」少了 DS,.al 底下任何網域都無法使用 DNSSEC 保護——但反過來,驗證器也不會再因為找不到對得上的金鑰而拒答,解析恢復正常。這其實是跟 Cloudflare 的 NTA 同一種邏輯的另一個版本:與其花時間把簽章銜接回去,不如先讓 .al 整個變成未簽章,用放棄保護換回可用性。差別在於這次是權威端自己動手,不是靠 resolver 端覆寫。
至於怎樣才算把輪替做對,Cloudflare 這篇沒有展開,合理的推測是:一次安全的金鑰輪替,關鍵在於任何一個瞬間,root 的 DS 跟 .al 正在服務的 DNSKEY 之間都至少留著一條對得上的鏈。常見的做法是讓新舊金鑰並存、雙重簽署一段時間,先把新金鑰的雜湊放進 root 的 DS、等它確實生效、也等全球快取裡的舊 DS 過期,最後才撤掉舊金鑰——順序上永遠是「先讓父區認得新金鑰,再淘汰舊金鑰」。.al 這次剛好把順序做反了:先停掉舊金鑰,卻沒等 DS 跟上,於是整條鏈在沒有任何緩衝的情況下瞬間斷開。這一步一旦錯了,後面不管是 Cloudflare 的 NTA 還是 AKEP 自己移除 DS,都只是在替這個沒有留退路的切換善後。
NTA 因此失去存在的理由——原文寫道:「The Negative Trust Anchor was removed the following day, once the .al operator had removed the DS record from the root zone.」隔天,Cloudflare 撤掉了對 .al 的 NTA。但這不等於事故真正解決:原文在發布當下的用詞是「As of publishing, .al remains unsigned.」截至那篇文章發布為止,.al 還沒有重新簽署。可用性回來了,加密保證還沒有——這也是為什麼判斷「.al 現在是不是已經修好」不能只看能不能查到網站,得看 DS 有沒有重新出現在 root 裡。
這種先求可用、後補驗證的順序,不是 .al 這次事故獨有的做法——任何依賴信任鏈驗證的系統,一旦鏈斷在關鍵位置,維運端能做的選擇通常就只剩兩種:等鏈修好,或者先繞過鏈。差別只在繞過的動作發生在 resolver 端(Cloudflare 的 NTA)還是權威端(AKEP 移除 DS),兩者對使用者呈現的結果一樣:網站查得到,但沒有人再幫你確認過這筆答案沒被動過手腳。
Next time:一個 TLD 級的 DNSSEC 金鑰輪替,不能只看新金鑰有沒有發布成功,root 端的 DS 有沒有跟上才是真正的斷點所在——下次遇到看起來像大規模解析異常的事故,先查這條鏈有沒有在某一層對不上,而不是先猜是不是哪裡被攻擊了。同時,如果你的監控只盯 SERVFAIL 或 NOERROR,現在起也該把 EDE 欄位讀進來:EDE 33 之前,一筆被 NTA 覆寫過的回應,你根本看不出跟正常驗證過的答案有什麼差別。