AMD Framework 13 with Nix, Secure Boot, & home-manager

Contents

I have spent the last week installing and configuring a Framework 13 laptop. My first attempt was to use NixOS (since that’s my distro of choice) but the Framework keyboard wasn’t detected by either the minimal or graphical NixOS iso. So I went with Arch Linux and installed Nix + Home-Manager to manage most of my system.

Hardware

I built the hardware, meaning my Framework 13 had last-mile assembly conducted by the end-user. This gives a better sense of control & repairability and lets you customize the machine a little more, and it only takes a few minutes to assemble.

I read complaints online about build quality but I am happy to report that I haven’t personally noticed anything bothersome besides wishing the bezel were aluminum (a very minor thing).

Processor & GPU

The processor and GPU are AMD. I prefer AMD. Also the Radeon 860M can be used with ollama.

$ cat /proc/cpuinfo | grep '^model name' | head -n1
model name      : AMD Ryzen AI 7 350 w/ Radeon 860M

This means that I have the AMD Pod Security Processor, AMD’s parallel to the Intel Management Enginge. The downside is, obviously, these secondary processors are often vulnerable and have privileged access to system hardware. The upside is this provides functionality such as Secure Boot.

As far as I know, it is not currently possible to use CoreBoot on the latest AMD Framework 13, but experimental support is in progress for the previous gen of AMD CPUs, leaving me hopeful that we will see it here eventually.

However, the default firmware supports Secure Boot with custom keys, which is good enough for me at the moment.

Trackpad

The trackpad impressed me. From what I hear, Apple has a patent on trackpads where the full tactile buttonpress can happen over the full Trackpad surface. So this trackpad lets you tap anywhere but the top half-centimeter or so. Not a bad compromise.

In my home-manager config for sway, I used this to get closer to Apple’s trackpad settings.

# framework laptop touchpad
"2362:628:PIXA3854:00_093A:0274_Touchpad" = {
  scroll_factor = "0.5";
  accel_profile = "adaptive";
  pointer_accel = "-0.1";
  dwt = "enabled";
  click_method = "clickfinger";
};

Keyboard

The keyboard is good quality. I went with the blank keycaps for style points and to encourage memorizing my keyboard layout, which I’m happy with after some initial hurdles learning how the F-keys function.

Fingerprint reader

The power button is also a fingerprint reader! It works with out-of-box drivers and seldom fails to register my fingerprint. That’s all I could ask for, really.

Nix vs Arch

As mentioned earlier, NixOS doesn’t work on this machine—the keyboard drivers fail to load. I went with Arch Linux and all hardware functions out of the box. I’m happy with this. Then I install Nix (pacman -S nix) and wrote a Flake for my home-manager configuration, reusing and improving configs from my NixOS desktop.

I had to come up with a dividing line between Nix and Arch, since in many cases they can replace components of one another. I ended up with home-manager managing nearly all of my homedir and pacman or interactive configuration for the remainder.

OS Configuration

Partitioning and Encryption

The system has a TPM so I’ve been able to do some interesting things, like configure the TPM to store my dm-crypt keys.

I use a configuration where /boot and /boot/efi are on separate physical partitions and then there’s a giant /dev/nvme0n1p2 partition housing LUKS. Within LUKS is LVM, with the first vg housing (encrypted) swap and the second divided into /home, /var/ and / btrfs subvolumes.

$ lsblk
NAME          MAJ:MIN RM  SIZE RO TYPE  MOUNTPOINTS
nvme0n1       259:0    0  1.8T  0 disk
├─nvme0n1p1   259:1    0    2G  0 part  /boot
├─nvme0n1p2   259:2    0  1.8T  0 part
│ └─cryptroot 253:0    0  1.8T  0 crypt
│   ├─vg-swap 253:1    0   32G  0 lvm   [SWAP]
│   └─vg-main 253:2    0  1.8T  0 lvm   /home
│                                       /var
│                                       /
└─nvme0n1p3   259:3    0    2G  0 part  /boot/efi

Secure Boot

For Secure Boot I am using systemd-ukify, sbsign, and pacman hooks.

