Contents

dev-initely.me

TODO Hardening AUR builds: paru, devtools chroot, and what’s still not covered archlinuxsecurityaurdraft

Recently a malicious AUR package used a npm dependency (atomic-lockfile) to compromise systems. What was insidious about the attack was that it was more indirect than installing a simple backdoor: the PKGBUILD looked clean, instead the payload was in a transitive npm dependency.

That means it was very hard to detect (I certainly do not check all AUR dependencies) and it ran silently during the install phase. The incident was a good opportunity to have a closer look at my own setup.

I covered the incident and how to audit your system for infection in a previous post.

The problem with yay (and most AUR helpers)

Before this attack I honestly did not think too much about installing packages from AUR. I have been using yay for the last couple of years, everything was working fine. This incident made me look a bit closer; here is what I found.

One of the main problems: when you install a package via yay (or other AUR package managers), the PKGBUILD’s build(), package(), and install() functions execute directly on your system as your user. A malicious hook can read ~/.ssh, exfiltrate credentials, curl from a malicious server and even drop a persistence backdoor on your system. All without root.

Obviously you are not helpless: yay has a diff review feature which can help to catch obvious attacks before they run. Emphasis on obvious. But this is a manual process that I all too often skip completely. And sophisticated supply chain attacks usually pass casual PKGBUILD review anyway.

How to mitigate (some of) issues: paru vs yay

The deeper issue: build-time and install-time code runs with the same privileges as your desktop session and can therefore exfiltrate sensitive data from your system. In my former article I have written about ways to mitigate this: inspect and minimize your foreign packages, use official repos where possible. Make a habit out of inspecting the PKGBUILD before installation.

However, none of this would have helped in the latest attack. What has me most worried is having a permanently compromised system. So after doing some research I have decided to switch my AUR package manager from yay to paru.

paru is a drop-in yay replacement written in Rust. For basic usage the difference is minimal — paru -Syu works exactly like yay -Syu. However, there are some meaningful differences:

  • paru shows PKGBUILDs before building foreign dependencies by default
  • paru has native makechrootpkg integration
  • paru introduces a local pacman repository AUR packages

The first point means you have to read the PKGBUILD before installation, paru will not let you continue otherwise. However, the real kicker are points 2 and 3.

So let’s have a closer look.

Using chroot for AUR packages

Now the paranoid way to go about this would be to actually build AUR packages in a separate virtual machine and then transfer the built package onto the live system. I decided early on that this would cause too much friction for my setup and have decided against doing so.

I have decided on an approach that imho offers a good balance between increased control and security on the one hand and usability on the other hand.

A chroot (change root) in Linux is a separate and isolated root environment, a so-called “chroot jail”. Processes run in these environments are restricted to that environment and cannot access files outside that environment.

The nice thing about paru is that it offers chroot integration under the hood without having to set up everything manually via devtools and/or aurutils. If you set paru to use chroot, the cascade looks as follows:

paru -S <package>
  └── makechrootpkg          (from devtools)
        └── systemd-nspawn   (kernel namespace isolation)
              └── PKGBUILD runs here, against /home/aurbuild/root

When you start a build, makechrootpkg creates a overlay on top of a clean base chroot at /home/aurbuild/root. The PKGBUILD runs inside that container. If a malicious install hook tries to write to ~/.config/systemd/user/, it writes into the overlay.

This overlay is discarded after the build. The built .pkg.tar.zst gets transferred into the local pacman repo at ~/paru-repo/, and pacman installs it from there onto the live host.

Now, a caveat: the chroot shares the host kernel, so it can still make syscalls and extract secrets this way, only the filesystem is isolated. It can still exfiltrate secrets, i.e. via accessing kernel keyrings.

paru actually goes one step further here by using systemd-nspawn which can isolate package builds from the network. So if a build snoops on your secrets, it would not be able to phone home.

However, I have found no easy option to do so using paru. You would have to pack the makechrootpkg directly and then make sure to repatch it after devtools updates. However, there is a trade-off: packages that fetch other dependencies during build time (Go modules, npm packages) would fail.

