Skip to content

Instantly share code, notes, and snippets.

@falconindy
Last active August 4, 2026 06:55
Show Gist options
  • Select an option

  • Save falconindy/42f9cd86c8c84e0713e4613a82c878c2 to your computer and use it in GitHub Desktop.

Select an option

Save falconindy/42f9cd86c8c84e0713e4613a82c878c2 to your computer and use it in GitHub Desktop.
Thoughts on a more trustworthy AUR

A More Trustworthy AUR

Background

The Arch User Repository (AUR) is a community-driven collection of package recipes — not binaries. Each AUR package is a git repository containing, at minimum, a PKGBUILD (a bash script describing how to fetch, build, and package software) and a .SRCINFO (machine-readable metadata derived from the PKGBUILD). Users — or, more often, AUR helpers like paru and yay — clone these repos, inspect the PKGBUILD, and run makepkg to build and install the result. Authentication for pushing is done over SSH using keys registered to an AUR account, and a push is only accepted if the tip commit contains a valid, matching PKGBUILD and .SRCINFO.

This is fundamentally different from the official repositories. Packages in core, extra, and multilib are built and signed by a vetted group of Arch Linux Package Maintainers (formerly Trusted Users) and Developers. Getting that status is a deliberate, social process. The AUR, by contrast, is open by default: anyone can register an account, upload a new package, or adopt an orphaned (unmaintained) package, and the change goes live immediately with no human in the loop. That openness is the AUR's greatest strength — it is why nearly anything you can imagine is already packaged — and its greatest liability.

That liability is no longer theoretical. The AUR has seen a string of malware incidents:

  • July 2025: A user uploaded librewolf-fix-bin, firefox-patch-bin, and zen-browser-patched-bin, whose PKGBUILDs pulled and installed the CHAOS remote-access trojan. The packages lived on the AUR for roughly 46 hours before being purged.
  • June 2026 ("Atomic Arch"): A far larger, automated campaign. Attackers systematically adopted orphaned packages and pushed PKGBUILD changes that deployed an infostealer and an eBPF-based rootkit that disguised itself as a kernel thread. Reports put the blast radius at 400+ (by some counts >1,600) packages. Arch staff responded by temporarily freezing account creation, package updates, and adoptions while they cleaned up.

Two patterns stand out across these incidents and should drive the design. First, time-to-detection is measured in tens of hours, during which every user who updates is exposed. Second, the most damaging vector is not novel uploads but the adoption of existing, trusted-looking packages — the attacker inherits a package's reputation and install base for free.

See:

Goals and non-goals

We need to balance the "freedom" that defines the AUR — roughly anyone can contribute — against security, while keeping the operational overhead of running the service as close to zero as it is today. The AUR works partly because nobody has to babysit it; any design that requires standing human effort proportional to AUR traffic is dead on arrival.

Two demonstrably bad outcomes bound the solution space:

  1. Shuttering the AUR (or locking contribution behind official-repo-style vetting). This destroys the thing's reason to exist and pushes users toward even less accountable third-party repos.
  2. Human review of every change. Consider the model used by Chaotic-AUR, which automatically rebuilds AUR packages. After the malware wave it added a trusted-maintainer list: if every maintainer of a package is trusted, updates flow silently; if any maintainer is untrusted and the diff touches more than pkgver/checksums, a pull request is opened for a human to review. That model is sane for Chaotic-AUR's curated subset, but reviewing every non-trivial change across the entire AUR — tens of thousands of packages, thousands of pushes a day — simply does not scale to a volunteer staff.

So the goals are:

  • Cheap by default. The common case (a known-good contributor pushing a routine update) must remain fully automated and instantaneous.
  • Fail closed on risk, not on volume. Human attention is the scarce resource; spend it only on the small slice of changes that actually look risky.
  • Raise the cost of the proven attacks. Specifically the two patterns above: brand-new contributors shipping payloads, and reputation-laundering via adoption of existing packages.
  • No regression for honest, established contributors. A maintainer who has pushed safe updates for years should never notice this system exists.

Non-goals:

  • Catching every malicious change. This is a risk-reduction system, not a proof of safety. We are trading the current "100% auto-merge" posture for one where the risky minority gets a second look.
  • Sandboxing or statically proving PKGBUILDs safe. PKGBUILDs are arbitrary bash that runs makepkg on the user's machine; true containment is the helper's and the user's job. We focus on the publishing gate.
  • Identity verification / KYC. We explicitly do not want to know who contributors are (see Reputation).

Design Ideas

Reputation

