Kiểm tra và bật lại AES-NI trên VPS
Kiểm tra VPS có expose AES-NI không, đo chi phí khi CPUID bị mask làm giảm throughput AES-GCM, rồi bật lại fast path bằng OPENSSL_ia32cap.
AES-NI trên VPS thực sự mang lại gì
AES-NI trên VPS là một tập hợp gồm sáu instruction x86 thực hiện một round của AES (advanced encryption standard) bằng phần cứng. Nếu nhà cung cấp ẩn model CPU và các instruction này, phần silicon bên dưới vẫn có chúng, nhưng OpenSSL không thể thấy chúng và phải chuyển sang implementation bằng software, với chi phí khoảng gấp mười lần số cycle trên mỗi byte. Bạn có thể kiểm tra feature này bằng một command, đo chênh lệch bằng hai command, và thường bật lại fast path bằng một biến môi trường.
Các instruction là AESENC, AESENCLAST, AESDEC, AESDECLAST, AESIMC và AESKEYGENASSIST. Intel đưa chúng vào CPU từ năm 2010 và AMD cũng làm theo, vì vậy mọi server CPU mà bạn có khả năng thuê đều có phần silicon này. Một instruction đi kèm là PCLMULQDQ, thực hiện phép nhân không nhớ, cần cho GCM (Galois/counter mode) để tạo authentication tag. AES-GCM chỉ nhanh khi cả hai đều khả dụng, vì cipher và tag là hai phần công việc riêng biệt.
Có 4 nơi trên VPS mà điều này xuất hiện trong monitoring của bạn:
- Kết thúc TLS (transport layer security). Web server cung cấp AES-128-GCM hoặc AES-256-GCM sẽ dành phần lớn thời gian xử lý bulk crypto bên trong AES.
- Volume được mã hóa. LUKS (Linux unified key setup) và dm-crypt chạy
aes-xtstrên mọi thao tác đọc và ghi, trong kernel, trên CPU. - Network traffic VPN dùng AES. OpenVPN với
AES-256-GCMvà IPsec với AES-GCM đều phụ thuộc vào nó. - Backup được mã hóa. Mọi công cụ mã hóa stream bằng AES trước khi dữ liệu rời khỏi máy đều chịu cùng chi phí này.
Có một workload phổ biến hoàn toàn không bị ảnh hưởng. WireGuard dùng ChaCha20-Poly1305 cho data và không bao giờ dùng AES, nên WireGuard tự host chạy với cùng tốc độ trên host bị mask flag. Đây là lý do thực tế để cân nhắc WireGuard so với OpenVPN trước khi chọn tunnel cho một VPS giá rẻ.
Cách kiểm tra VPS có AES-NI hay không
Kernel sao chép các bit tính năng CPUID vào /proc/cpuinfo, nên chỉ cần một lệnh grep là có câu trả lời.
grep -m1 -o '\baes\b' /proc/cpuinfo
lscpu | grep -i -o '\baes\b'Nếu một trong hai lệnh in ra aes, CPU đang thông báo rằng guest này hỗ trợ AES-NI. Nếu không in gì, CPU không hỗ trợ. lscpu đọc cùng các flag, nên hai lệnh luôn cho kết quả giống nhau. Dùng lệnh nào đã được cài đặt cũng được.
Bây giờ hãy xem host cho biết bạn đang chạy trên CPU nào.
grep -m1 'model name' /proc/cpuinfoChuỗi model thực như Intel(R) Xeon(R) Gold 6338 CPU @ 2.00GHz hoặc AMD EPYC 7443P 24-Core Processor cho biết host chuyển model CPU vật lý cho bạn. QEMU Virtual CPU version 2.5+ hoặc Common KVM processor cho biết đang có cơ chế khác, và đó là trường hợp cần tìm hiểu.
Vì sao flag bị thiếu dù CPU vật lý có hỗ trợ
CPUID là instruction mà chương trình dùng để hỏi CPU hỗ trợ những gì. Trong virtual machine, CPUID luôn bị trap đến hypervisor, nên hypervisor quyết định thông tin mà guest nhận được. Hầu hết panel đều hiển thị quyết định này dưới dạng CPU model của guest. qemu64 và kvm64 là các model baseline generic, và không model nào có AES-NI hoặc SSSE3 trong feature set. Vì vậy, guest không thấy flag aes ngay cả khi host vật lý là một EPYC đời mới. VPS là guest chạy trên phần cứng của bên khác, nên mọi feature mà nó báo đều là quyết định được đưa ra ở một lớp bên trên. Nếu cách phân lớp này còn mới với bạn, hãy bắt đầu từ VPS là gì.
Host cố ý chọn model generic vì live migration giữa các máy có CPU khác nhau chỉ hoạt động khi guest chưa từng được thông báo về một feature mà máy đích không có. Bạn là bên chịu chi phí. Kernel và bản OpenSSL của bạn đều đọc CPUID đã bị mask một lần khi khởi động. Sau đó, cả hai chọn code path chậm trong suốt vòng đời của process.
Cách sửa ở nguồn là một thiết lập phía host: -cpu host theo thuật ngữ QEMU, một named model có AES-NI, hoặc thêm +aes rõ ràng vào model. Bạn không thể thiết lập những tùy chọn đó từ bên trong guest. Mở support ticket hoặc chọn plan có hypervisor truyền nguyên CPU model xuống guest là giải pháp lâu dài.
Đo chênh lệch bằng openssl speed
Không nên mặc định tin vào benchmark đã công bố. Hãy chạy cipher mà chính server của bạn thực sự thương lượng.
openssl version
openssl speed -evp aes-128-gcmDòng kết quả có nhãn AES-128-GCM và cho biết throughput ở sáu kích thước block, tính theo đơn vị 1000 byte mỗi giây. Hãy đọc cột 8192 byte để đánh giá truyền dữ liệu lớn, vì cột 16 byte bị chi phối bởi overhead của từng lần gọi và không cho biết gì về tốc độ tải file.
Bây giờ chạy lại cùng lệnh với AES-NI và PCLMULQDQ bị tắt trong phần mềm:
OPENSSL_ia32cap="~0x200000200000000" openssl speed -evp aes-128-gcmGiá trị đó lấy từ tài liệu về capability vector của chính OpenSSL. ~ ở đầu có nghĩa là “xóa các bit này”. Bit 57 là AES-NI và bit 33 là PCLMULQDQ, nên 0x200000200000000 chỉ định chính xác hai bit đó và không thêm bit nào khác. Nếu giá trị thứ hai thấp hơn nhiều so với giá trị thứ nhất, máy của bạn có AES-NI hoạt động bình thường và bạn đã hoàn tất. Nếu hai giá trị bằng nhau, OpenSSL vốn đã dùng software path, vì flag đó không tồn tại để bị xóa.
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
}
]Đó là các số liệu tham khảo đã công bố cho một core x86 hiện đại ở khoảng 3.4 GHz, không phải kết quả đo từ một host cụ thể. Hãy xem chúng như một xu hướng. Hardware path chạy ở khoảng 0.7 cycles mỗi byte, còn software fallback chạy ở khoảng 11.0, tương đương khoảng 4,850 MB/s so với 310 MB/s trên một core. Hai lệnh ở trên mới tạo ra con số mô tả chính xác server của bạn. Cách kiểm tra này cũng áp dụng cho các phần khác của máy, vì vậy hãy kết hợp với cách benchmark VPS có thể lặp lại trước khi đưa ra kết luận về một gói dịch vụ.
Bật lại các bit bằng OPENSSL_ia32cap
Đây là phần thường khiến mọi người bất ngờ. Các instruction AES-NI không cần quyền đặc biệt và hypervisor không chặn chúng. Chỉ có CPUID bị chặn. Vì vậy, host có thể báo với guest rằng AES-NI không tồn tại, trong khi AESENC vẫn thực thi native ở tốc độ tối đa. Software bỏ qua fast path vì đã hỏi CPUID và nhận được câu trả lời sai. Bản thân instruction chưa bao giờ ngừng hoạt động.
OpenSSL cho phép bạn trả lời thay cho CPU. Một giá trị hexadecimal thuần trong OPENSSL_ia32cap sẽ ghi đè capability vector thay vì mask nó.
OPENSSL_ia32cap="0x0200020207000000" openssl speed -evp aes-128-gcmNếu lần chạy này nhanh hơn nhiều lần so với lần chạy thông thường, silicon có AES-NI và host đang ẩn nó. Trước hết, đây là cách chẩn đoán. Với OpenSSL, nó cũng đồng thời là cách khắc phục.
Cách tạo giá trị hex đó
Logical vector đầu tiên đóng gói CPUID leaf 1 EDX vào 32 bit thấp và leaf 1 ECX vào 32 bit cao. Ở nửa thấp, bit 24 là FXSR, bit 25 là SSE và bit 26 là SSE2, tạo thành 0x07000000. Ở nửa cao, bit 33 là PCLMULQDQ, bit 41 là SSSE3 và bit 57 là AES-NI, tạo thành 0x02000202. Ghép lại sẽ được 0x0200020207000000. SSSE3 nằm trong danh sách vì GHASH dựa trên PCLMULQDQ của OpenSSL dùng pshufb để đổi thứ tự byte, và generic guest CPU model ẩn SSSE3 cùng với AES-NI.
Có 2 cảnh báo, và bạn có thể chủ động kích hoạt cả hai.
Chỉ đặt vector đầu tiên sẽ để các vector phía sau bằng 0, từ đó tắt các code path AVX2 và AVX-512. Đây là chủ ý trong trường hợp này. Không được cố bật các bit AVX trên guest đã bị mask, vì các thanh ghi AVX cần operating system bật extended state trong XCR0, nhưng kernel của bạn đã từ chối làm việc đó dựa trên CPUID bị mask tương tự. Sau đó, instruction được mã hóa bằng VEX sẽ gây undefined-opcode fault và process sẽ dừng.
Bật AES-NI trên core thực sự không hỗ trợ nó sẽ làm process dừng ngay lập tức:
Illegal instruction (core dumped)Đó là AESENC gây ra undefined-opcode fault, vì core đó không có instruction tương ứng để thực thi. Một số firmware của server cũng có thể tắt AES-NI trong hardware cho đến lần reset tiếp theo, và triệu chứng sẽ giống hệt. Trong cả hai trường hợp, giải pháp là dùng host khác, không phải đổi environment variable.
Để giữ override này cho một service chạy lâu dài, hãy dùng 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 EnvironmentLệnh cuối cùng phải in lại biến cho bạn. Hãy hiểu rõ hệ quả: nếu server đó được migrate sang host có CPU thực sự không hỗ trợ AES-NI, nginx sẽ dừng do illegal instruction ngay khi nhận TLS connection đầu tiên. Hãy ghi việc này vào runbook, hoặc hoàn toàn không dùng override trong production và chỉ dùng nó để chứng minh nguyên nhân khi mở ticket.
Override này không sửa được gì
OPENSSL_ia32cap chỉ được OpenSSL đọc và không ảnh hưởng đến phần mềm nào khác. Mỗi phần mềm còn lại tự phát hiện tính năng và không bao giờ đọc biến đó.
Kernel là trường hợp quan trọng. dm-crypt và LUKS sử dụng kernel crypto API, còn module aesni_intel sẽ từ chối load khi thiếu bit tính năng của CPU:
modprobe: ERROR: could not insert 'aesni_intel': No such deviceKhông có biến nào ở user space cho việc này. Kernel đọc CPUID một lần khi boot và giữ nguyên quyết định đó cho đến khi bạn reboot trên host khác. Vì vậy, encrypted volume vẫn dùng software cipher, bất kể OpenSSL đang làm gì. Hãy đo đúng hiệu năng thực tế:
sudo cryptsetup benchmark -c aes-xts -s 256Dòng aes-xts 256b đạt hàng nghìn MiB/s khi có hardware AES và chỉ ở mức vài trăm MiB/s khi không có. Các language runtime tự phát hiện tính năng, trong đó có Go và Java, cũng không bị ảnh hưởng. crypto/aes của Go kiểm tra CPUID trực tiếp và âm thầm dùng implementation phần mềm constant-time khi bit này bị tắt. Nếu service xử lý TLS của bạn là một binary Go, biến OpenSSL không thay đổi gì đối với service đó.
Nếu không có AES-NI, hãy ưu tiên ChaCha20
ChaCha20-Poly1305 được thiết kế để chạy nhanh bằng software thuần. Trên CPU không có AES-NI dùng được, nó thường nhanh hơn AES-GCM khá nhiều. Vì vậy, lựa chọn hợp lý trên máy chủ như vậy là không còn ưu tiên AES.
Với nginx 1.19.4 trở lên, được build với OpenSSL 1.1.1 trở lên:
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 áp dụng cho TLS 1.2. ssl_conf_command Ciphersuites áp dụng cho TLS 1.3. nginx không có directive riêng cho TLS 1.3 mà chuyển chuỗi này thẳng cho OpenSSL và không kiểm tra. Vì vậy, lỗi chính tả ở đó sẽ được chấp nhận mà không báo lỗi. Reload nginx rồi xác nhận client được cung cấp cipher nào:
sudo nginx -t && sudo systemctl reload nginx
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>/dev/null | grep -i cipherKết quả đúng phải có TLS_CHACHA20_POLY1305_SHA256. Trước khi áp dụng thay đổi, chạy openssl speed -evp chacha20-poly1305 bên cạnh lần chạy AES trên cùng máy chủ và dựa vào hai con số đó để quyết định.
Các host ARM dùng bộ mở rộng khác
AES-NI chỉ có trên x86. VPS ARM dùng các bộ mở rộng mã hóa ARMv8, là một tập lệnh riêng nhưng thực hiện cùng chức năng. Trên aarch64, các flag nằm trong Features thay vì flags:
grep -m1 Features /proc/cpuinfoTìm aes và pmull. pmull là thành phần tương đương với PCLMULQDQ trên ARM, và GCM cần nó vì cùng lý do. Biến override của OpenSSL trên ARM là OPENSSL_armcap, với cách bố trí bit riêng được định nghĩa trong crypto/arm_arch.h trong mã nguồn OpenSSL, nên giá trị hex dành cho x86 trong hướng dẫn này không có ý nghĩa trên ARM. Trên thực tế, các core server ARM được bán làm host VPS thường cung cấp các bộ mở rộng này. Vì vậy, lỗi feature bị mask chủ yếu là vấn đề trên x86.
Các dạng lỗi và chuỗi bạn sẽ thấy
Không có aes trong /proc/cpuinfo, và lần chạy ép buộc nhanh hơn nhiều. Host đang che giấu CPUID. Xác nhận tên model là dạng chung, sau đó hỏi nhà cung cấp xem hypervisor của họ cung cấp model CPU guest nào.
Không có aes trong /proc/cpuinfo, và lần chạy ép buộc in ra Illegal instruction. Các instruction thực sự không tồn tại, hoặc firmware đã tắt chúng. Chuyển workload sang host khác.
aes có mặt nhưng throughput vẫn thấp. Kiểm tra xem bạn có đang đọc cột 8192-byte không, đồng thời xác nhận không có tiến trình nào khác đang dùng core đó. Trên gói shared, một noisy neighbour trông hoàn toàn giống CPU thiếu feature cho đến khi bạn chạy test 2 lần vào các thời điểm khác nhau trong ngày.
Lần chạy đã che giấu và lần chạy bình thường cho cùng một con số. OpenSSL vốn đã sử dụng software path. Kết quả đó chính là phát hiện cần ghi nhận, không phải lỗi trong bài test.
Nested virtualisation làm thay đổi kết quả ở một cấp bên dưới. Guest nằm trong một guest sẽ nhận CPUID theo lựa chọn của lớp trung gian. AES-NI rất dễ bị mất ở đó mà không ai nhận ra. Nếu bạn chạy máy ảo lồng nhau trên VPS, hãy kiểm tra flag bên trong inner guest cũng như trên máy bạn đã thuê.
FAQ
Vì sao VPS của tôi không có cờ aes trong /proc/cpuinfo?
Vì hypervisor đang cung cấp một model CPU guest chung. qemu64 và kvm64 không có AES-NI trong tập tính năng, nên CPUID báo tính năng này không tồn tại dù bộ xử lý vật lý là loại nào. Host làm vậy để có thể migrate guest đang chạy giữa các máy có CPU khác nhau. Chạy grep -m1 'model name' /proc/cpuinfo: chuỗi như QEMU Virtual CPU version 2.5+ hoặc Common KVM processor là dấu hiệu cho thấy điều đó, còn chuỗi model Xeon hoặc EPYC thật nghĩa là model CPU được pass-through và cờ này thực sự không có trên silicon.
OPENSSL_ia32cap có thực sự bật AES-NI không, hay chỉ giả lập?
Nó bật các instruction thật. AES-NI là các instruction không yêu cầu quyền đặc biệt và hypervisor không trap chúng, nên AESENC thực thi native bất kể CPUID báo gì. Chỉ instruction CPUID bị intercept. Đặt OPENSSL_ia32cap thành một giá trị hexadecimal thuần sẽ thay thế kết quả OpenSSL nhận được từ CPUID, vì vậy OpenSSL chọn code path phần cứng và phần cứng chạy code đó ở tốc độ tối đa. Nếu silicon thực sự không có các instruction này, process sẽ dừng với Illegal instruction (core dumped) ngay lần đầu thực hiện thao tác AES.
Override này có tăng tốc volume LUKS được mã hóa không?
Không. OPENSSL_ia32cap được OpenSSL đọc và không được thành phần nào khác đọc. LUKS và dm-crypt dùng kernel crypto API, trong đó module aesni_intel không load được với modprobe: ERROR: could not insert 'aesni_intel': No such device khi bit tính năng bị tắt. Kernel đọc CPUID lúc boot và không có biến user-space nào thay đổi được việc đó. Đo số liệu thực tế bằng sudo cryptsetup benchmark -c aes-xts -s 256 rồi so sánh dòng aes-xts 256b với một host có báo cờ này.
Cờ AES-NI bị thiếu có làm WireGuard chậm không?
Không. WireGuard dùng ChaCha20-Poly1305 cho toàn bộ data và không dùng AES instruction, nên throughput giống nhau trên host bị mask và host không bị mask. OpenVPN và IPsec được cấu hình với AES-GCM sẽ giảm throughput trên host không có AES-NI. Vì vậy, 2 tunnel trên cùng một VPS có thể hoạt động rất khác nhau. Bạn nên biết điều này trước khi quy lỗi cho network.
Làm sao kiểm tra AES-NI trên VPS ARM?
CPU ARM không có AES-NI. Chúng có các extension mã hóa ARMv8, thực hiện cùng chức năng bằng các instruction khác. Chạy grep -m1 Features /proc/cpuinfo và tìm aes và pmull, vì aarch64 liệt kê chúng dưới Features thay vì flags. Giá trị OPENSSL_ia32cap của x86 không có ý nghĩa trên ARM. Biến tương đương của OpenSSL trên đó là OPENSSL_armcap, còn layout bit được định nghĩa trong crypto/arm_arch.h trong source của OpenSSL.