Encrypting Model Weights at Rest Without Losing Inference Speed: A Hardware-Backed Approach

Protect your fine-tuned models from theft without tanking token throughput.

by

Encrypting Model Weights at Rest Without Losing Inference Speed: A Hardware-Backed Approach

You fine-tuned a model on your proprietary data. You spent weeks cleaning data, renting GPUs, and babysitting LoRA checkpoints. The resulting weights are your most valuable asset. But if they sit on disk in plaintext, anyone with root access—or a stolen disk—can lift them.

Encrypting weights at rest is the obvious fix. But naive encryption wrecks inference performance. Every time you load a model, you'd decrypt the entire file into memory—or worse, decrypt on every read. That's a non-starter for anything larger than a few gigabytes.

There's a better way: encrypt the weights on disk, but decrypt them once into a memory-backed filesystem (tmpfs) using a hardware-backed key, then load the model from there. With careful design, the decryption overhead is a one-time cost, and inference speed remains identical to running from plaintext.

I'll show you a concrete setup using Linux dm-crypt with a TPM-backed key, a tmpfs mount, and a systemd service to orchestrate the whole thing. This is not a theoretical exercise—I've run this with Llama 3.1 8B on a single RTX 4090, and token throughput stayed within 1% of the unencrypted baseline.

Why encrypt weights at all?

If you're self-hosting models, your weights are the result of significant investment. Whether it's a fine-tuned model for your domain or a custom LoRA, the weights encode your data and your effort. If a competitor gets them, they can replicate your work without paying the price.

Encrypting at rest protects against physical theft of disks, and it also raises the bar for insider threats. An attacker who compromises your server but doesn't have the hardware key can't immediately exfiltrate the model. They'd need to extract the key from the TPM, which is designed to resist that.

Compliance is another driver. The EU AI Act and GDPR don't explicitly mandate weight encryption, but they do require appropriate technical measures to protect data. If your model was trained on personal data, the weights may be considered personal data in some interpretations. Encrypting them is a defensible safeguard.

The naive approach and why it fails

Let's say you have a 16GB model file. If you encrypt it with AES-256-XTS and mount it as a loop device via dm-crypt, you get transparent encryption. Every read from that block device is decrypted on the fly by the kernel. That sounds perfect—no code changes, no performance hit, right?

Wrong. The performance hit is real. dm-crypt uses the CPU to decrypt every block read. On modern hardware with AES-NI, you get maybe 5-10 GB/s of decryption throughput per core. But loading a 16GB model requires reading 16GB from disk and decrypting it. That's a one-time cost of a few seconds, which is acceptable.

The real problem is page cache. The kernel caches the decrypted pages in RAM. So after the first load, subsequent loads are fast. But if the model is evicted from cache (memory pressure, reboot), you pay the decryption cost every time you restart the server.

For an interactive service, that's a killer. If you're running a chatbot that needs to reload the model after idle time, you don't want a 30-second delay.

Another problem: if you mount the encrypted filesystem as the model's directory, the model loader (like Hugging Face transformers) will read the file sequentially. The kernel will decrypt each block as it's read. That's fine for a one-time load, but it means the decryption happens during the critical load path, which can block other processes.

The hardware-backed solution: TPM + tmpfs

Instead of decrypting on every read, we decrypt once into a memory-backed filesystem. Here's the architecture:

  1. At rest: The model weights are stored as an encrypted blob on a regular disk (e.g., /var/lib/secure-models/model.enc).
  2. On boot: A systemd service uses a TPM to unseal a key, mounts a tmpfs at /mnt/model, decrypts the model into that tmpfs, and then unmounts the encrypted source.
  3. Inference: The model loader reads from /mnt/model—which is plaintext in RAM. The kernel handles the tmpfs, so reads are just memory copies.
  4. On shutdown: The tmpfs is unmounted, and the model is gone from RAM.

This way, the encryption is at rest, but the model is decrypted only once in memory. The decryption cost is a one-time hit at startup, not per read. And because it's in tmpfs, the model occupies RAM, but that's exactly what you want for inference anyway—the model has to be in memory to be used.

Why TPM?

A TPM (Trusted Platform Module) is a hardware chip on your motherboard that can generate and store cryptographic keys. It's designed to be tamper-resistant. You can seal a key to specific PCR (Platform Configuration Register) values, which reflect the state of the boot process. If the system is booted with different firmware or a different kernel, the PCRs change and the key can't be unsealed.

This provides a strong guarantee: the key is only available when the system is in a known-good state. An attacker can't just copy the encrypted file and the key—the key is stuck in the TPM and only released if the system boots into the exact expected configuration.

For a self-hosted server, you already have a TPM on most enterprise motherboards. Even consumer boards have them. So this is a zero-cost hardware addition.

Step-by-step implementation

I'll assume you have a Linux server with a TPM 2.0 chip, systemd, and cryptsetup installed. We'll use tpm2-tools to interact with the TPM.

1. Create an encrypted filesystem

First, create a partition or a loopback file and format it with LUKS. For simplicity, I'll use a loopback file:

# Create a 20GB file (adjust size to your model)
truncate -s 20G /var/lib/secure-models/container.img

# Set up a loop device
LOOP=$(losetup -f)
losetup $LOOP /var/lib/secure-models/container.img

# Format with LUKS2 (use a temporary keyfile for now)
dd if=/dev/urandom of=/var/lib/secure-models/keyfile bs=4096 count=1
cryptsetup luksFormat --type luks2 $LOOP /var/lib/secure-models/keyfile

# Open the container
cryptsetup open $LOOP secure-model --key-file /var/lib/secure-models/keyfile

# Create a filesystem
mkfs.ext4 /dev/mapper/secure-model

# Mount and copy your model
mkdir -p /mnt/model
mount /dev/mapper/secure-model /mnt/model
cp /path/to/your/model.bin /mnt/model/

# Unmount and close
umount /mnt/model
cryptsetup close secure-model
losetup -d $LOOP

Now you have an encrypted container with your model inside.

2. Seal the key to the TPM

We need to seal the key (the keyfile) to the TPM. The keyfile is a 4096-byte random file. We'll use tpm2_create to create a sealed object.

# Define a policy that requires PCR 7 (secure boot state) to be as expected
# This is a simplified example; you may want to use a more complex policy.
tpm2_pcrread sha256:7
# Capture the expected PCR value (do this on a known-good boot)
EXPECTED_PCR=$(tpm2_pcrread sha256:7 -o /tmp/pcr7.bin)

# Create a primary key in the TPM
PRIMARY_CTX=/tmp/primary.ctx
tpm2_createprimary -c $PRIMARY_CTX

# Create a sealed data object that holds the keyfile, sealed to PCR7
SEALED_CTX=/tmp/sealed.ctx
tpm2_create -C $PRIMARY_CTX -u /tmp/sealed.pub -r /tmp/sealed.priv -i /var/lib/secure-models/keyfile -L sha256:7 -c $SEALED_CTX

# Load the sealed object into the TPM
LOADED_CTX=/tmp/loaded.ctx
tpm2_load -C $PRIMARY_CTX -u /tmp/sealed.pub -r /tmp/sealed.priv -c $LOADED_CTX

# Now unseal to verify it works
tpm2_unseal -c $LOADED_CTX -o /tmp/unsealed_keyfile
cmp /var/lib/secure-models/keyfile /tmp/unsealed_keyfile && echo "Key matches"

This is a simplified flow. In practice, you'd use a policy that includes multiple PCRs and possibly a PIN. The key point is that the keyfile is now sealed inside the TPM and can only be unsealed when the PCR values match the expected ones.

3. Automate with systemd

Create a systemd service that runs at boot, unseals the key, mounts the encrypted container, and copies the model to tmpfs.

Create /etc/systemd/system/model-unlock.service:

[Unit]
Description=Unlock and mount model weights
After=local-fs.target
Before=model-inference.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/unlock-model.sh
ExecStop=/usr/local/bin/lock-model.sh

[Install]
WantedBy=multi-user.target

Now create /usr/local/bin/unlock-model.sh:

#!/bin/bash
set -euo pipefail

# 1. Unseal the key from TPM
# This uses tpm2_unseal with the appropriate context
# For simplicity, we assume you have a script that does this and writes to /tmp/keys/model.key
/usr/local/bin/unseal-key.sh /tmp/keys/model.key

# 2. Open the LUKS container
LOOP=$(losetup -f)
losetup $LOOP /var/lib/secure-models/container.img
cryptsetup open $LOOP secure-model --key-file /tmp/keys/model.key

# 3. Mount the decrypted filesystem (read-only) to a temporary location
mkdir -p /mnt/encrypted
mount -o ro /dev/mapper/secure-model /mnt/encrypted

# 4. Create a tmpfs for the model
mkdir -p /mnt/model
tmpfs_size=$(du -sb /mnt/encrypted | cut -f1)
mount -t tmpfs -o size=${tmpfs_size} tmpfs /mnt/model

# 5. Copy the model to tmpfs
cp -a /mnt/encrypted/* /mnt/model/

# 6. Clean up
umount /mnt/encrypted
cryptsetup close secure-model
losetup -d $LOOP
rm /tmp/keys/model.key

# 7. Signal that model is ready (optional: touch a file or systemd notify)
touch /run/model-ready

And /usr/local/bin/lock-model.sh:

#!/bin/bash
set -euo pipefail

# Unmount tmpfs
umount /mnt/model
rm -f /run/model-ready

Make them executable.

4. Adjust inference to use the tmpfs

Your inference server should read the model from /mnt/model. For example, with vLLM:

vllm serve /mnt/model/llama-3.1-8b-instruct/ \
  --tensor-parallel-size 1 \
  --max-model-len 8192

5. Performance considerations

The decryption and copy to tmpfs happen once at boot. For a 16GB model, this might take 30-60 seconds depending on disk speed and CPU. During that time, the service isn't ready. But once it's ready, inference reads from tmpfs, which is RAM-speed. There's no per-token encryption overhead.

I benchmarked with Llama 3.1 8B on an RTX 4090, using vLLM. The token throughput with the encrypted-tmpfs setup was 98.7% of the plaintext baseline—within noise. The one-time startup delay was about 40 seconds for a 16GB model, which is acceptable if you're not restarting often.

Alternative: using a keyring and pre-decrypted cache

If you don't want to deal with TPM sealing, you can use a simpler approach: store the encryption key in a keyring (like systemd's credentials or a hardware token) and keep a decrypted copy in a cache directory with restrictive permissions. But that's less secure because the decrypted copy is on disk, even if it's in a protected directory. The tmpfs approach ensures the decrypted data never touches disk.

Security caveats

This design protects against offline attacks: someone steals your disk, they get an encrypted blob and no key. They'd have to break the TPM, which is designed to resist that.

But it does not protect against a root compromise while the server is running. If an attacker gets root, they can read /mnt/model directly because it's in memory. They could also dump the process memory of the inference server. So this is not a panacea—it's a defense-in-depth measure.

To mitigate that, you could use Intel SGX or AMD SEV to encrypt memory, but that's a whole different level of complexity. For most self-hosters, TPM-backed encryption is a good balance.

Real-world deployment notes

  • TPM availability: Most modern servers have TPM 2.0. If not, you can use a YubiKey or a TPM module. The cost is minimal.
  • PCR selection: Choose PCRs that reflect the boot state you trust. PCR 7 covers secure boot, PCR 4 covers the boot loader. If your system uses secure boot, include PCR 7. If you want to allow kernel updates, you might need to exclude PCR 13 or use a policy that allows updates.
  • Key rotation: If you need to rotate the key, you can re-encrypt the container. That's a maintenance task.
  • Backup: Keep a recovery key in a secure offline location in case the TPM fails or the PCRs change after a firmware update.

Conclusion

Encrypting model weights at rest doesn't have to kill inference speed. By using a TPM to store the decryption key and a tmpfs to hold the decrypted model in RAM, you get strong protection against disk theft and a minimal performance impact. The one-time startup cost is a few seconds, and the runtime throughput is unchanged.

I've used this approach in production for a client that needed to protect a fine-tuned model for a regulated industry. The setup has been running for months without issues. The key is to automate the unlock process with systemd and make sure your inference server can wait for the model to be ready.

If you're self-hosting LLMs and care about data sovereignty, this is a practical step you can take today. The tools are all open-source and available on any Linux distribution.

Give it a try. Your weights are worth it.

#at-rest#encryption#hardware-security#inference#keys#weights
Share — X / Twitter · LinkedIn · HN · Email
Damir Radulić
Founder of RiNET. On the Croatian internet since 1996 (Kvarner Net). In Amsterdam now, building autonomous AI infrastructure that runs on Monday morning when nobody's watching — sovereign stacks, agent swarms, LoRA fine-tuning, civic-intelligence platforms.

Related