The core of the proposal is a reputation system that lets the service decide, per push, whether a contributor is trusted enough to bypass review. High trust means low barriers — the push auto-merges, exactly like today. Low trust means the change may be set aside for occasional human review. Reputation is what makes the whole thing low-effort: it concentrates scrutiny on newcomers and anomalies and leaves the established majority untouched.

Reputation is tied to an SSH key, not to a user account. A contributor earns reputation against the specific key they sign/push with, and that reputation travels with the key.

Pros:

  • No identity required. We never need to trust the account system, email verification, or any notion of a "real person." The key is the principal. This keeps the AUR's pseudonymous, low-friction character intact.
  • Account takeover is not key takeover. The June 2026 attack leaned on adopting existing packages through account-level actions. If trust lives in the key, compromising someone's web account or password doesn't hand the attacker their accrued reputation — they'd need the private key itself.
  • Clean, narrow trust boundary. "Did this exact key sign this exact commit?" is a crisp, cryptographic question with no fuzzy account-recovery edges.

Cons:

  • Key loss = reputation loss. Lose the key (disk failure, no backup) and you start over. We need a sane story for rotation: e.g. allow a key to vouch for its successor, transferring some fraction of reputation, so honest rotation doesn't nuke years of standing.
  • Multi-device / multi-key friction. Contributors who push from several machines either share a key (bad hygiene) or split reputation across keys. A key-grouping or vouching mechanism mitigates this but adds complexity.
  • Key sharing / sale. Reputation that lives in a portable secret can be handed off or sold. A high-reputation key is now an asset an attacker might buy rather than steal. Slow decay and behavioral anomaly checks (below) partially counter this, but it is a real trade-off versus account-bound trust.

