VPSでAES-NIを確認・復元する方法
VPSでAES-NIが見えるかを確認し、CPUIDをマスクした場合のAES-GCM速度低下を測定します。OPENSSL_ia32capで高速経路を戻す方法も解説します。
VPS で AES-NI が実際にもたらす効果
VPS の AES-NI は、AES(advanced encryption standard)の 1 ラウンドをハードウェアで実行する 6 つの x86 命令です。プロバイダーが CPU モデルでこれらの命令を隠している場合でも、基盤となるシリコンには通常搭載されています。ただし、OpenSSL からは利用できないため、1 バイトあたりおよそ 10 倍のサイクル数を要するソフトウェア実装にフォールバックします。1 つのコマンドで機能の有無を確認し、2 つのコマンドで差を測定できます。さらに、1 つの環境変数で高速な処理経路を再び有効にできる場合があります。
命令は AESENC、AESENCLAST、AESDEC、AESDECLAST、AESIMC、AESKEYGENASSIST です。Intel は 2010 年にこれらを出荷し、AMD も続きました。そのため、通常レンタルできるサーバー CPU には搭載されています。関連する命令 PCLMULQDQ はキャリーレス乗算を実行します。これは、GCM(Galois/counter mode)が認証タグを生成するために必要です。AES-GCM が高速になるのは、両方を利用できる場合だけです。暗号化処理とタグ生成は別々の処理だからです。
VPS の監視でこの影響が現れる場所は、次の 4 つです。
- TLS(transport layer security)の終端。AES-128-GCM または AES-256-GCM を提供する Web サーバーでは、バルク暗号処理の大半を AES 内で実行します。
- 暗号化ボリューム。LUKS(Linux unified key setup)と dm-crypt は、カーネル内で読み取りと書き込みのたびに CPU 上で
aes-xtsを実行します。 - AES ベースの VPN トラフィック。
AES-256-GCMを使用する OpenVPN と AES-GCM を使用する IPsec は、どちらもこの機能に大きく依存します。 - 暗号化バックアップ。AES でストリームを暗号化してからサーバー外へ送信する処理では、同じコストが発生します。
一般的なワークロードの 1 つは、まったく影響を受けません。WireGuard はデータ処理に ChaCha20-Poly1305 を使用し、AES を使用しません。そのため、自己ホスト型 WireGuard VPN は、フラグをマスクしたホスト上でも同じ速度で動作します。この違いは、安価な VPS でトンネルを選ぶ前に、WireGuard と OpenVPN を比較するべき実用的な理由になります。
VPS で AES-NI を利用できるか確認する方法
カーネルは CPUID の機能ビットを /proc/cpuinfo にコピーするため、grep で確認できます。
grep -m1 -o '\baes\b' /proc/cpuinfo
lscpu | grep -i -o '\baes\b'いずれかのコマンドで aes が出力されれば、CPU はこのゲストに AES-NI を通知しています。何も出力されなければ、通知していません。lscpu も同じフラグを読み取るため、結果は常に一致します。インストールされている方を使用してください。
次に、ホストがどの CPU 上で実行していると認識させているかを確認します。
grep -m1 'model name' /proc/cpuinfoIntel(R) Xeon(R) Gold 6338 CPU @ 2.00GHz や AMD EPYC 7443P 24-Core Processor のような実際のモデル文字列が表示される場合、ホストは物理 CPU のモデル情報をゲストに渡しています。QEMU Virtual CPU version 2.5+ や Common KVM processor が表示される場合は別の仕組みが動作しており、そのケースを理解する価値があります。
シリコンに機能があるのにフラグが表示されない理由
CPUID は、プログラムが CPU の対応機能を確認するために使用する命令です。仮想マシン内では、CPUID は必ずハイパーバイザーにトラップするため、ゲストに何を通知するかはハイパーバイザーが決定します。多くの管理画面では、その決定をゲスト CPU モデルとして公開しています。qemu64 と kvm64 は汎用のベースラインモデルであり、どちらの機能セットにも AES-NI または SSSE3 が含まれていません。そのため、物理ホストが最新の EPYC であっても、ゲストには aes フラグが表示されません。VPS は他者のハードウェア上で動作するゲストです。したがって、報告される機能はすべて、1 段上の層で決定されています。この構成に慣れていない場合は、まず VPS とは何か を確認してください。
ホストが汎用モデルを選ぶのは意図的です。プロセッサの異なるマシン間でライブマイグレーションを行うには、移行先にない機能をゲストへ通知していないことが条件になるためです。そのコストは利用者が負担します。カーネルと OpenSSL は、起動時にマスクされた CPUID を一度読み取ります。その後、プロセスが終了するまで低速なコードパスを選択します。
根本的な解決策は、ホスト側の設定です。QEMU では -cpu host を使用するか、AES-NI を含む名前付きモデルを指定します。または、モデルに明示的な +aes を追加します。これらの設定をゲスト内から変更することはできません。サポートチケットを起票するか、ハイパーバイザーが CPU モデルをそのまま渡すプランを選択することが、恒久的な解決策です。
openssl speed で差を測定する
公開されているベンチマークをそのまま信頼しないでください。実際に自分のサーバーがネゴシエートする暗号を実行します。
openssl version
openssl speed -evp aes-128-gcm結果の行には AES-128-GCM と表示され、6 種類のブロックサイズにおけるスループットが、毎秒 1000 バイト単位で示されます。バルク転送では 8192 バイトの列を確認してください。16 バイトの列は呼び出しごとのオーバーヘッドの影響が大きく、ファイルのダウンロード性能は判断できません。
次に、ソフトウェア上で AES-NI と PCLMULQDQ を無効にして、同じコマンドを実行します。
OPENSSL_ia32cap="~0x200000200000000" openssl speed -evp aes-128-gcmこの値は、OpenSSL が公開している機能ビットベクトルの仕様に基づいています。先頭の ~ は「これらのビットをクリアする」という意味です。ビット 57 は AES-NI、ビット 33 は PCLMULQDQ なので、0x200000200000000 はこの 2 つだけを正確に指定します。2 つ目の値が 1 つ目より大幅に小さければ、サーバーの AES-NI は正常に動作しており、確認は完了です。2 つの値が一致する場合、OpenSSL はもともとソフトウェア処理の経路を使用していました。クリア対象のフラグが存在しなかったためです。
The data behind this chart
[
{
"label": "AES-NI and PCLMULQDQ",
"mb_per_sec": "4,850",
"cycles_per_byte": 0.7
},
{
"label": "Software fallback",
"mb_per_sec": 310,
"cycles_per_byte": 11.0
}
]これらは 3.4 GHz 前後で動作する最新の x86 コアについて公開されている代表値であり、特定のホストで測定した値ではありません。性能の傾向を把握するために使用してください。ハードウェア処理の経路は 1 バイトあたりおよそ 0.7 サイクル、ソフトウェアフォールバックはおよそ 11.0 サイクルです。1 コアあたりの性能に換算すると、約 4,850 MB/s と 310 MB/s になります。上の 2 つのコマンドで得られる値だけが、実際のサーバーを表す数値です。同じ考え方をマシンの他の要素にも適用してください。プランについて結論を出す前に、VPS の性能を再現可能な方法で測定する手順 と組み合わせて確認します。
OPENSSL_ia32cap でビットを再度有効にする
ここが意外に思われる点です。AES-NI 命令は非特権命令であり、ハイパーバイザーによってトラップされません。トラップされるのは CPUID だけです。そのため、ホストはゲストに AES-NI がないと通知しながら、AESENC はネイティブ速度で実行させ続けることができます。ソフトウェアは CPUID を実行して誤った結果を受け取るため、高速パスを使用しません。命令自体は停止していません。
OpenSSL を使うと、CPU の代わりにこの情報へ回答できます。OPENSSL_ia32cap に通常の 16 進数値を指定すると、機能ベクトルをマスクするのではなく上書きします。
OPENSSL_ia32cap="0x0200020207000000" openssl speed -evp aes-128-gcmこの実行結果が通常の実行結果より数倍速ければ、その CPU には AES-NI があり、ホストがそれを隠しています。これはまず診断のための確認です。OpenSSL では、そのまま修正にもなります。
その 16 進数値の作り方
最初の論理ベクトルでは、CPUID leaf 1 の EDX を下位 32 ビットに、leaf 1 の ECX を上位 32 ビットに格納します。下位半分では、bit 24 が FXSR、bit 25 が SSE、bit 26 が SSE2 であり、0x07000000 になります。上位半分では、bit 33 が PCLMULQDQ、bit 41 が SSSE3、bit 57 が AES-NI であり、0x02000202 になります。これらを組み合わせると 0x0200020207000000 です。SSSE3 を含めるのは、OpenSSL の PCLMULQDQ ベースの GHASH が pshufb を使ってバイトを入れ替えるためです。また、汎用ゲスト CPU モデルでは AES-NI と同時に SSSE3 も隠されます。
注意点が 2 つあります。どちらも意図的に発生させることができます。
最初のベクトルだけを設定すると、後続のベクトルは 0 のままになり、AVX2 と AVX-512 のコードパスが無効になります。ここでは意図した動作です。マスクされたゲストで AVX ビットを強制的に有効にしないでください。AVX レジスターを使用するには、オペレーティングシステムが XCR0 で拡張状態を有効にする必要があります。しかし、カーネルは同じくマスクされた CPUID の結果に基づいて、その機能を有効にしていません。VEX エンコード命令を実行すると未定義オペコード例外が発生し、プロセスが終了します。
実際には AES-NI を持たないコアで AES-NI を強制的に有効にすると、プロセスは直ちに終了します。
Illegal instruction (core dumped)これは AESENC が未定義オペコード例外を発生させるためです。そのコアには実行可能な該当命令が存在しません。一部のサーバーファームウェアでは、次回のリセットまで AES-NI をハードウェアで無効にすることもできます。この場合も症状は同じです。いずれの場合も、必要なのは別の環境変数ではなく、別のホストです。
長時間稼働するサービスでこの上書きを維持するには、systemd の drop-in を使用します。
sudo systemctl edit nginx[Service]
Environment="OPENSSL_ia32cap=0x0200020207000000"sudo systemctl daemon-reload
sudo systemctl restart nginx
sudo systemctl show nginx -p Environment最後のコマンドでは、設定した変数が表示されるはずです。この設定によって生じる条件を理解してください。そのサーバーが、CPU に本当に AES-NI がないホストへ移行されると、nginx は最初の TLS 接続で不正命令により終了します。runbook に記載するか、本番環境では上書きを完全に避け、チケットを起票して問題を説明するときだけ、事実確認のために使用してください。
オーバーライドで修正できないこと
OPENSSL_ia32cap は OpenSSL にだけ適用されます。他のソフトウェアはそれぞれ独自に機能検出を行い、この変数を読みません。
重要なのはカーネルです。dm-crypt と LUKS はカーネルの暗号 API を使用します。CPU の機能ビットがない場合、aesni_intel モジュールはロードに失敗します。
modprobe: ERROR: could not insert 'aesni_intel': No such deviceこの動作を制御するユーザー空間の変数はありません。カーネルはブート時に CPUID を 1 回読み取り、その判定を再起動するまで維持します。別のホストで再起動しない限り、暗号化ボリュームは OpenSSL の動作に関係なくソフトウェア暗号方式を使用します。実際の性能を測定してください。
sudo cryptsetup benchmark -c aes-xts -s 256aes-xts 256b の行は、ハードウェア AES が有効な場合は数千 MiB/s になり、無効な場合は数百 MiB/s 前半になります。Go や Java など、独自に機能検出を行う言語ランタイムにも同じ変数は適用されません。Go の crypto/aes は CPUID を直接確認し、そのビットがない場合は定数時間のソフトウェア実装を静かに使用します。TLS を終端するサービスが Go バイナリの場合、OpenSSL の変数を変更しても効果はありません。
AES-NI を利用できない場合は ChaCha20 を優先する
ChaCha20-Poly1305 は、ソフトウェアだけで高速に動作するよう設計されています。利用可能な AES-NI がないコアでは、通常 AES-GCM を大幅に上回るため、そのようなホストでは AES を優先しない構成に変更するのが適切です。
OpenSSL 1.1.1 以降に対してビルドされた nginx 1.19.4 以降では、次のように設定します。
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_conf_command Ciphersuites TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384;ssl_ciphers は TLS 1.2 を対象にします。ssl_conf_command Ciphersuites は TLS 1.3 を対象にします。nginx には TLS 1.3 専用のディレクティブがなく、この文字列を検証せずにそのまま OpenSSL へ渡すため、入力を誤っても警告なしに受け入れられます。設定をリロードし、クライアントに提示される内容を確認します。
sudo nginx -t && sudo systemctl reload nginx
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>/dev/null | grep -i cipher正常な結果には TLS_CHACHA20_POLY1305_SHA256 が含まれます。変更を確定する前に、同じホストで AES の実行結果と並べて openssl speed -evp chacha20-poly1305 を実行し、2 つの数値に基づいて判断します。
ARM ホストでは拡張機能が異なります
AES-NI は x86 専用です。ARM VPS では、同じ処理を行う別の命令セットである ARMv8 暗号拡張機能を使用します。aarch64 では、フラグは flags ではなく Features にあります。
grep -m1 Features /proc/cpuinfoaes と pmull を確認します。pmull は PCLMULQDQ に相当する ARM 側の機能であり、同じ理由で GCM に必要です。ARM で OpenSSL の機能を上書きする変数は OPENSSL_armcap です。そのビット配置は OpenSSL ソースの crypto/arm_arch.h で定義されているため、このガイドに記載した x86 の 16 進値は ARM では意味を持ちません。実際には、VPS ホストとして販売されている ARM サーバーのコアではこれらの拡張機能が公開されているため、機能マスクの問題は主に x86 で発生します。
確認できる症状と表示される文字列
/proc/cpuinfo に aes がなく、強制実行の方が大幅に速い。 ホストが CPUID をマスクしています。モデル名が汎用的なものになっていることを確認し、ハイパーバイザーがゲストに提示している CPU モデルをプロバイダーへ問い合わせてください。
/proc/cpuinfo に aes がなく、強制実行で Illegal instruction と表示される。 命令が実際に存在しないか、ファームウェアによって無効化されています。ワークロードを別のホストへ移してください。
aes は存在するが、スループットが低いままである。 8192-byte の列を読み取っていることと、ほかの処理がその CPU コアを使用していないことを確認してください。共有プランでは、異なる時刻にテストを 2 回実行するまで、ノイジーネイバーが CPU 機能の欠落とまったく同じように見えることがあります。
マスクした実行と通常の実行で同じ値になる。 OpenSSL はすでにソフトウェアパスを使用しています。この結果がテストで得られる結論であり、テストの誤りではありません。
ネストした仮想化によって、1 段下の環境で結果が変わる。 ゲスト内のゲストは、中間層が渡すことを選択した CPUID を受け取ります。そのため、AES-NI は気付かないうちに失われやすくなります。VPS 上で ネストした仮想マシンを運用する場合は、借りたマシンだけでなく、内側のゲストでもフラグを確認してください。
FAQ
VPS の /proc/cpuinfo に aes フラグがないのはなぜですか?
ハイパーバイザーが汎用のゲスト CPU モデルを提示しているためです。qemu64 と kvm64 には機能セットに AES-NI が含まれていないため、物理プロセッサに関係なく、CPUID は AES-NI がないと報告します。これは、異なる CPU を搭載したマシン間で実行中のゲストを移行できるように、ホストが行う処理です。grep -m1 'model name' /proc/cpuinfo を実行してください。QEMU Virtual CPU version 2.5+ や Common KVM processor のような文字列が表示されれば、その CPU モデルが使われています。実際の Xeon または EPYC のモデル文字列が表示される場合は、CPU モデルがパススルーされており、フラグがないのはシリコン自体に命令がないためです。
OPENSSL_ia32cap は本当に AES-NI を有効にしますか。それとも有効になったように見せるだけですか?
実際の命令を有効にします。AES-NI 命令は非特権命令であり、ハイパーバイザーが命令をトラップすることもないため、AESENC は CPUID の報告内容に関係なくネイティブに実行されます。インターセプトされるのは CPUID 命令だけです。OPENSSL_ia32cap に単純な 16 進値を設定すると、OpenSSL が CPUID から取得した結果が置き換わります。そのため OpenSSL はハードウェア用のコードパスを選択し、ハードウェアがそれを最大速度で実行します。シリコンに実際には命令がない場合、最初の AES 処理で Illegal instruction (core dumped) が発生し、プロセスは終了します。
この上書きで LUKS 暗号化ボリュームは高速化しますか?
いいえ。OPENSSL_ia32cap を読み取るのは OpenSSL だけです。LUKS と dm-crypt はカーネルの暗号 API を使用するため、機能ビットがクリアされている場合、aesni_intel モジュールは modprobe: ERROR: could not insert 'aesni_intel': No such device で読み込みに失敗します。カーネルはブート時に CPUID を読み取るため、ユーザー空間の変数で変更することはできません。実際の値は sudo cryptsetup benchmark -c aes-xts -s 256 で測定し、aes-xts 256b 行をフラグを報告するホストと比較してください。
AES-NI フラグがないと WireGuard は遅くなりますか?
いいえ。WireGuard はすべてのデータに ChaCha20-Poly1305 を使用し、AES 命令を使用しません。そのため、フラグがマスクされたホストとマスクされていないホストでスループットは変わりません。AES-GCM で設定した OpenVPN と IPsec は、AES-NI のないホストではスループットが低下します。そのため、同じ VPS 上の 2 つのトンネルでも動作が大きく異なる場合があります。ネットワークを原因と判断する前に、この点を確認してください。
ARM VPS で AES-NI を確認するにはどうすればよいですか?
ARM コアには AES-NI がありません。代わりに ARMv8 暗号拡張を備えており、異なる命令で同じ処理を行います。grep -m1 Features /proc/cpuinfo を実行し、aes と pmull を確認してください。aarch64 では、これらは flags ではなく Features の下に一覧表示されます。x86 の OPENSSL_ia32cap 値は ARM では意味を持ちません。ARM における OpenSSL の同等の変数は OPENSSL_armcap であり、そのビット配置は OpenSSL のソースにある crypto/arm_arch.h で定義されています。