Setting it up

So in order to set this up, here is what I did:

  1. First install devtools from the official repos
  2. Then bootstrap paru from source with makepkg -si
  3. sudo mkarchroot /home/aurbuild/root base-devel to initialise the base image
  4. Declare the local pacman repo in /etc/pacman.conf.
  5. ~/.config/paru/paru.conf with LocalRepo, Chroot = /home/aurbuild, and CleanAfter

Step-by-step:

# 1. Install devtools
sudo pacman -S devtools

# 2. Bootstrap paru from source (live system; one-time exception)
git clone https://aur.archlinux.org/paru.git && cd paru && makepkg -si

# 3. Initialise the chroot base image (~1 GB — pick a path with space)
sudo mkarchroot /home/aurbuild/root base-devel

# 4. Create the local repo directory
mkdir ~/paru-repo

For step 4, add this to /etc/pacman.conf before [core]:

[paru-repo]
  SigLevel = Optional TrustAll
  Server = file:///home/YOUR_USER/paru-repo

For step 5, ~/.config/paru/paru.conf:

[options]
LocalRepo
Chroot = /home/aurbuild
CleanAfter

BottomUp
NewsOnUpgrade
RemoveMake = ask
CombinedUpgrade
UpgradeMenu
PgpFetch

LocalRepo tells paru to use [paru-repo] as the build output target. Chroot points at the base image directory. CleanAfter discards the overlay when the build finishes.

One additional benefit: I ran into a full root partition every now and then. So I have changed the default chroot path from /var/lib/aurbuild/ to /home via ChrootDir which is essential for me.

One catch that I ran into and that produced cascading failures: mkarchroot copies your host’s /etc/pacman.conf into the chroot verbatim. Any IgnorePkg entries on the host level dependency (for me it was fontconfig) will be ignored in the chroot as well.

Everything that depends on it fails to build inside the chroot. The error messages won’t mention IgnorePkg at all. The solution is to either comment out IgnorePkg once while building the base root or delete the offending package from your live system.

After any chroot rebuild, verify:

grep IgnorePkg /home/aurbuild/root/etc/pacman.conf

Remove any entries that don’t make sense inside a clean build environment. The chroot should build against current, unmodified packages regardless of what you’ve pinned on the host.

After switching from yay, rebuild all existing AUR packages through the chroot:

paru -Syu --rebuild all

This takes a while on a large AUR footprint. After running it once normal upgrades should go through the chroot automatically.

Daily workflow

Upgrading everything:

paru -Syu

Installing a new package:

paru -S somepackage

What this actually protects against

The chroot isolates the build and install phase. Concretely:

  • Malicious install() hooks writing to your home directory: blocked
  • Post-install scripts dropping persistence into ~/.config/systemd/user/: blocked
  • Build scripts reading ~/.ssh or ~/.gnupg: blocked (those paths don’t exist in the chroot)

What it does not protect:

  • Network access during builds: by default makechrootpkg has outbound internet access. A malicious PKGBUILD can still exfiltrate data during build() — it just can’t write to your local filesystem. Closing this requires passing network isolation flags to nspawn, which afaik has not yet been implemented. There might be solutions to doing this via pacman hooks and patching makchrootpkg directly. I have not done this yet.
  • Binary packages at runtime: -bin packages ship pre-compiled binaries. The chroot protects the install phase, but the binary itself runs on your live system with full user privileges after installation. These are a different threat model entirely.
  • The paru bootstrap itself: paru was built on the live system with makepkg -si. This is a one-time exception but it means the tool you now trust to enforce isolation was itself never isolated. Mitigated by reviewing the PKGBUILD manually before building.
  • Kernel exploits: the chroot shares the host kernel. A PKGBUILD that exploits a kernel vulnerability could escape. However, I consider this risk rather theoretical for a personal computer setup.

The chroot is a meaningful improvement over plain yay — build scripts can no longer touch the live system. But it’s one layer, not a complete answer.