How reputation moves:

  • Gained when a commit is accepted to main and survives without being flagged/reverted — i.e. good, safe pushes slowly build standing. Gains should be sublinear (you can't farm reputation by spamming trivial commits) and ideally weighted by how many people actually consume the package.
  • Neutral when a commit is held for review. Being sent to review is not a punishment — false positives must be cheap, or contributors will route around the system. You simply wait.
  • Lost when an in-review commit is rejected by a reviewer as malicious or abusive. This is the sharp edge: getting caught trying to ship something bad costs you dearly, ideally enough to wipe out a long history of small gains, so reputation can't be slowly farmed and then cashed in on a single payload.
  • Decays extremely slowly with time. Slow decay (not fast) is deliberate: it means a long-dormant, high-trust key that resurfaces is slightly staler and re-enters with marginally more scrutiny — a hedge against an old key being taken over — without punishing legitimately low-frequency maintainers who touch a package once a year.

Open questions: the exact gain/decay curves; whether reputation should be global to a key or per-package (a key trusted on its own packages but treated as a newcomer when it shows up on a package it has never touched — which directly addresses the adoption-laundering vector); and how reputation interacts with co-maintainership.

Tying reputation to per-change trust

Reputation and the per-change risk score are not two independent systems — they are the two halves of a single decision, and the design only coheres when they're expressed in the same units. The cleanest framing:

  • A change is scored for how much trust it demands. A routine pkgver bump with matching checksums demands almost none. Adopting an orphaned package and simultaneously adding an install script that fetches a binary from a new host demands a great deal. The Review Heuristics section is, in effect, a function that maps a diff to "how much trust would I need to wave this through unseen?"
  • A key carries reputation: the amount of trust it has banked through a history of safe, accepted changes.
  • The gate is a single comparison: does the contributor's reputation cover the trust the change demands? If supplied ≥ demanded, the change auto-merges. If not, it routes to human review.

Stated the way you'd say it out loud: reputation entitles a key to perform lower-trust operations on its own. Trust is spent on risk, not consumed like a currency — covering a risky change doesn't deduct reputation; it just has to be large enough to reach that level of risk in the first place.

The practical consequences fall out directly:

  • A brand-new key has near-zero reputation, so it can only auto-merge the safest class of operation — the trivial, well-shaped update. Anything with the slightest risk signal exceeds its budget and goes to review. This is the desired posture for newcomers without singling them out by name; they're simply capable of less unsupervised action.
  • A veteran key has banked enough reputation that introducing an install script, adding a new source, or making a structural build() change stays under its budget and flows straight to main. The same diff that queues from a newcomer sails through from someone who has earned it.
  • The riskiest operations — say, adopt-and-immediately-rewrite, the Atomic Arch shape — demand so much trust that effectively no realistic reputation covers them, so they always route to review regardless of who pushes. Some operations should simply never be auto-approvable; expressing them as a very high trust demand gives us that ceiling for free, without a separate "always review" list to maintain.

This is why reputation must be earned slowly and lost sharply (above): reputation is the spending limit on unsupervised risk. Letting it accrue cheaply, or survive a rejection, would let an attacker bank a budget and then spend it on a single payload — exactly the laundering motion we're trying to price out.

If reputation is made per-package rather than global, this comparison gets even sharper: trust banked on packages you maintain doesn't pay for risky changes to a package you just adopted, so the demanding "first change after adoption" operation meets a near-empty budget and reliably routes to review.

High Level Commit Flow

The publishing path becomes a gate rather than a pass-through:

  1. A contributor pushes a new commit (signed by their key).
  2. Correctness check. Validate the push the way the AUR does today: the tip commit must contain a PKGBUILD and a .SRCINFO, and the .SRCINFO must be consistent with the PKGBUILD (e.g. regenerated, matching pkgname/pkgver). Malformed pushes are rejected outright, before any trust logic runs.
  3. Trust check. Compute how much trust the change demands (see Review Heuristics) and compare it against the trust the contributor's key has banked (its reputation). See "Tying reputation to per-change trust" — the pass/fail in the next step is exactly this supplied-vs-demanded comparison.
  4. Branch on the result:
    • Pass: the commit is fast-forwarded onto main and is live immediately — identical to today's experience.
    • Fail: the commit is not placed on main. Instead it lands on a separate review branch (e.g. refs/review/<id>) and is enqueued for human review. main continues to serve the last known-good commit, so consumers are never exposed to an unreviewed change.
  5. Review resolution. A reviewer either merges the review branch into main (commit goes live, contributor gains reputation) or rejects it (branch discarded, contributor loses reputation).

Concurrency rule: if a push targets a repo that already has a pending/in-review commit, the push is rejected. This is a deliberate simplification — it sidesteps having to reason about stacked or conflicting review branches and keeps the per-repo state machine to "clean" or "one pending review." The contributor simply waits for the queue to clear and re-pushes. We should make the rejection message explicit about why, so it doesn't read as a generic git failure.

Things to nail down: what a reviewer-facing "merge" does to authorship/signing; whether trivial follow-up fixes during review are allowed (probably not, given the concurrency rule); and how a new package (no existing main) differs from an update in this flow.

Reviewers

Reviewers are the human safety valve, and the design only works if this pool is both competent and effectively free to staff.

  • Reviewers need not be Arch Developers or Package Maintainers. The set of highest-reputation contributors is, almost by definition, the set of people who push the most safe changes — they are active, fluent in PKGBUILDs, and already invested. Drawing reviewers from the top of the reputation distribution makes the role largely self-staffing and self-renewing.
  • A review dashboard surfaces the queue of pending commits: the diff against main, which heuristics fired and how much each cost, the contributor key's reputation and history, and package popularity/blast-radius. The goal is to let a reviewer triage a typical change in seconds, because the heuristics have already done the narrowing.
  • Reviewer actions are merge or reject, with reject feeding back into the contributor's reputation. We likely want lightweight accountability on reviewers too (who approved what), so that a compromised or careless reviewer is itself a detectable, reversible event — without turning review into a bureaucratic process.

Open questions: how reviewers are promoted/demoted (purely reputation threshold, or with a vouch step?); incentives so the queue actually gets serviced; and conflict-of-interest handling (a reviewer shouldn't clear their own key's changes).

Review Heuristics

Routing is decided by a scoring system. Every change starts as presumed good with a baseline score (say 1000). Independent heuristics inspect the commit and subtract points when they match a risk signal. If the resulting score falls below a threshold, the change is routed to human review instead of auto-merging. That deducted total is the "trust demanded" quantity from the previous section — the more points a diff sheds, the more banked reputation a key must have to clear it unsupervised. Equivalently, the effective threshold is set by the contributor's reputation: a veteran's threshold sits low enough that a risky diff still passes, while a newcomer's sits just under 1000 so almost any deduction tips the change into review. Same diff, different outcome, depending on who earned the right to push it.

This design is attractive because it's composable and tunable: heuristics are cheap, independent checks that can be added, weighted, and retired as attack patterns evolve, and the single threshold gives operators one dial to trade false positives (annoyed contributors) against false negatives (escaped malware).

Example signals, several of which map directly to the observed attacks:

  1. A first contribution from a key — no track record at all.
  2. A first contribution after a package changes ownership/adoption — this is the Atomic Arch laundering vector; an orphaned package being adopted and immediately modified by an unknown key is exactly the shape we want to catch.
  3. Sudden introduction of an install script (.install file / *_install hooks), which run code at package install/upgrade/remove time on the user's system — a classic payload location.
  4. New or changed network sourcessource=() URLs pointing at new hosts, raw IPs, URL shorteners, or git/curl fetches added where there were none.
  5. -bin style packages that fetch prebuilt binaries (opaque, unauditable) rather than building from source — the July 2025 packages fit this mold.
  6. Obfuscation / encoding in the PKGBUILD: base64 -d | bash, eval of constructed strings, hex blobs, unusually long one-liners.
  7. Diff shape anomalies: a "version bump" commit that nonetheless rewrites build()/package()/prepare(), or touches far more than pkgver and checksums (mirrors Chaotic-AUR's pkgver-only fast path).
  8. Maintainer/contact churnmaintainer or upstream URL changing in lockstep with a payload-shaped change.

An LLM as the primary heuristic

Rather than hand-maintaining a large rulebook of pattern-matchers, the bulk of the scoring can be delegated to a model that performs textual analysis of the commit diff and emits a goodness/risk judgment. The AUR is an unusually good fit for this: it has existed for a long time and carries an enormous history of both good and bad commits. That history is a ready-made labeled corpus — overwhelmingly-benign routine updates on one side, and the known malicious commits (July 2025, June 2026, and whatever else gets caught going forward) on the other — which means we should be able to train or fine-tune a relatively small, cheap model specialized to "is this PKGBUILD change suspicious?" rather than reaching for a giant general-purpose LLM on every push.

Why this is appealing:

  • Generalization beyond fixed rules. A model can flag a payload shaped slightly differently from anything in the explicit rule list, where a brittle regex would miss it.
  • One cheap inference per push. A small specialized model keeps the gate fast and inexpensive, which matters when it runs on every push.
  • Self-improving. Every reviewer merge/reject decision is a fresh labeled example. The review loop is the training-data pipeline; the model gets better precisely on the changes humans found ambiguous.

Caveats to design around:

  • The model is a scorer, not the judge. It contributes points to the same 0–1000 budget; it does not unilaterally accept or reject. A high-risk model verdict routes to a human, who makes the call.
  • Adversarial pressure. Once contributors know a model gates pushes, some will try to phrase payloads to read as benign. The model's score must be one input among several (reputation, deterministic signals above), and exact behavior shouldn't be fully public.
  • Explainability. A bare score is hard for a reviewer to act on. The model should surface what it found suspicious (which lines, which behavior) so the dashboard can show a reason, not just a number.

Pluggability is non-negotiable

Whatever the mix of model and rules, the heuristic layer must be pluggable. Malware will slip through — the design assumes this — and when it does, the operators need to respond in minutes, not in a model-retraining cycle. Concretely that means:

  • Human override in both directions. An operator can force a specific change (or contributor key) to review regardless of score, and can clear a false-positive that the model is stubbornly flagging.
  • Emergent rule injection. When a gap is found (a new payload pattern slips through), a new deterministic heuristic can be dropped in immediately to catch that shape across all in-flight and future pushes, ahead of and independent of the slower model-retraining loop.
  • Kill switches and thresholds as live config. During an active incident (à la June 2026) operators should be able to globally tighten the threshold — e.g. route all adoptions, or all -bin packages, to review — without a code deploy.

So the model is the high-recall workhorse for the steady state; the pluggable rule/override layer is the fast-reaction patch surface for when reality outmaneuvers the model.

Design notes:

  • Heuristics should be explainable — each match (model or rule) records what it saw and how many points it cost — so reviewers see why something queued and so the system can be audited and improved.
  • Scores and thresholds want to be data-driven: backtest candidate heuristics and model versions against the known malicious commits and against a large sample of known-good history to estimate the false-positive rate before shipping. A heuristic (or model) that would have queued half the AUR is useless no matter how well it catches malware.
  • Treat the scoring internals as adversarial and secret-ish. Publishing exact weights or model behavior invites gaming (e.g. staying one point above threshold). Keeping the categories public for transparency while keeping exact weights private is a reasonable middle ground.

Summary

The proposal keeps the AUR open and the steady state free to operate, and spends human review only where risk concentrates. Three mechanisms interlock: key-bound reputation decides who is trusted, a scoring gate — driven by a small purpose-trained model and backed by a pluggable rule/override layer — decides which changes are risky, and a review branch + dashboard gives humans a cheap way to clear the small risky remainder while feeding every decision back as training data. The design is explicitly aimed at the two behaviors the real-world attacks relied on — unknown contributors shipping payloads, and reputation laundering through package adoption — while leaving the honest, established majority of pushes exactly as frictionless as they are today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment