一つのブートエラー、五つの根本原因
顧客のSUSEサーバーが起動を拒んだ。復旧は五層の発掘作業になった——放置されたUSBエントリ、壊れた証明書チェーン、pipの罠、一文字のタイプミス、そしてファームウェアの落とし穴。一層直すたびに、次の層が顔を出した。
[ TIME ] Timed out waiting for device /dev/disk/by-uuid/XXXX-XXXX. [DEPEND] Dependency failed for /mnt/usb. You are in emergency mode. After logging in, type "journalctl -xb" to view system logs, "systemctl reboot" to reboot. Give root password for maintenance (or press Control-D to continue):
依頼は至ってシンプルだった。顧客のSAP HANAデータベースを載せた、SUSE Linux Enterprise Server 15搭載のHPE ProLiant DL380 Gen9が、シャットダウン後に起動してこない。コンソールにはログイン画面の代わりに緊急シェルが表示されていた。この箱が本当に健康を取り戻すまでに、互いにまったく無関係な五つの独立した問題を見つけて直すことになった。回り道も含めた全記録がこれだ——教訓はまさにその回り道に宿っているのだから。
REQUESTS_CA_BUNDLEのはずがREQUEST_CA_BUNDLE。環境変数は音もなく失敗する。意味のない怖いメッセージと、すべてを語る静かなメッセージ
コンソールは本気で不安を煽るテキストで出迎えてくれた。[Firmware Bug]: the BIOS has corrupted hw-PMU resources (MSR 38d is 330)、それにMDSやMMIO Stale DataといったCPUセキュリティ勧告の壁。サーバーが起動しない場面で画面に「Firmware Bug」と出ていたら、まずそれを追いかけたくなるのを我慢するには自制心が要る。メモだけ取って(第3幕で効いてくる)先へ進んだ——これらのメッセージはどれもブートが止まった原因ではなかった。健康だろうがなかろうが、このマシンは毎回の起動で同じものを表示する。
最初の本命仮説は、このハードウェアの定番だった。initrd——本物のディスクが使えるようになる前にカーネルが使う小さな起動用ファイルシステム——にHPE Smart Array P440ar RAIDコントローラのドライバが入っていないのでは、というものだ。ProLiant機ではおなじみの故障パターン。だが成立しなかった。緊急シェルからディスクは見えていたし、ルートファイルシステムのUUIDはカーネルコマンドラインの期待値と一致していた。回り道その一、数分で棄却——推測せずに確認したからこそだ。
本当の手がかりはほとんど見えないものだった。タイムスタンプの空白である。journalctl -xbをスクロールしていくと、ログが約89秒間、ただ…止まっていた。何もクラッシュせず、エラーも出ず——システムが静止していた。そして90秒とは、systemdがデバイスを待つときのデフォルトタイムアウトそのものだ。
# 何が失敗した?なぜ? journalctl -xb # 今回のブートの全ログ — 時間の空白を探す systemctl --failed # どのユニットが諦めたか ls /dev/disk/by-uuid # いま実際に存在するディスクは? cat /proc/cmdline # カーネルに指定されたルートデバイスは?
そしてsystemctl --failedが平文で告げた。Timed out waiting for device——続いてUSBメモリが使う短いFAT形式のUUID——そしてDependency failed for /mnt/usb。誰かがかつてUSBドライブを挿し、/etc/fstab(起動時に何をマウントするかを列挙するファイル)に登録し、後日ドライブごと持ち去ったのだ。nofailオプションがない限り、systemdはfstabの各行を約束として扱う。デバイスが現れなければブートは壊れたと見なされ、緊急シェル行きになる。原因 1
# 修正前: メモリが無いとブートは90秒固まってから失敗する UUID=XXXX-XXXX /mnt/usb vfat defaults 0 0 # 修正後: 無くてもブートは続行し、待ち時間も5秒だけ UUID=XXXX-XXXX /mnt/usb vfat nofail,x-systemd.device-timeout=5s 0 0
屈辱的なのはここだ。画面の一番最初のメッセージが犯人を正確に名指ししていた。「Timed out waiting for device… Dependency failed for /mnt/usb」は謎かけではない。何度でも学び直す教訓——理論を組み立てる前にsystemdの言葉を文字通りに読むこと。ついでにジャーナルはEFIブートパーティションのFAT-fs (sda1): Volume was not properly unmountedも見せてくれた——過去の不正なシャットダウンの傷跡だ——ので、そちらもfsck.vfatで掃除した。再起動一回でサーバーは立ち上がった。物語はここで終われたはずだった。終わらなかった。
「Certificate verify failed」——どこもかしこも
箱が起動するようになると、顧客のアプリケーションがPythonパッケージの更新を求めてきた。pipは拒否した。CERTIFICATE_VERIFY_FAILED。苛立たしいがよくある話——普通はアプリケーション側の問題だ。ところがcurlも同じホストに対して失敗した(エラー60、証明書のやつだ)。さらにzypper refがすべてのリポジトリで失敗した。このエスカレーションで診断は一変した。これはアプリの問題ではない。OS全体がインターネットを信頼するのをやめていた。
鶏と卵のオチまであった。「ca-certificatesパッケージを再インストールする」という自明の修理は、そのパッケージのダウンロード自体が壊れているTLSを必要とする状況では機能しない。(完全に追い詰められたときのために:検証を無効にしてRPMを一度だけ取得するのは許容できる最終手段だ。RPMパッケージはインストール時に検査される独自のGPG署名を持っているからだ。ただし本当の原因を直すほうがいい。)
システム全体のTLS障害では、決まった梯子を安い確認から順に降りていく。
- 時計。証明書には有効期間がある。システム日付が狂えば全部一斉に壊れる。(今回は正常。)
- 信頼ストア。
/etc/ssl/ca-bundle.pemは存在するか、健全な140本超の証明書を含んでいるか。(今回は正常。) - サーバーが実際に送ってくるもの。
openssl s_clientは提示された証明書チェーンをそのまま見せてくれる——そしてverifyエラーの番号こそが根本原因への最短ルートだ。
echo | openssl s_client -connect pypi.org:443 -servername pypi.org 2>&1 | grep -E "s:|i:|verify" 0 s:CN = pypi.org ← リーフ。署名者は… i:CN = corp-SubCA ← …チェーンのどこにも存在しない中間CA verify error:num=2:unable to get issuer certificate
目に飛び込んできたことが二つ。第一に、pypi.orgがcorp-SubCA——顧客自身の内部CAであり、PyPIが使うはずのないもの——によって発行された証明書を提示していた。第二に、verify error num=2は「誰が署名したかは見えるが、その署名者の証明書が見つからない」という意味だ。二つ合わせれば、それで話の全貌だった。
TLS検査プロキシが実際にやっていること
このネットワークも多くの企業ネットワークと同じく、暗号化トラフィックを検査するセキュリティアプライアンスを動かしている。自分の接続が途中に検問所を挟んでウェブサイトに届く、と人は想像する。実際には単一の接続など存在しない——二本あるのだ。
偽造チェーン
本物のチェーン
あなたのマシンは本物のウェブサイトの証明書を一度も見ない。見るのはプロキシが鋳造した模造品——会社のプライベート認証局が署名したものだ。マルウェアではない。アプライアンスは公言された仕事をしているだけだ。ただしそれが成立するのは、全マシンがそのプライベートCAを信頼していて、かつプロキシが書類一式を完全に渡す場合に限る。そこで「完全」とは何か。証明書チェーンには三つの輪がある。ルートCA(あなたのマシンにインストール済み、信頼の錨)、中間CA(ルートに署名され、日々の署名業務を担う)、そしてリーフ(実際のウェブサイトの証明書)だ。サーバーはリーフと中間CAを送る決まりで、ルートはあなたのマシンが持ち寄る。
署名
│
署名
│
今回のプロキシは偽造リーフと、奇妙なことにルートは配っていた——だが中間CAのcorp-SubCAは配っていなかった。チェーンは断裂、検証は不能、TLSは全滅。原因 2
では、なぜ社内のWindowsノートPCは同じプロキシ相手に平然と動いていたのか。理由は二つ。Windowsマシンはグループポリシー経由で社内CAを自動的に受け取るし、Windowsは「AIAチェイシング」を行う——中間CAが欠けていると、証明書に埋め込まれたURLをたどって欠けた輪を自力でダウンロードするのだ。Linuxのほぼすべてが使うOpenSSLは、これを意図的に拒否する。だからTLSの「Windowsでは動くのにLinuxでは失敗する」は、実質的にこの状態——不完全なチェーン——の署名なのである。
cp corp-RootCA.crt corp-SubCA.crt /etc/pki/trust/anchors/ update-ca-certificates trust list | grep -c corp # 両方ストアに入ったことを確認
ルートと欠けていた中間CAの両方がシステム信頼ストアに入れば、OpenSSLはチェーンをローカルで完成できる。curlは動いた。zypperは全リポジトリを更新した。(本当に正しい修正はプロキシに完全なチェーンを配らせる設定変更で、それはネットワークチームへのチケット案件。サーバー側の修正は今日あなたの作業を進めてくれる。)
二十分後に戻ってきた問題
pipが動くようになった——となれば当然、最初に起きるのはpip install --upgrade pipだ。そしてTLSエラーが即座に再発した。venvの中だけで。システム側は何も変わっていない。変わったのはpip自身だった。SUSEはシステム信頼ストア——まさに直したばかりのあれ——を読むようパッチしたpipを出荷している。アップグレードはそれをPyPI上のupstream版に置き換えた。upstream版はシステムストアを完全に無視し、同梱の証明書コレクション(certifiというパッケージ)を使う。corp-RootCAなど聞いたこともないコレクションを。原因 3 一行でpipはタイプミス耐性つきの恒久対応になる。
pip config set global.cert /etc/ssl/ca-bundle.pem
アプリケーション本体——Python製のセキュリティフィード集約ツール、要は脆弱性情報のRSSリーダー——も実行時に同じ形で、同じcertifi理由で失敗した。だがその失敗パターンが診断の金脈だった。MicrosoftとGoogle/Feedburnerのフィードだけが動き、他はすべて失敗する。それらはまさにプロキシのSSL検査バイパスリストに載ったドメインだった——そこ宛のトラフィックは本物の公的に信頼された証明書のまま素通りし、検査対象のドメインはcertifiが拒む社内偽造品を受け取る。一部のTLS接続先だけ動くときは、動くものの共通点をコードではなくネットワークに問うべし。原因 4
一時間かかった五分の修正
Pythonのrequestsライブラリの標準的な修正は、システムのバンドルを指す環境変数だ。設定した。何も変わらなかった。サービスを確認し、シェルプロファイルを確認し、再起動の順序を確認した——何もない。恥ずかしいほど後になって見つかった本当の問題がこれだ。
REQUEST_CA_BUNDLE=/etc/ssl/ca-bundle.pem # 私が打ったもの — 音もなく無視された REQUESTS_CA_BUNDLE=/etc/ssl/ca-bundle.pem # ライブラリが読むもの — Sに注目
足りなかったのは一文字。原因 5 そしてこれこそが環境変数の静かな凶悪さだ。綴りを間違えた変数はエラーにならない。誰にも読まれない変数になるだけだ。警告なし、ログ一行なし、何もなし。以来、黙ってタイプミスできない仕組みを好んでいる——上記のpip configか、アプリケーション自身のコードでos.environ.setdefault("REQUESTS_CA_BUNDLE", "/etc/ssl/ca-bundle.pem")とデフォルトを設定する方法だ。後者ならタイプミスは少なくともレビュー可能な一箇所に住む。
一層直せば、次の層が姿を現す。不運ではない——層状のシステムとはそういうものだ。
輪を閉じる——そしてダウングレードをかわす
一番最初の画面の[Firmware Bug]メッセージを覚えているだろうか。他がすべて健康になったところで、そこに戻ってきた。HPEのProLiant Gen9向け最終Service Pack——2022.08 SPP、この世代が受け取る最後のファームウェアバンドル——を、SUMのオンラインモードで稼働中のOSから直接適用したのだ。
そこに罠が座っていた。SUMはいくつかのコンポーネントに「Forced」トグルを表示していた。「念入りに」という響きだが、実際の意味は「提供バージョンはインストール済みより新しくない」だ。サーバーのBIOS(P89 v3.30、2023年)とiLO(2.82)は、2022年のSPPが積んでいたもの(P89 v2.92、iLO 2.81)よりすでに新しかった。これらのコンポーネントは最終バンドルが切られた後も個別のセキュリティ更新を受け続けていたからだ。「全部デプロイ」をクリックしていたら、本番HANAホストのシステムボードファームウェアをダウングレードするところだった。EOLハードウェアでは、最新の公式バンドルがあなたのマシンより古いことがある——デプロイ前に必ずInstalled列とAvailable列を読むこと。
というわけでデプロイは選択式にした。NICファームウェア、ドライブファームウェア(HPD4 → HPD6)、管理ツール群(ssacliほか)——本当に新しかった部品だけだ。それから検証の一周。
sut -status # HPEエージェント満足、保留なし ssacli ctrl all show status # RAIDコントローラ / キャッシュ / バッテリー: OK journalctl -b -p err # 今回のブート、エラーのみ: 静か
Gen9機を生かし続けている同志への覚え書き:SPPはもう二度と出ない。今後の修正は個別コンポーネント更新として届く——iLOのWeb画面からアップロードするか、Linuxの.scexe/.rpmパッケージとしてインストールする形だ。このハードウェアにとって、一つの大きなバンドルの時代は終わった。
持ち帰るもの
- 一つの症状、多数の原因。目に見える一つの障害の下に、独立した問題が何層も潜んでいることがある。一層直して新しいエラーが出るのは失敗ではない——前進だ。
- systemdは文字通りに読む。「Timed out waiting for device X」は犯人を名指ししている。理論を組む前にまず信じること。
- 約90秒は署名である。一分半の空白や停止は、ほぼ確実にsystemdがデバイスかマウントを待っている。fstabを確認。
- リムーバブルとネットワークのマウントには必ず
nofailを。USBメモリ一本の不在にブートを人質に取られてはいけない。 - TLS障害ではverifyエラー番号を取る。
openssl s_clientのnum=2は欠けた中間CAを直接指した。道具箱で一番速い工具だ。 - TLSの「Windowsでは動くのにLinuxでは失敗」は、ほぼ必ず不完全なチェーン+それを覆い隠すWindowsのAIAチェイシングを意味する。
- ディストリビューションがパッチした道具はupstreamの道具ではない。SUSEのpipはシステムストアを信頼していた。アップグレード後は黙って信頼しなくなった。
- 環境変数は音もなく失敗する。重要なものには設定ファイルかコード内デフォルトを。
- 「Forced」ファームウェアを盲目的にデプロイしない。EOLハードウェアでは最後の公式バンドルがインストール済みバージョンより古いことがある。
被害総額:古びたfstab一行、怠惰なプロキシ一台、せっかちなpipアップグレード一回、同梱の信頼ストア一つ、欠けた文字一つ——そして後退したがるファームウェアバンドル一式。どれも珍しくない。それが全部、一台のマシンに積み重なり、一つの症状の仮面をかぶっていた。
以来、このサーバーは退屈だ。この仕事において、退屈こそがトロフィーである。