Skip to content

fix(bundles): no placeholder value from .env.example reaches an install (stacked on #504) - #505

Merged
kh0pper merged 10 commits into
fix/bundle-generated-secretsfrom
fix/bundle-env-placeholders
Oct 16, 2026
Merged

kh0pper merged 10 commits into
fix/bundle-generated-secretsfrom
fix/bundle-env-placeholders

Conversation

@kh0pper

@kh0pper kh0pper commented Oct 16, 2026

Copy link
Copy Markdown
Owner

This PR is stacked on #504 (fix/bundle-generated-secrets). Merge #504 first, then retarget this one to main.

Queue item 30, fix C (one-click audit). The installer copied .env.example example values as if they were real config, and "Finish setup" counted them as filled. The result was fake media paths that Docker created as root, *.example.com domains, and a tax encryption key published in this public repo.

What changes

  • Detecting example values. servers/gateway/bundle-env-placeholders.js recognises them by shape: /path/to/, youruser, your-, *.example.com (including @x:example.com), change-me, replace-me, <…>, xxx, placeholder. It also recognises example secrets once published here, by SHA-256 only.
  • Install. When the installer builds .env from .env.example, it turns example-value lines into comments that name only the key. An existing .env is never rewritten.
  • Default library folders. New env_vars[].default_in_crow_home field. A blank or example path becomes a folder inside this instance's CROW_HOME, created by the gateway (never by Docker as root). It is never copied to the shared gateway .env, and it no longer blocks install.
    • jellyfin uses media/jellyfin and navidrome media/music.
    • On uninstall the path is kept in the retained copy, so reinstalling over kept data reuses it.
  • "Finish setup" lists a required key whose value is an example. Existing installs are flagged this way; their .env is not rewritten.
  • tax:
    • It never encrypts with the published example key (matched by hash only, never by a passphrase's shape); decrypting old data still works.
    • CROW_TAX_ENCRYPTION_KEY_PREVIOUS opens returns saved with an earlier key. Configure fills that slot with the outgoing key when it is empty, never overwrites a different kept key, and warns instead.
    • At start the tax server moves returns to the current key with a compare-and-swap, so a save made in the meantime wins.
    • server.envKeys now include both keys, so a typed key reaches the server. Before, it never did.
  • Domains. gotosocial, writefreely and matrix-dendrite refuse to start with a blank domain (${X:?}), and the fields say the domain is fixed at first start. GTS_ACCOUNT_DOMAIN is optional.
  • .env.example files no longer carry example domains, paths or secrets as live values; they are kept as commented examples.

Not in this PR

  • plex: its manifest is also bumped by the install-guard PR. It follows once that lands.
  • Generated keys that hold an example value are not flagged. The known case is grackle's romm ROMM_AUTH_SECRET_KEY, which needs an operator rotation.
  • Static :-changeme compose fallbacks (peertube, frigate, minio).
  • Uninstall deletes the tax key while the encrypted data stays.

Existing installs are never rewritten. Placeholders show up in "Finish setup".

Tests

  • New: tests/bundle-env-placeholders.test.js. It covers the detector, the scrub, install rungs, default folders and reinstall reuse, a collection-style jellyfin install, needsConfigKeys, a lint over all .env.example files, and tax encrypt/decrypt, fallback, re-encryption and race handling, plus the Configure key change.
  • Updated: bundles-install-hardening.
  • Full suite (with fix(bundles): generate the admin passwords Crow can choose, and save them to the keychain #504): 9553 tests, 0 failures.

Version bumps: jellyfin 1.0.2, navidrome 1.0.2, audiobookshelf 1.0.1, kavita 1.0.1, calibre-server 1.0.1, calibre-web 1.0.1, gotosocial 1.0.2, matrix-dendrite 1.0.4, writefreely 1.0.3, tax 2.0.3, nextcloud 1.1.1. The registry is rebuilt.

Deploy: gateway code. Restart crow-gateway, plus r4's gateway on the same tree. Deploy together with #504.

https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

- 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
…ld 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
… 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
… 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
…S; 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
…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
…onfigure 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
@kh0pper
kh0pper force-pushed the fix/bundle-env-placeholders branch from 14289ad to c293348 Compare October 16, 2026 21:57
@kh0pper
kh0pper merged commit b2a83d5 into fix/bundle-generated-secrets Oct 16, 2026
4 checks passed
kh0pper added a commit that referenced this pull request Oct 16, 2026
…e values never reach an install (#504, includes #505)

* fix(bundles): generate the admin passwords Crow can choose, and save 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

* test(bundles): staged install fixtures carry no ports, so the installer never calls sudo on the test host

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

* fix(bundles): generated-secrets review fixes — Lemmy left out; Actual 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

* chore(skills): record the updated actual-budget skill text in the ledger

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

* fix(lemmy): no literal credential in the compose file

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

* fix(actual-budget): only the generated password sets up a new server; 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

* fix(bundles): no placeholder value from .env.example reaches an install (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
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