AES-NI on a VPS: Check and Restore It
Check whether your VPS exposes AES-NI, measure what a masked CPUID costs your AES-GCM throughput, and force the bits back on with OPENSSL_ia32cap.
What AES-NI on a VPS actually buys you
AES-NI on a VPS is a set of six x86 instructions that perform one round of AES (advanced encryption standard) in hardware. If your provider's CPU model hides them, the silicon underneath still has them, but OpenSSL cannot see them and falls back to a software implementation that costs roughly ten times as many cycles per byte. You can check for the feature in one command, measure the gap in two, and often force the fast path back on with one environment variable.
The instructions are AESENC, AESENCLAST, AESDEC, AESDECLAST, AESIMC and AESKEYGENASSIST. Intel shipped them in 2010 and AMD followed, so any server CPU you are likely to rent has the silicon. A companion instruction, PCLMULQDQ, does carry-less multiplication, which is what GCM (Galois/counter mode) needs to build its authentication tag. AES-GCM is fast only when both are available, because the cipher and the tag are separate pieces of work.
Four places on a VPS where this shows up in your monitoring:
- TLS (transport layer security) termination. A web server handing out AES-128-GCM or AES-256-GCM spends most of its bulk-crypto time inside AES.
- Encrypted volumes. LUKS (Linux unified key setup) and dm-crypt run
aes-xtson every read and every write, in the kernel, on the CPU. - AES-based VPN traffic. OpenVPN with
AES-256-GCMand IPsec with AES-GCM both lean on it. - Encrypted backups. Anything that encrypts a stream with AES before it leaves the box pays the same cost.
One common workload is not affected at all. WireGuard uses ChaCha20-Poly1305 for its data and never touches AES, so a self-hosted WireGuard VPN runs at the same speed on a host with the flag masked. That difference is a practical reason to weigh WireGuard against OpenVPN before you pick a tunnel for a cheap VPS.
How to check whether your VPS has AES-NI
The kernel copies the CPUID feature bits into /proc/cpuinfo, so one grep answers the question.
grep -m1 -o '\baes\b' /proc/cpuinfo
lscpu | grep -i -o '\baes\b'Either command printing aes means the CPU advertises AES-NI to this guest. Printing nothing means it does not. lscpu reads the same flags, so the two always agree. Use whichever is installed.
Now look at which CPU the host claims you are running on.
grep -m1 'model name' /proc/cpuinfoA real model string such as Intel(R) Xeon(R) Gold 6338 CPU @ 2.00GHz or AMD EPYC 7443P 24-Core Processor means the host passes the physical CPU model through to you. QEMU Virtual CPU version 2.5+ or Common KVM processor means something else is happening, and that case is the one worth understanding.
Why the flag is missing when the silicon has it
CPUID is the instruction a program uses to ask the CPU what it supports. Inside a virtual machine, CPUID always traps to the hypervisor, so the hypervisor decides what the guest is told. Most panels expose that decision as a guest CPU model. qemu64 and kvm64 are generic baseline models, and neither includes AES-NI or SSSE3 in its feature set, so the guest sees no aes flag even when the physical host is a current EPYC. A VPS is a guest on someone else's hardware, so every feature it reports is a decision made one level up. If that layering is new to you, start with what a VPS is.
Hosts choose a generic model deliberately, because live migration between machines with different processors only works if the guest was never told about a feature the destination lacks. The cost lands on you. Your kernel and your copy of OpenSSL both read that masked CPUID once at startup, and both then pick the slow code path for the life of the process.
The fix at the source is a host-side setting: -cpu host in QEMU terms, a named model that includes AES-NI, or an explicit +aes added to the model. You cannot set any of that from inside the guest. Opening a support ticket, or choosing a plan whose hypervisor passes the CPU model through, is the durable answer.
Measure the gap with openssl speed
Do not take a published benchmark on faith. Run the cipher your own server actually negotiates.
openssl version
openssl speed -evp aes-128-gcmThe result row is labelled AES-128-GCM and gives throughput at six block sizes, in units of 1000 bytes per second. Read the 8192-byte column for bulk transfer, because the 16-byte column is dominated by per-call overhead and tells you nothing about a file download.
Now run the same command with AES-NI and PCLMULQDQ switched off in software:
OPENSSL_ia32cap="~0x200000200000000" openssl speed -evp aes-128-gcmThat value comes from OpenSSL's own capability vector documentation. A leading ~ means "clear these bits". Bit 57 is AES-NI and bit 33 is PCLMULQDQ, so 0x200000200000000 names exactly those two and nothing else. A second number far below the first means your box has working AES-NI and you are finished. Two numbers that match mean OpenSSL was already on the software path, because the flag was never there to clear.
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
}
]Those are representative published figures for a modern x86 core near 3.4 GHz, not a measurement taken from any single host. Read them as a shape. The hardware path runs at roughly 0.7 cycles per byte and the software fallback at roughly 11.0, which works out to about 4,850 MB/s against 310 MB/s on one core. Your two commands above produce the only number that describes your server. The same discipline applies to the rest of the machine, so pair this with a repeatable way to benchmark a VPS before you draw conclusions about a plan.
Force the bits back on with OPENSSL_ia32cap
Here is the part that surprises people. The AES-NI instructions are unprivileged, and the hypervisor does not trap them. Only CPUID is trapped. So a host can tell your guest that AES-NI is absent while AESENC keeps executing natively at full speed. Software skips the fast path because it asked CPUID and got a wrong answer. The instruction itself never stopped working.
OpenSSL lets you answer on the CPU's behalf. A plain hexadecimal value in OPENSSL_ia32cap overwrites the capability vector rather than masking it.
OPENSSL_ia32cap="0x0200020207000000" openssl speed -evp aes-128-gcmIf that run is several times faster than the plain run, the silicon has AES-NI and your host is hiding it. That is a diagnosis first. For OpenSSL, it also happens to be a fix.
How that hex value is built
The first logical vector packs CPUID leaf 1 EDX into the low 32 bits and leaf 1 ECX into the high 32 bits. In the low half, bit 24 is FXSR, bit 25 is SSE and bit 26 is SSE2, giving 0x07000000. In the upper half, bit 33 is PCLMULQDQ, bit 41 is SSSE3 and bit 57 is AES-NI, giving 0x02000202. Put together that is 0x0200020207000000. SSSE3 is in the list because OpenSSL's PCLMULQDQ based GHASH uses pshufb to swap bytes, and a generic guest CPU model hides SSSE3 alongside AES-NI.
Two warnings apply, and you can trigger both on purpose.
Setting only the first vector leaves the later vectors at zero, which turns off the AVX2 and AVX-512 code paths. That is deliberate here. Do not try to force AVX bits on a masked guest, because AVX registers need the operating system to enable extended state in XCR0, and your kernel declined to do that based on the same masked CPUID. A VEX-encoded instruction then raises an undefined-opcode fault and the process dies.
Forcing AES-NI on a core that genuinely lacks it kills the process immediately:
Illegal instruction (core dumped)That is AESENC raising an undefined-opcode fault, because on that core there is no such instruction to execute. Some server firmware can also disable AES-NI in hardware until the next reset, and the symptom is identical. Either way the answer is a different host, not a different environment variable.
To keep the override for a long-running service, use a 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 EnvironmentThe last command should print the variable back to you. Understand what you are signing up for: if that server is ever migrated to a host whose CPU really lacks AES-NI, nginx dies with an illegal instruction on its first TLS connection. Put it in your runbook, or keep the override out of production entirely and use it only to prove the point when you open a ticket.
What the override does not fix
OPENSSL_ia32cap reaches OpenSSL and nothing else. Every other piece of software runs its own feature detection and never reads that variable.
The kernel is the important case. dm-crypt and LUKS use the kernel crypto API, and the aesni_intel module refuses to load when the CPU feature bit is missing:
modprobe: ERROR: could not insert 'aesni_intel': No such deviceThere is no user-space variable for this. The kernel reads CPUID once at boot and that decision stands until you reboot on a different host, so your encrypted volume stays on the software cipher no matter what OpenSSL is doing. Measure what you are actually getting:
sudo cryptsetup benchmark -c aes-xts -s 256The aes-xts 256b row lands in the thousands of MiB/s with hardware AES and in the low hundreds without. Language runtimes with their own detection, Go and Java among them, are equally out of reach. Go's crypto/aes checks CPUID directly and quietly uses its constant-time software implementation when the bit is clear. If the service terminating your TLS is a Go binary, the OpenSSL variable changes nothing for it.
If you cannot get AES-NI, prefer ChaCha20
ChaCha20-Poly1305 was designed to be fast in plain software. On a core without usable AES-NI it usually beats AES-GCM by a wide margin, so the sensible move on such a host is to stop preferring AES.
For nginx 1.19.4 and newer, built against OpenSSL 1.1.1 or newer:
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 covers TLS 1.2. ssl_conf_command Ciphersuites covers TLS 1.3, where nginx has no dedicated directive and passes the string straight to OpenSSL without checking it, so a typo there is silently accepted. Reload and confirm what a client is offered:
sudo nginx -t && sudo systemctl reload nginx
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>/dev/null | grep -i cipherA healthy result names TLS_CHACHA20_POLY1305_SHA256. Before you commit to the change, run openssl speed -evp chacha20-poly1305 next to the AES run on the same box and let the two numbers decide.
ARM hosts use different extensions
AES-NI is x86 only. An ARM VPS uses the ARMv8 cryptographic extensions, a separate instruction set doing the same job. On aarch64 the flags live under Features rather than flags:
grep -m1 Features /proc/cpuinfoLook for aes and pmull. pmull is the ARM counterpart to PCLMULQDQ, and GCM needs it for the same reason. OpenSSL's override variable on ARM is OPENSSL_armcap, with its own bit layout defined in crypto/arm_arch.h in the OpenSSL source, so the x86 hex value in this guide means nothing there. In practice the ARM server cores sold as VPS hosts expose these extensions, so the masked-feature problem is mostly an x86 story.
Failure modes, with the strings you will see
No aes in /proc/cpuinfo, and the forced run is much faster. The host is masking CPUID. Confirm the model name is generic, then ask your provider which guest CPU model their hypervisor presents.
No aes in /proc/cpuinfo, and the forced run prints Illegal instruction. The instructions are genuinely absent, or firmware has disabled them. Move the workload to a different host.
aes is present but throughput is still low. Check that you are reading the 8192-byte column, and that nothing else is using the core. On a shared plan a noisy neighbour looks exactly like a missing CPU feature until you run the test twice at different times of day.
The masked run and the normal run give the same number. OpenSSL was already on the software path. That result is the finding, not a mistake in the test.
Nested virtualisation changes the answer one level down. A guest inside a guest gets whatever CPUID the middle layer chose to pass on, and AES-NI is easy to lose there without noticing. If you run nested virtual machines on a VPS, check the flag inside the inner guest as well as on the machine you rented.
FAQ
Why does my VPS have no aes flag in /proc/cpuinfo?
Because the hypervisor is presenting a generic guest CPU model. qemu64 and kvm64 do not include AES-NI in their feature sets, so CPUID reports it as absent whatever the physical processor is. Hosts do this so a running guest can be migrated between machines with different CPUs. Run grep -m1 'model name' /proc/cpuinfo: a string like QEMU Virtual CPU version 2.5+ or Common KVM processor is the tell, while a real Xeon or EPYC model string means the CPU model is passed through and the flag really is missing from the silicon.
Does OPENSSL_ia32cap really enable AES-NI, or does it only pretend?
It enables the real instructions. AES-NI instructions are unprivileged and the hypervisor never traps them, so AESENC executes natively whatever CPUID reports. Only the CPUID instruction is intercepted. Setting OPENSSL_ia32cap to a plain hexadecimal value replaces the answer OpenSSL got from CPUID, so OpenSSL selects its hardware code path and the hardware then runs it at full speed. If the silicon truly lacks the instructions, the process dies with Illegal instruction (core dumped) on the first AES operation.
Will the override speed up my LUKS encrypted volume?
No. OPENSSL_ia32cap is read by OpenSSL and by nothing else. LUKS and dm-crypt use the kernel crypto API, where the aesni_intel module fails to load with modprobe: ERROR: could not insert 'aesni_intel': No such device when the feature bit is clear. The kernel reads CPUID at boot and no user-space variable changes that. Measure the real figure with sudo cryptsetup benchmark -c aes-xts -s 256 and compare the aes-xts 256b row against a host that reports the flag.
Does a missing AES-NI flag slow down WireGuard?
No. WireGuard uses ChaCha20-Poly1305 for all data and never uses AES instructions, so its throughput is the same on a masked host and an unmasked one. OpenVPN and IPsec configured with AES-GCM do lose throughput on a host without AES-NI. Two tunnels on the same VPS can therefore behave very differently, which is worth knowing before you blame the network.
How do I check for AES-NI on an ARM VPS?
ARM cores do not have AES-NI. They have the ARMv8 cryptographic extensions, which do the same job with different instructions. Run grep -m1 Features /proc/cpuinfo and look for aes and pmull, since aarch64 lists them under Features rather than flags. The x86 OPENSSL_ia32cap value has no meaning on ARM. OpenSSL's equivalent variable there is OPENSSL_armcap, and its bit layout is defined in crypto/arm_arch.h in the OpenSSL source.