Skip to main content

max / alloy

Ship a Secret Service provider, a password manager, and satty's real repo Three findings from the 2026-07-29 feature audit, all verified against a built image rather than argued from package names. Nothing in the image claimed org.freedesktop.secrets. The Rust keyring crate and Tauri's credential plumbing both resolve to the Secret Service on Linux, so GoingsOn and Balanced Breakfast had nowhere to put a credential and synckit-client had nowhere to persist its E2EE key: the distro breaking its own author's software, surfacing as a runtime error four layers from the cause. gnome-keyring-pam is a separate package, and that is the part worth recording. Fedora's /etc/pam.d/greetd already carries both unlock stanzas and both are `-` prefixed, so PAM skips a module it cannot load without a word in any log. The provider alone would have shipped a working Secret Service and a keyring behind a second password prompt, with the config that was meant to prevent it sitting in place looking correct. That is the third arrival of one defect class, after the emoji alias naming an absent font and the swayosd unit at a path udev does not read, so it gets an assertion: provider claimed, module loadable, stanzas still present. No /etc/pam.d file ships from here; greetd owns that stack and it already says what Alloy needs. gopass lands with it, on the age backend and synced by git, per the 2026-07-30 decision. The assertion runs against a throwaway HOME because gopass writes a config on any invocation including --help, so the plain form baked /root/.config/gopass/config into the image: a guard that leaves state behind changes what it was checking. satty needed no COPR. It resolves from Terra at 0.21.1, which the build's own dnf output now confirms; with terra absent, repoquery returns nothing for satty at all. The block claimed it was "expected in ublue-os/staging" and the install worked the whole time because Terra was answering a question nobody had asked it. Recording the wrong repo costs nothing until someone drops the one actually carrying the package. Both projects are added to credits.toml, whose header makes a Containerfile package missing from it a quiet wrongness on the install page. Licenses read from upstream rather than rpm: gopass is MIT, and rpm reports the Go vendor soup. Open and not decided here: whether a fresh install provisions the age identity or leaves it manual (GoingsOn alloy c113feec).
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 19:33 UTC
Signed with PGP, not checked
Commit: df2eb6e6bab294f2ab908af69b460f5a4e406782
Parent: 147e293
3 files changed, +165 insertions, -3 deletions
M Containerfile +123 -3
@@ -235,7 +235,7 @@
235 235 https://pkgs.tailscale.com/stable/fedora/tailscale.repo
236 236
237 237 # Terra — Fedora repo for parts of the Wayland ecosystem (swww,
238 - # starship, and some others). Does not carry Rust binaries like
238 + # starship, satty, and some others). Does not carry Rust binaries like
239 239 # nushell or yazi; COPRs below handle those.
240 240 # https://terra.fyralabs.com/
241 241 RUN dnf install -y --nogpgcheck \
@@ -266,8 +266,16 @@
266 266 # yazi binary
267 267 RUN dnf copr enable -y varlad/yazi
268 268
269 - # satty (screenshot annotator) is expected in ublue-os/staging;
270 - # if it turns out to need its own COPR, add here.
269 + # satty (screenshot annotator) needs no COPR. It resolves from Terra, at
270 + # 0.21.1-1.fc43 — checked 2026-07-30 by repoquery against the full repo
271 + # set this file configures, which is the only way to answer it, since the
272 + # question is which of four enabled repos wins.
273 + #
274 + # This block used to say satty "is expected in ublue-os/staging", and it
275 + # was not there: repoquery with terra absent returns nothing for satty at
276 + # all. The install line worked the whole time because Terra was answering
277 + # a question nobody had asked it. Recording the wrong repo costs nothing
278 + # until someone drops the one that was actually carrying the package.
271 279
272 280 # =====================================================================
273 281 # Fonts pulled from upstream releases (not cleanly packaged in Fedora):
@@ -487,6 +495,50 @@
487 495 # and is also the install path for ungoogled-chromium.
488 496 podman \
489 497 flatpak \
498 + # Secrets (docs/STACK.md#secrets). Two different jobs, and only the
499 + # first of them is about Max's own logins.
500 + #
501 + # gnome-keyring is the Secret Service provider, and it is here
502 + # because its absence broke Make Creative's own software on Make
503 + # Creative's own distro. Nothing in the image registered
504 + # org.freedesktop.secrets, and the Rust `keyring` crate and Tauri's
505 + # credential plumbing both resolve to it on Linux, so GoingsOn and
506 + # Balanced Breakfast had nowhere to put a credential and
507 + # synckit-client had nowhere to persist its E2EE key. That surfaces
508 + # as a confusing runtime error inside an app, four layers from the
509 + # cause.
510 + #
511 + # gnome-keyring-pam is a SEPARATE package and not a weak dependency
512 + # of the one above, which matters more than it looks. Fedora's
513 + # /etc/pam.d/greetd already carries the two stanzas that unlock the
514 + # keyring with the login password, and both are `-` prefixed:
515 + #
516 + # -auth optional pam_gnome_keyring.so
517 + # -session optional pam_gnome_keyring.so auto_start
518 + #
519 + # A leading `-` tells PAM to skip a module it cannot load, without a
520 + # word in any log. So the provider alone gets you a Secret Service
521 + # that works and a keyring locked behind a second password prompt
522 + # nobody chose, and the config that was supposed to prevent that is
523 + # sitting right there looking correct. Same defect class as the
524 + # emoji alias and the swayosd unit path: configuration that is
525 + # correct about something absent. Both packages or neither.
526 + #
527 + # Nothing here ships a /etc/pam.d file. greetd owns that one and the
528 + # stanzas are already in it, so the fix is a package rather than a
529 + # config edit; the assertion further down is what keeps that true.
530 + #
531 + # KeePassXC also provides the interface and was rejected for
532 + # dragging Qt into a ratatui/egui design language, not on
533 + # capability. systemd-creds is unrelated and stays: service
534 + # credentials for daemons, no interactive component.
535 + gnome-keyring \
536 + gnome-keyring-pam \
537 + # gopass is the password manager, on its age backend, synced by git
538 + # (decided 2026-07-30). Fedora main carries it at 1.16.1; no COPR.
539 + # It needs no gpg on the age backend, and the git half is the `git`
540 + # already named below.
541 + gopass \
490 542 # -----------------------------------------------------------------
491 543 # Session prerequisites — not curated picks. See the header above.
492 544 # fedora-bootc is a server base; everything in this group is
@@ -778,6 +830,74 @@
778 830 RUN grep -q -- '--cmd alloy-session' /etc/greetd/config.toml \
779 831 || { echo "greetd does not launch the session wrapper; the skeleton would never be applied" >&2; exit 1; }
780 832
833 + # =====================================================================
834 + # The Secret Service — assert the provider is reachable and its unlock
835 + # is not inert.
836 + #
837 + # Three things have to hold together for an app to store a credential
838 + # without prompting, and every one of them fails quietly on its own.
839 + #
840 + # 1. Something has to claim org.freedesktop.secrets. That is a D-Bus
841 + # activation file, not a running daemon: the bus starts
842 + # gnome-keyring-daemon on the first call. Absent, the `keyring`
843 + # crate returns an error from inside the app, which is where this
844 + # whole task started.
845 + # 2. pam_gnome_keyring.so has to be loadable, or the `-` prefixed
846 + # stanzas in greetd's PAM stack are skipped in silence and the
847 + # keyring stays locked behind a second prompt. The module is in
848 + # gnome-keyring-pam, a separate package; see the install group.
849 + # 3. greetd's PAM stack has to still carry those stanzas. They are
850 + # Fedora's, not Alloy's, so nothing in this repo would notice them
851 + # leaving. Alloy ships no /etc/pam.d file, which is deliberate —
852 + # copying greetd's stack in to add two lines it already has means
853 + # owning a file that upstream keeps changing — and the cost of not
854 + # owning it is exactly this check.
855 + #
856 + # The `auto_start` on the session line is named rather than matched
857 + # loosely: without it the module authenticates against the keyring and
858 + # never starts the daemon, which reads as the unlock working right up
859 + # until an app asks for a secret.
860 + # =====================================================================
861 + RUN set -eux; \
862 + service=/usr/share/dbus-1/services/org.freedesktop.secrets.service; \
863 + [ -f "$service" ] \
864 + || { echo "nothing claims org.freedesktop.secrets; every app using the keyring crate fails at runtime" >&2; exit 1; }; \
865 + grep -q '^Name=org.freedesktop.secrets$' "$service" \
866 + || { echo "$service no longer claims the org.freedesktop.secrets name" >&2; exit 1; }; \
867 + module=$(find /usr/lib64/security /usr/lib/security -name pam_gnome_keyring.so 2>/dev/null | head -n 1); \
868 + [ -n "$module" ] \
869 + || { echo "pam_gnome_keyring.so is absent; greetd's '-' prefixed stanzas would be skipped silently and the keyring would prompt separately" >&2; exit 1; }; \
870 + grep -q '^-*auth .*pam_gnome_keyring\.so' /etc/pam.d/greetd \
871 + || { echo "greetd's PAM stack no longer unlocks the keyring on auth; Alloy would have to ship its own /etc/pam.d/greetd" >&2; exit 1; }; \
872 + grep -q '^-*session .*pam_gnome_keyring\.so.*auto_start' /etc/pam.d/greetd \
873 + || { echo "greetd's PAM stack no longer starts the keyring daemon at session open; secrets would fail after a login that looked fine" >&2; exit 1; }; \
874 + echo "secret service: provider present, $module loadable, greetd unlocks it"
875 +
876 + # gopass, and the one thing about it that can be wrong without saying so.
877 + #
878 + # The store is age-backed rather than GPG-backed (docs/STACK.md#secrets),
879 + # and age support is compiled into gopass rather than shipped as a plugin,
880 + # so a Fedora build with the feature dropped would still install, still
881 + # run, and only fail when a store is initialized. `gopass age --help` is
882 + # the cheapest question that distinguishes the two.
883 + #
884 + # HOME is redirected at a temporary directory and the directory removed,
885 + # which is not tidiness. gopass writes a config file on any invocation,
886 + # including one that only asks for help, so the plain form of this check
887 + # bakes /root/.config/gopass/config into the shipped image: a config for
888 + # an account with no store, authored by a build step, sitting where the
889 + # real one would go the first time anyone runs gopass as root. A guard
890 + # that leaves state behind is a guard that changes what it was checking.
891 + RUN command -v gopass >/dev/null \
892 + || { echo "gopass is missing; docs/STACK.md names it as the password manager" >&2; exit 1; }
893 + RUN set -eux; \
894 + probe=$(mktemp -d); \
895 + HOME="$probe" gopass age --help >/dev/null 2>&1 \
896 + || { rm -rf "$probe"; echo "this gopass build has no age backend; the whole reason it was picked over pass is gone" >&2; exit 1; }; \
897 + rm -rf "$probe"; \
898 + [ ! -e /root/.config/gopass ] \
899 + || { echo "the gopass probe wrote into /root anyway; it would ship in the image" >&2; exit 1; }
900 +
781 901 # =====================================================================
782 902 # The screenshot helper — assert the binds reach it.
783 903 #
@@ -176,6 +176,37 @@
176 176
177 177 Risk to track: distrobox v2 is a Go rewrite, at rc.3 as of 2026-06-29. Its own announcement says v1 remains the production recommendation and that exported binaries and apps must be re-exported after upgrading. The Containerfile pins Fedora's 1.8.2.5. Do not chase the rc; keep a release note ready for whenever the pin moves.
178 178
179 + ## Secrets
180 +
181 + Two jobs that get confused for one. **gnome-keyring** is the API programs call; **gopass** is where a person keeps their logins. Neither substitutes for the other, and Alloy shipped with neither until 2026-07-30.
182 +
183 + ### Secret Service provider: **gnome-keyring**
184 +
185 + **Alloy ships the daemon that claims `org.freedesktop.secrets`, because software Alloy's own author writes depends on it.** The Rust `keyring` crate and Tauri's credential plumbing both resolve to the Secret Service on Linux, so GoingsOn and Balanced Breakfast had nowhere to put a credential and `synckit-client` had nowhere to persist its E2EE key. The failure surfaced as a runtime error inside an app, four layers from the cause. This is the distro breaking its own software, which is the sharpest version of the dogfooding argument.
186 +
187 + **Two packages, `gnome-keyring` and `gnome-keyring-pam`, and the second is the one that is easy to miss.** Fedora's `/etc/pam.d/greetd` already carries `-auth optional pam_gnome_keyring.so` and `-session optional pam_gnome_keyring.so auto_start`. The leading `-` means PAM skips a module it cannot load without logging anything, so installing the provider alone yields a working Secret Service and a keyring locked behind a second password prompt nobody asked for, with the config that was meant to prevent it sitting in place looking correct. That is the same defect class as the emoji alias naming an absent font and the SwayOSD unit at a path udev does not read. The Containerfile asserts all three conditions (provider claimed, module loadable, stanzas still present) rather than trusting them.
188 +
189 + Alloy ships no `/etc/pam.d` file. greetd owns that stack and it already says what Alloy needs; copying it in to add two lines it already has would mean owning a file upstream keeps editing.
190 +
191 + Rejected: **KeePassXC**, which also provides the interface but drags Qt into a ratatui/egui design language. Rejected on that, not on capability. **`systemd-creds`** is not an alternative and stays: it handles service credentials for daemons, with no interactive component and no D-Bus interface for an app to call.
192 +
193 + ### Password manager: **gopass**, age backend, synced by git
194 +
195 + **The deciding factor is key-hierarchy sprawl, not features.** `_private/` is migrating to sops plus age, so an age-backed store reuses an identity that migration already requires. **rbw** against a self-hosted Vaultwarden was the runner-up and is the better answer on mobile and in a browser, but it introduces a second master secret with its own recovery story, and the failure mode of two hierarchies is that backup discipline silently covers one and not the other. **KeePassXC** was rejected for Qt, as above. **`pass`** was rejected for being GPG-centric when the whole direction is away from GPG toward age.
196 +
197 + **git, not Syncthing, even though the store is encrypted.** Syncthing replicating age ciphertext is technically sound, and the confidentiality objection does disappear once everything is encrypted at rest. It is still wrong here: Syncthing resolves a concurrent edit by writing `secret.age.sync-conflict-<timestamp>-<id>` beside the original and carrying on. For a credential store that is two versions of a password, no signal about which is current, and nothing forcing you to look. git turns the same situation into a merge conflict that blocks until resolved. Loud beats silent when the file is what you are about to authenticate with. gopass is also git-native, so Syncthing would mean disabling the mechanism the tool is built around.
198 +
199 + Because the store is ciphertext it can live in the same astra repo as the rest of `_private/`, with no separate remote and no special handling. Not `mnw`: no reason to publish it, and that push is one-way.
200 +
201 + **The age identity never travels with the ciphertext.** Not in the repo it decrypts, not through Syncthing, not in the same channel by any route. Syncing it collapses the scheme into "one compromised device loses everything".
202 +
203 + Accepted costs, stated rather than discovered later:
204 +
205 + - **No zero-ceremony multi-machine editing.** Pull before, push after. That friction is real and is the thing Syncthing did better.
206 + - **No usable phone access.** Credentials needed away from a machine (registrar, bank, Apple Developer) either live somewhere else or are deliberately not covered.
207 +
208 + Open: whether a fresh install provisions the age identity or leaves it manual. A fresh install is exactly when doing it by hand is most annoying, and `alloy setup` already owns first-boot enrollment for tailnet and Syncthing ([CONTINUITY.md](CONTINUITY.md)), so the shape would be one screen rather than a subsystem. Undecided, and it belongs with the user manual's recovery section whenever that lands, since an age identity and a disk-encryption recovery phrase are both things you need when a machine is gone.
209 +
179 210 ## Shell
180 211
181 212 **Nushell as the login shell; bash unchanged as `/bin/sh` and `/bin/bash`.** Full rationale, architecture, and interoperability rules in [SHELL.md](SHELL.md).
@@ -333,6 +333,17 @@
333 333 license = "GPL-3.0-only"
334 334 url = "https://distrobox.it"
335 335
336 + [[section.project]]
337 + name = "GNOME Keyring"
338 + license = "GPL-2.0-or-later AND LGPL-2.1-or-later"
339 + url = "https://gitlab.gnome.org/GNOME/gnome-keyring"
340 + note = "the Secret Service apps store credentials in; the PAM module is the LGPL half"
341 +
342 + [[section.project]]
343 + name = "gopass"
344 + license = "MIT"
345 + url = "https://gopass.pw"
346 +
336 347 [[section]]
337 348 title = "hardware health"
338 349