Skip to content

fix(bundles): generate the admin passwords Crow can choose, and save them to the keychain - #504

Merged
kh0pper merged 7 commits into
mainfrom
fix/bundle-generated-secrets
Oct 16, 2026
Merged

kh0pper merged 7 commits into
mainfrom
fix/bundle-generated-secrets

Conversation

@kh0pper

@kh0pper kh0pper commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Queue item 30, fix B (one-click audit). When you install a collection, every member gets an empty env and no keychain request. These bundles needed a password typed by hand, so they were left without a usable admin, or their compose hard-failed.

What changes

  • miniflux (MINIFLUX_ADMIN_PASSWORD) and maker-lab-advanced (MLA_ADMIN_PASSWORD): Crow generates the password (generate: "secret") and saves it to the Crow keychain with the matching username, through the existing keychain: true path that minio, browser and vaultwarden already use. The app creates its admin from it on first start. MLA_ADMIN_USER defaults to admin, and the compose no longer hard-fails on it.
  • actual-budget:
    • New ACTUAL_SERVER_PASSWORD, generated and saved to the keychain, for the Actual server Crow installs.
    • A postInstall hook sets it right after install, because Actual's first-run bootstrap goes to whoever calls first.
      • It talks to loopback only.
      • The password goes in on stdin and is never printed.
      • If the server already has a password (data kept from an earlier install), the hook says so and asks you to type the old one into ACTUAL_PASSWORD.
    • The MCP server and the panel bootstrap only with the generated password, and log in with the typed one first, then the generated one (loopback only).
    • ACTUAL_PASSWORD stays a typed field, for an Actual server you run elsewhere.
  • lemmy:
    • The first-admin password was a literal in the public compose file. It is now a generated LEMMY_ADMIN_PASSWORD saved to the keychain.
    • Every Lemmy secret reaches the entrypoint through the container environment ($${…}), so the script text in PID 1's command line holds variable names, not values.
    • Known issue, not fixed here (already on main): the entrypoint runs a path the pinned image does not have, so the container does not start. It's in the bundle notes; the fix needs a live test.
  • Installer (servers/gateway/routes/bundles.js): generated values that a manifest lists in server.envKeys go into this instance's mcp-addons.json entry (mode 600), and never into the gateway .env that co-hosted instances share. Stored hashes (store_as) are filtered out.
    • Side effect: the browser bundle's VNC password also lands in that entry, and therefore in pi-bot session .mcp.json files (both mode 600).

Deferred

  • campaigns / tax encryption keys: generating these waits for the recovery-key decision (EXEMPT in tests/bundle-form-friction.test.js). Losing a generated key loses the data.
  • motioneye: its admin password lives hashed in motioneye.conf, with no env variable, so it doesn't fit this mechanism.

Existing installs: nothing rewrites a live .env. A reinstall reuses the values already there, and keys are never regenerated.

Tests:

  • tests/bundle-generated-secrets.test.js: collection-style install, the keychain entry, the MCP entry, reinstall reuse, Actual bootstrap and hook against a fake server, no literal passwords in tracked compose files.
  • Updated: bundle-form-friction, bundles-install-hardening.
  • Full suite: 9536 tests, 0 failures.

Version bumps: miniflux 1.0.3, maker-lab-advanced 0.1.2, actual-budget 1.0.1, lemmy 1.0.3. Registry and skill ledger are rebuilt.

Deploy: gateway code. Restart crow-gateway, plus r4's gateway on the same tree, for new installs to use it.

https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

…them to the keychain

A one-click (collection) install left these blank, so the install failed or
the app had no usable login:

- miniflux MINIFLUX_ADMIN_PASSWORD and maker-lab-advanced MLA_ADMIN_PASSWORD
  are generated, saved to the keychain with their usernames, and seed the
  app's admin on first start. MLA_ADMIN_USER defaults to admin (the compose
  no longer hard-fails on it).
- lemmy's first-admin password was a literal in the public compose file. It
  is a generated LEMMY_ADMIN_PASSWORD now, and every secret reaches the
  entrypoint through the environment, so the script text in PID 1's command
  line holds names, not values. The read-only ./config mount the entrypoint
  wrote into is gone.
- actual-budget: a generated ACTUAL_SERVER_PASSWORD for the server Crow
  installs, set right after install by a postInstall hook (Actual's first-run
  bootstrap is first-caller-wins) and as a fallback on first connect. It is
  only ever sent to a loopback URL. ACTUAL_PASSWORD stays typed, for an
  Actual server you run elsewhere.
- The installer puts generated values a manifest lists in server.envKeys
  into this instance's MCP entry (mcp-addons.json, mode 600), never into the
  gateway .env that co-hosted instances share.

campaigns and tax encryption keys are not generated yet: that waits for the
recovery-key decision (tests/bundle-form-friction.test.js EXEMPT).

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
… explains a kept password

- Lemmy is left out of this PR: its entrypoint execs a binary the pinned
  image doesn't have, so it can't start today; generating an admin password
  it never creates would put a wrong entry in the keychain. Follow-up.
- Actual: when the server already has a password (kept data from an earlier
  install), the hook checks whether the generated one opens it and, if not,
  says to type the old one into ACTUAL_PASSWORD; the MCP login error says
  the same. The hook tolerates a missing .env and stays within its timeout.
