Skip to content

Releases: tilas01/arch-guides-dynamic

Security Tools Release (main)

Choose a tag to compare

Refresh arch-security-suite's lockfile after anti-evil-maid gained libc

The suite depends on the five other crates by path, so adding libc to
anti-evil-maid changed the suite's dependency graph too. Its own Cargo.lock
still had the old graph, and the release workflow builds with --locked, so the
job failed with "cannot update the lock file because --locked was passed"
before it compiled a single line.

The website deploy was green on the same commits, which is why this was not
obvious: only the Rust release workflow enforces --locked.

All six lockfiles now verify under `cargo metadata --locked`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Security Tools Release (main)

Choose a tag to compare

anti-ducky: trusted-device enrolment, and one allowlist instead of two

Input Guard grants trust by a vote from an already-trusted keyboard. On a
fresh install there is no such keyboard, so the first one plugged in was the
one it sandboxed — the owner's. There was no way to tell it otherwise.

Adds --enroll (confirms each attached device individually, with a rescan loop
for plugging in more) and --export-whitelist (names one per line). Enrolment
asks about each device separately rather than offering "yes to all", because a
machine that already has something malicious attached should not have it
blessed by a single keypress. --export-whitelist exits non-zero on an empty
list so a caller piping it into a config file cannot silently write an empty
allowlist and lock the owner out.

Also fixes a flag that never existed: the generator emitted
`anti-ducky --approve-current` inside the chroot. clap rejects it, so that line
could only ever fail. Same class of bug as libre-otp's documented --interactive.
Enrolment is now deliberately not run during the install — it is interactive,
and the devices attached while installing are not the devices the machine is
used with. The unit is installed and left stopped, with the enrolment steps in
/etc/motd.d, because enabling the daemon before enrolment is the lockout.

Where usbkill is also selected, it is handed the same list rather than
generating a rival one. Two tools with two different ideas of what is trusted
is how a machine powers itself off over its owner's own keyboard.

The export command deliberately avoids `shred` on the temp file: the destructive
matcher in command-by-command mode keys on \bshred\b, and gating a harmless
post-install step behind typed disk-path confirmation is how people learn to
type past that confirmation.

All 13 gates green: 578 configs, 16,319 assertions, every script through bash -n.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Security Tools Release (main)

Choose a tag to compare

docs: the wiki still said Libre OTP was "enforced through PAM"

The README was corrected for this months ago; the wiki was not. It told readers
the tool is enforced through PAM, which would send them to add
`pam_libre_otp.so` to their SSH auth stack — a module that does not exist,
referencing which can lock them out of the machine. The correction says what was
wrong and what to remove, rather than quietly editing the sentence, because
someone may have acted on it.

It now describes what the tool actually does, including two things that were
implemented but never explained:

**The prompt shows a code and asks for one.** That reads like redundancy and is
not. An ordinary 2FA prompt proves only that *you* hold the secret; it says
nothing about the machine, so a convincing fake prompt harvests a code and you
learn nothing. Showing the code first inverts that — if the number on screen does
not match your authenticator, the thing asking does not hold the secret and you
should not answer it. Worth knowing before setting the display mode to `none`.

**`--double-check` wants two consecutive codes**, so a single shoulder-surfed
code is not enough on its own.

Also adds the authenticator-app guidance the user asked for: Aegis or FreeOTP,
offline and open source. An authenticator that syncs seeds to a vendor's cloud
makes the second factor only as strong as that vendor and their willingness to
refuse a request for the seed — which is most of what a second factor exists to
remove.

The scarecrow entry now documents the three duress PINs with the real flags, the
requirement to name a device before anything can be erased, the header-backup
advice, and the note that all three are verified every attempt so the timing does
not reveal which matched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Security Tools Release (main)

Choose a tag to compare

docs: the wiki still said Libre OTP was "enforced through PAM"

The README was corrected for this months ago; the wiki was not. It told readers
the tool is enforced through PAM, which would send them to add
`pam_libre_otp.so` to their SSH auth stack — a module that does not exist,
referencing which can lock them out of the machine. The correction says what was
wrong and what to remove, rather than quietly editing the sentence, because
someone may have acted on it.

It now describes what the tool actually does, including two things that were
implemented but never explained:

**The prompt shows a code and asks for one.** That reads like redundancy and is
not. An ordinary 2FA prompt proves only that *you* hold the secret; it says
nothing about the machine, so a convincing fake prompt harvests a code and you
learn nothing. Showing the code first inverts that — if the number on screen does
not match your authenticator, the thing asking does not hold the secret and you
should not answer it. Worth knowing before setting the display mode to `none`.

**`--double-check` wants two consecutive codes**, so a single shoulder-surfed
code is not enough on its own.

Also adds the authenticator-app guidance the user asked for: Aegis or FreeOTP,
offline and open source. An authenticator that syncs seeds to a vendor's cloud
makes the second factor only as strong as that vendor and their willingness to
refuse a request for the seed — which is most of what a second factor exists to
remove.

The scarecrow entry now documents the three duress PINs with the real flags, the
requirement to name a device before anything can be erased, the header-backup
advice, and the note that all three are verified every attempt so the timing does
not reveal which matched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Security Tools Release (main)

Choose a tag to compare

feat: choose who answers your DNS, and encrypt the queries

Every site you visit starts with a DNS lookup, and by default that goes to
whoever DHCP nominated — usually the ISP — as plaintext on port 53. HTTPS does
not help: the encryption starts after the name has been resolved. So the ISP,
and anyone watching the connection, sees every domain you ask for.

The generator had a *caching resolver* choice (systemd-resolved / unbound /
dnscrypt-proxy) but neither front end let you pick the upstream. Now there is a
`dns_provider` question offering Quad9, Mullvad, Cloudflare, dns0.eu, AdGuard,
or whatever DHCP hands out, with each provider's addresses and DoT hostname
taken from its own documentation.

Two things this got right only after being checked:

**The certificate hostname is actually pinned now.** The first version wrote
`DNS=9.9.9.9` with `DNSOverTLS=yes` and a comment claiming the hostname was
pinned. It was not — that needs `9.9.9.9#dns.quad9.net`, and without it the
query is encrypted but *not authenticated*, so anyone able to answer on port 853
is trusted. On a hostile network that is the network, which is most of the
threat this is meant to remove. A comment describing protection the config does
not provide is worse than no comment.

**`FallbackDNS=` is set empty on purpose.** systemd's compiled-in fallbacks are
other providers, so leaving the default silently leaks a share of queries to
whoever those are — defeating the point of choosing one.

The option descriptions say what each provider *claims* rather than asserting
it: "states it does not log" is a published policy, not something verifiable
from the machine. Cloudflare carries a note that one very large company seeing a
large share of the internet's DNS is a centralisation trade-off, not a technical
fault. And the guide closes by saying what this does not do — it does nothing
about the IP you then connect to, or about SNI without ECH, so an observer can
often still infer the site. A real improvement over plaintext; not anonymity.

New wiki section `#encrypted-dns` covering the providers, why the `#hostname`
suffix matters, and the same limits, citing the Arch Wiki's systemd-resolved and
Domain name resolution pages as the authority.

Caught by tests/wiki-targets.mjs: the question first pointed at
`wiki:'network-hardening'`, which is a *document* in docs/, not an anchor in
wiki.html — so right-clicking it would have gone nowhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Security Tools Release (main)

Choose a tag to compare

v2026.08.01-0439-main

feat: the three duress PINs are selectable in the walkthrough and car…

Security Tools Release (main)

Choose a tag to compare

feat(scarecrow): three LUKS PINs — duress, decoy, and both at once

Scarecrow had one password. It raised a silent signal and opened a decoy
session, and it did not wipe anything, despite the docs and the wiki implying a
duress mechanism. Now there are three, each optional and independent:

  duress        erases the LUKS header of the configured device, silently
  decoy         a believable working session, erases nothing
  duress+decoy  erases the header AND opens the decoy, so the data is gone and
                the screen still shows a working system

Design decisions worth keeping:

- **All three are verified on every attempt, with no early exit.** Stopping at
  the first match would make the time taken depend on which slot matched, and
  three Argon2 verifications at m=64MiB are slow enough for that to be
  measurable. Two extra verifications on a machine about to be wiped is a cheap
  price for not leaking which PIN was used.
- **Most destructive interpretation wins** if one password is enrolled in more
  than one slot. Under duress is the wrong moment to resolve ambiguity in favour
  of doing less.
- **Nothing is erased until --set-duress-device names a device.** Refusing to
  guess is the point: erasing the header of a device nobody named is
  unrecoverable and might be the wrong disk. The value is re-validated at wipe
  time as well, so a mangled config file cannot become arbitrary arguments.
- **The wipe is the header, not the disk.** luksErase takes milliseconds and is
  irreversible; overwriting a whole disk cannot finish before someone notices,
  which defeats the purpose.
- **Every path is silent**, including the failures. No progress, no confirmation,
  no error — not on success, not when cryptsetup is missing, not when no device
  is set. An error mentioning cryptsetup on a login screen says exactly what was
  attempted. A duress-only match prints `Login incorrect`, the same as a wrong
  password, because a disk that will not unlock reads as a forgotten passphrase.

The README now states the limits plainly rather than implying more than the tool
does: the ciphertext survives on the platter and this is useless against someone
who imaged the disk first; it cannot help with a machine seized powered-on; the
decoy is a session, not disk-level deniability; and anyone who knows this tool
exists knows a duress PIN might. The deniability is that they cannot tell which
password you handed over — not that the mechanism is a secret, since it is in a
public repository.

fix(ci): --locked was being satisfied by a lockfile the previous step wrote

Three lockfiles — anti-ducky, arch-security-suite and scarecrow — had drifted
from their Cargo.toml: dependencies were declared and never resolved into the
lock. `cargo metadata --locked` fails on all three at the parent commit.

`cargo build --release --locked` should have caught that. It did not, because
the clippy step runs first *without* --locked, silently re-resolves, and
rewrites the lock — so --locked was checking a file generated moments earlier
rather than the one in the repository, and the released binaries were not built
from the committed lock at all. That is exactly the guarantee --locked exists to
provide, and it was not being provided.

Locks regenerated, and every cargo invocation before the build now carries
--locked, with an explicit `cargo metadata --locked` first so a stale lock fails
loudly at a step whose name says what is wrong.

Verified locally: `cargo clippy --target x86_64-unknown-linux-gnu --all-targets
--locked -- -D warnings` is clean for scarecrow, libre-otp, anti-ducky and
kernel-watcher. anti-evil-maid and arch-security-suite cannot be checked from
this machine — libudev-sys needs a system library that is not present when
cross-checking from Windows — so CI remains the check for those two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Security Tools Release (main)

Choose a tag to compare

fix: two pages called renderMarkdown while loading no markdown.js

The Live Editor's preview button switched panes and rendered an empty div. The
Dynamic Generator's live preview was blank for the same reason: both pages call
`window.renderMarkdown`, and neither loaded `markdown.js`.

The calls are guarded — `if (typeof window.renderMarkdown === 'function')` —
which is correct defensive style and is exactly why nobody noticed. There was no
error, no warning, and no visible difference between "the renderer is missing"
and "your document is empty".

It got that way from my own bulk edit that added the script tag to every page,
skipping any file that already contained the string "markdown.js". Both files
mention markdown.js *in a comment*, so the guard matched and the tag was never
added. A grep for a filename is not a test that the file is loaded.

Two gates, because the class of bug matters more than the instance:

- tests/live-editor.mjs asserts on behaviour: put markdown in the box, press the
  button, and check that a heading, bold text, list items, a table and a
  highlighted code block actually came out, that no raw `**` survived, and that
  the toggle round-trips. An empty preview is the specific thing it catches.
- tests/page-runtime-audit.mjs now asserts, on every page, that renderMarkdown,
  renderMarkdownInto, highlightAll and setHighlightedCode are all callable.
  Checking the global rather than the <script> tag is deliberate: a tag that
  404s, and a file with a SyntaxError, both leave the global undefined and both
  should fail.

Also renames the walkthrough's mode button to "One command at a time" — "One at
a time" said nothing about what it switched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Security Tools Release (main)

Choose a tag to compare

v2026.07.31-1437-main

fix(walkthrough): the command list had no background, so it sat on to…

Security Tools Release (main)

Choose a tag to compare

feat(generator): wallpapers, closing the last gap in all four routes

The walkthrough, the markdown guide and the bash script gained the Dusky
wallpapers in dca7ad5; the Dynamic Generator did not, so one of the four routes
to an install could not offer them. Three selects now mirror the walkthrough's
three questions exactly — same ids, same option values — and the emission is
shared by both the script path and the markdown path through one
emitWallpapers() helper, so the two cannot describe different downloads.

Identical ids and values on both sides is deliberate: it means
config-translate.js needs no value map for them, and a value map is precisely
where the two front ends get a chance to disagree. That mapping is now in place,
which it could not be before — the translate test asserts the layer never
invents an answer, so a mapping pointing at a generator control that did not
exist would have been a lie the test was right to reject.

The panel shows the resulting counts rather than the percentage: "75%" does not
tell anyone how many files they are about to download. It reads "38 images — 28
dark and 10 light, roughly 6 MB", recomputed live, and the count and split
controls disable themselves when they cannot apply.

Two things worth recording about placement. The fetch goes in the same block
that already curls the cheatsheets, not into the first-boot rollover script:
that heredoc is quoted (`cat << 'ROLLOVER'`), so $u1 would have been written
literally rather than expanded, and the wallpapers would have landed in a
directory called '$u1'. And the counts are capped per folder, because a 50/50
split of "all" cannot draw 135 light images from a folder holding 134.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>