# sbctl status
Installed:      ✓ sbctl is installed
Owner GUID:     9df5ea07-e33e-4361-9c03-51d46b0dd9ed
Setup Mode:     ✓ Disabled
Secure Boot:    ✓ Enabled
Vendor Keys:    none

To get here you need to generate keys:

# sbctl create-keys

This creates keys in /var/lib/sbctl/keys/:

# ls /var/lib/sbctl/keys
db  KEK  PK

Then you reboot, put the secure-boot in setup mode (which allows writing keys), and enroll your keys with sbctl enroll-keys.

sbctl and signing

When you enable secure boot, your bootloader and kernel images will require signing. I ensure this with a pacman hook:

# cat /etc/pacman.d/hooks/90-secureboot-systemd-update.hook
[Trigger]
Operation = Upgrade
Type = Package
Target = systemd # systemd-boot is part of the systemd package

[Action]
Description = Updating and signing systemd-boot on ESP for Secure Boot...
When = PostTransaction
Exec = /usr/local/bin/update-signed-bootloader
NeedsTargets

Which calls a script /update-signed/bootloader1.

The sbctl sign-all signs the files enumerated in /var/lib/sbctl/files.json:

# cat /var/lib/sbctl/files.json
{
    "/boot/efi/EFI/BOOT/BOOTX64.EFI": {
        "file": "/boot/efi/EFI/BOOT/BOOTX64.EFI",
        "output_file": "/boot/efi/EFI/BOOT/BOOTX64.EFI"
    },
    "/boot/efi/EFI/Linux/arch-secure.efi": {
        "file": "/boot/efi/EFI/Linux/arch-secure.efi",
        "output_file": "/boot/efi/EFI/Linux/arch-secure.efi"
    },
    "/boot/efi/EFI/Linux/arch-troubleshoot.efi": {
        "file": "/boot/efi/EFI/Linux/arch-troubleshoot.efi",
        "output_file": "/boot/efi/EFI/Linux/arch-troubleshoot.efi"
    },
    "/boot/efi/EFI/systemd/systemd-bootx64.efi": {
        "file": "/boot/efi/EFI/systemd/systemd-bootx64.efi",
        "output_file": "/boot/efi/EFI/systemd/systemd-bootx64.efi"
    }
}

Files will be added here if you sign them manually ever. So you can edit the files.json database with, for example, sbctl sign -s /boot/efi/EFI/Linux/arch-troubleshoot.efi.

ukify

I’m using ukify to generate unified kernel images with an embedded initramfs and kernel image. This is done via a file /etc/pacman.d/hooks/95-sbctl-ukify:

[Trigger]
Operation = Upgrade
Type = Package
Target = linux
Target = amd-ucode # Add other relevant packages like 'systemd' if its updates affect initramfs build

[Action]
Description = Rebuilding UKIs and signing with sbctl...
When = PostTransaction
Exec = /usr/local/bin/rebuild-ukis-for-sbctl.sh
NeedsTargets

This calls a script /usr/local/bin/rebuild-ukis-for-sbctl.sh2.

TPM decryption

Once you have a standard dm-crypt/LUKS configuration you can enroll it in the TPM. First ensure you have a TPM.

# ls /dev/tpm*

Then you can enroll the device with:

# systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0,4,6,7 /dev/nvme0n1p2

You’ll note I specify --tpm2-pcrs=0,4,6,7 there. I do that because I want to measure the platform-code, boot-loader-code, host-platform, and secure-boot-policy and only release keys from the TPM if all match. This is an agressive config; most people only pick 7.

You’ll see the TPM PCR unlock attached to our drive:

# cryptsetup luksDump /dev/nvme0n1p2 | grep pcr | head
  0: systemd-tpm2
        tpm2-hash-pcrs:   7
        tpm2-pcr-bank:    sha256
        tpm2-pubkey:
        tpm2-pubkey-pcrs:
        tpm2-primary-alg: ecc

For more on how to configure this, see ArchWiki: Encrypting an entire System#Luks on a partition with TPM2 and Secure Boot.

Note that the TPM will only grant access to the decryption key if the Secure Boot chain does not show signs of tampering.

Fingerprint Unlock

First install fprint with pacman. Then add it to /etc/pam.d/system-auth:

# diff -c10 /etc/pam.d/system-auth.bk /etc/pam.d/system-auth
*** /etc/pam.d/system-auth.bk   2025-05-01 04:34:54.821598714 -0400
--- /etc/pam.d/system-auth      2025-04-29 23:22:27.302944835 -0400
***************
*** 1,16 ****
--- 1,17 ----
  #%PAM-1.0

  auth       required                    pam_faillock.so      preauth
  # Optionally use requisite above if you do not want to prompt for the password
  # on locked accounts.
  -auth      [success=2 default=ignore]  pam_systemd_home.so
+ auth       sufficient                  pam_fprintd.so
  auth       [success=1 default=bad]     pam_unix.so          try_first_pass nullok
  auth       [default=die]               pam_faillock.so      authfail
  auth       optional                    pam_permit.so
  auth       required                    pam_env.so
  auth       required                    pam_faillock.so      authsucc
  # If you drop the above call to pam_faillock.so the lock will be done also
  # on non-consecutive authentication failures.

  -account   [success=1 default=ignore]  pam_systemd_home.so
  account    required                    pam_unix.so

Now use fprintd-enroll (optionally with -f right-middle-finger and so forth) to enroll your fingers, which are stored in /var/lib/fprint. You can now login and sudo with your fingerprint.

Home Configuration

I now use nix with home-manager to manage my homedir, similar to my NixOS machine.

home-manager entrypoint

I have moved to Nix Flakes for my home-manager entrypoint. Flakes are still new to me, but they solve reproducibility issues present with regular Nix.

{
  description = "My full system config (Arch + Home Manager)";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, home-manager, ... }: let
    system = "x86_64-linux";
    username = builtins.getEnv "USER";
    homeDirectory = builtins.getEnv "HOME";

    secrets = {
      # ...
    };
  in {
    homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration {
      pkgs = import nixpkgs {
        inherit system;
      };
      modules = [
        ../home-manager/arch.nix
        {
          programs.home-manager.enable = true;
          home.stateVersion = "24.11";
        }
      ];
      extraSpecialArgs = {
        inherit system;
        secrets = secrets;
        username = username;
        homeDirectory = homeDirectory;
      };
    };
  };
}

I know that, in principle, I should have multiple entrypoints here and condition them based on the machine (so I use the same Flake for Darwin and Arch). I’m not there yet.

I run my home-manager setup with this:

#!/usr/bin/env bash

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )

. "$SCRIPT_DIR/secrets.sh"
export NIXPKGS_ALLOW_UNFREE=1
nix run github:nix-community/home-manager -- switch --impure --flake "${SCRIPT_DIR}"

It requires --impure because it takes env vars as inputs, such as USER, HOME, USER_EMAIL. I don’t want my user email cleartext in my dotfiles (though in practice, people can steal it from GitHub).

ollama

I’m currently using nix-shell to run ollama. I had a hell of a time getting this to work with the Radeon 860M so here are my steps.

First, use rocminfo to get some magic variables:

$ nix-shell -p "rocmPackages.rocminfo" --run "rocminfo" | grep "gfx"
  Name:                    gfx1152
        Name:                    amdgcn-amd-amdhsa--gfx1152

Now set the env vars with the magic make rocm work values, the first of which is taken from rocminfo:

#!/usr/bin/env bash

OLLAMA_PACKAGE=ollama-rocm
ENV_VARS='HCC_AMDGPU_DEVICES=gfx1152 HSA_OVERRIDE_GFX_VERSION="11.0.0" ROCR_VISIBLE_DEVICES=0'
COMMAND="$ENV_VARS ollama serve"
echo "running '${COMMAND}'"
echo nix-shell -p "${OLLAMA_PACKAGE}" --command "${COMMAND}"

If it works, you will see output lines like this, indicating rocm is working:

time=2025-05-06T23:21:04.985-04:00 level=INFO source=types.go:130 msg="inference compute" id=0 library=rocm variant="" compute=gfx1152 driver=0.0 name=1002:1114 total="8.0 GiB" available="7.9 GiB"

If you get an error, like msg=unsupported Radeon iGPU detected skipping" id=0 total="512.0 MiB" then this means you need to reboot to your bootloader and change the iGPU settings to allocate static vRAM to your GPU. Sorry. I gave 8 GiB to mine, which is stolen permanently from RAM.


  1. /usr/local/bin/update-signed-bootloader:

    #!/usr/bin/env bash
    
    set -e
    
    ESP_PATH="/boot/efi"
    SYSTEMD_BOOT_SRC="/usr/lib/systemd/boot/efi/systemd-bootx64.efi"
    SYSTEMD_BOOT_DEST_MAIN="${ESP_PATH}/EFI/systemd/systemd-bootx64.efi"
    SYSTEMD_BOOT_DEST_FALLBACK="${ESP_PATH}/EFI/BOOT/BOOTX64.EFI"
    
    echo "Updating systemd-boot on ESP..."
    # Ensure destination directories exist
    mkdir -p "${ESP_PATH}/EFI/systemd"
    mkdir -p "${ESP_PATH}/EFI/BOOT"
    
    # Copy the new bootloader files to the ESP
    cp "${SYSTEMD_BOOT_SRC}" "${SYSTEMD_BOOT_DEST_MAIN}"
    cp "${SYSTEMD_BOOT_SRC}" "${SYSTEMD_BOOT_DEST_FALLBACK}"
    echo "Successfully copied systemd-boot to ESP."
    
    echo "Running sbctl sign-all to update signatures for bootloader..."
    # sbctl sign-all will detect that the files on the ESP have changed
    # (because their checksums are now different after the copy)
    # and re-sign them if they are tracked in its database.
    sbctl sign-all -g
    
    # Alternatively, for more explicit signing of just these files:
    # sbctl sign "${SYSTEMD_BOOT_DEST_MAIN}"
    # sbctl sign "${SYSTEMD_BOOT_DEST_FALLBACK}"
    
    echo "Bootloader update and signing process complete."
    ↩︎
  2. /usr/local/bin/rebuild-ukis-for-sbctl.sh:

    #!/bin/bash
    set -e
    
    LUKS_UUID_VALUE=$(sudo blkid -s UUID -o value /dev/nvme0n1p2) # Or hardcode
    SECURE_UKI_PATH="/boot/efi/EFI/Linux/arch-secure.efi"
    TROUBLESHOOT_UKI_PATH="/boot/efi/EFI/Linux/arch-troubleshoot.efi"
    
    echo "Rebuilding ${SECURE_UKI_PATH}..."
    # Ensure your ukify/systemd-ukify command is correct here
    ukify build \
        --linux /boot/vmlinuz-linux \
        --initrd /boot/initramfs-linux.img \
        --microcode /boot/amd-ucode.img \
        --os-release /etc/os-release \
        --cmdline "${COMMON_CMDLINE} rd.luks.options=${LUKS_UUID_VALUE}=tpm2-device=auto" \
        --output "${SECURE_UKI_PATH}"
    
    echo "Rebuilding ${TROUBLESHOOT_UKI_PATH}..."
    ukify build \
        --linux /boot/vmlinuz-linux \
        --initrd /boot/initramfs-linux-fallback.img \
        --microcode /boot/amd-ucode.img \
        --os-release /etc/os-release \
        --cmdline "${COMMON_CMDLINE}" \
        --output "${TROUBLESHOOT_UKI_PATH}"
    
    echo "Running sbctl sign-all to update signatures..."
    # This assumes SECURE_UKI_PATH and TROUBLESHOOT_UKI_PATH have been added to sbctl's database
    # via `sbctl sign -s ...` at least once.
    # If not, or for explicit control, use:
    # sbctl sign "${SECURE_UKI_PATH}"
    # sbctl sign "${TROUBLESHOOT_UKI_PATH}"
    sbctl sign-all
    
    echo "UKIs rebuilt and signed."
    echo "------------------------------------------------------------------------------------"
    echo "IMPORTANT REMINDER:"
    echo "If the kernel or related files for '${SECURE_UKI_PATH}' changed,"
    echo "you MUST re-enroll the TPM key after the next reboot (enter password once manually):"
    echo "  sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p2"
    echo "  sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0,6,7 /dev/nvme0n1p2"
    echo "------------------------------------------------------------------------------------"
    ↩︎