- A stored hash (store_as) never reaches an MCP entry.
- maker-lab-advanced's panel says log in (the admin exists from first boot).
- Actual docs (en/es) and skill describe the generated password.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
The first-admin password was a literal in the public compose file. It is a
generated LEMMY_ADMIN_PASSWORD now, saved to the keychain (username
admin_pending), and every Lemmy secret reaches the entrypoint through the
environment ($${...}), so the script text in PID 1's command line holds
names, not values. The bundle's entrypoint still runs a path the pinned image
doesn't have (a known issue in its notes; the fix needs a live test).

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
… login falls back to it

A typed ACTUAL_PASSWORD can be stale (kept in the gateway .env from an older
install). It is never used to bootstrap a new server any more, and on a
loopback server a failed typed login retries with the generated password,
in the MCP server and the panel.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
@kh0pper
kh0pper force-pushed the fix/bundle-generated-secrets branch from f8308d5 to d467777 Compare October 16, 2026 21:56
…ll (stacked on #504) (#505)

* fix(bundles): no placeholder value from .env.example reaches an install

- servers/gateway/bundle-env-placeholders.js recognises example values
  (/path/to/, youruser, your-, *.example.com, change-me, replace-me, <...>,
  xxx, placeholder) and, by SHA-256, example secrets once published here.
- The installer leaves example values out when it builds .env from
  .env.example: such a line becomes a comment naming only the key.
- env_vars[].default_in_crow_home: a blank or example path becomes a folder
  inside this instance's CROW_HOME, created by the gateway (never by Docker
  as root), and the key no longer blocks install. jellyfin, navidrome,
  audiobookshelf and kavita start with an empty library there.
- 'Finish setup' lists a required key whose value is an example, so existing
  installs are flagged without rewriting their .env.
- tax never encrypts with an example key (decrypt still reads data saved with
  one); its .env.example no longer carries a key.
- .env.example files no longer carry example domains, paths or secrets as
  live values (kept as commented examples). plex follows once the
  install-guard PR, which also bumps it, lands.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* docs(bundles): jellyfin and audiobookshelf start with an empty library in Crow's folder

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* fix(bundles): placeholder fixes after review — tax key change keeps old returns readable; domains must be real

- tax refuses only the published example key (by hash), never a passphrase
  for its shape. CROW_TAX_ENCRYPTION_KEY_PREVIOUS opens returns saved with
  an old passphrase; saving re-encrypts them with the current one.
- gotosocial, writefreely and matrix-dendrite fail to start without a real
  domain (${X:?}) instead of booting with a blank one.
- kavita and audiobookshelf keep their named-volume fallback (only the
  example paths are left out); jellyfin/navidrome's CROW_HOME paths are never
  copied to the shared gateway .env (propagate: false).

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* fix(bundles): placeholder review fixes — tax key change keeps the old key; reinstall keeps the library path

- env_vars[].previous_key: changing tax's encryption key in Configure keeps
  the old one in CROW_TAX_ENCRYPTION_KEY_PREVIOUS (when that slot is empty),
  so returns saved with it stay readable. The contract checks the name.
- Uninstall keeps default_in_crow_home paths in the retained copy and a
  reinstall reuses them, so kept Jellyfin/Navidrome data never gets an empty
  library mounted over it.
- GTS_ACCOUNT_DOMAIN is optional (compose falls back to GTS_HOST); the domain
  fields say they are fixed at first start.
- The scrub also handles KEY = value lines; navidrome docs (en/es).

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* fix(tax): key changes never strand data; the tax server reads its key from its own MCP entry

- Configure reads the outgoing key where the tax server really runs with it
  (its mcp-addons entry, then the gateway env, then the bundle .env) and keeps
  it in CROW_TAX_ENCRYPTION_KEY_PREVIOUS. A second change while that slot
  still holds a different older key is refused (409), so no key in use is
  ever overwritten.
- At start the tax server re-encrypts every return that only the previous
  key opens, so the slot can then be cleared.
- tax's server.envKeys list both keys, so a typed key reaches the server.
- A kept library path follows the latest install; log wording.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* test(tax): the start-time re-encryption to the current key, as a tested function

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* fix(tax): simpler key change — the user keeps the old key in _PREVIOUS; re-encryption never overwrites a newer save

The Configure-side copy of the outgoing key (and its 409) is removed: it
could only be undone by hand-editing mcp-addons.json. A key change now means:
set the new key and put the old one in CROW_TAX_ENCRYPTION_KEY_PREVIOUS (the
field and the refusal message say so). Returns move to the new key when the
tax server starts or a return is saved; the start-time move is a
compare-and-swap, so a save that lands meanwhile wins.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* fix(tax): a key change keeps the outgoing key in _PREVIOUS when that slot is empty

Configure copies the outgoing CROW_TAX_ENCRYPTION_KEY (read where the tax
server runs with it) into CROW_TAX_ENCRYPTION_KEY_PREVIOUS when the request
leaves that slot alone and it is empty or holds the new key. A slot holding
another key is never overwritten; the response warns, naming keys only. A
typed previous key always wins.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* fix(tax): the previous-key slot always takes the key just replaced; Configure shows server warnings

The tax server moves every return to the current key when it starts, so the
key that matters after a change is always the one just replaced. Keeping an
older one instead stranded everything on the current key at the next change.
The Configure dialog now shows the response's warnings in the warning
colour, long enough to read.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

* fix(plex): no example media path from .env.example (now that the install-guard PR is underneath); tax error text

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
@kh0pper
kh0pper merged commit 3a5c7be into main Oct 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant