chore(deploy): seed a minimal .env — stop pre-seeding maskable vars (#70)

The bitspire-env activation seeds .env only when ABSENT (never refreshes on
redeploy), and env WINS over the pairing seed — so any value written at first
boot is frozen for the disk's life and silently masks the seed's source. That's
how a dead relay.aiolabs.dev and a provisioned VITE_OPERATOR_PUBKEYS made stale
installs "work" while a fresh machine broke.

Seed ONLY image-baked, non-maskable values (model, fiat, ELECTRON_FORCE_PROD,
DISPLAY, empty VITE_SPIRE_SEED placeholder). Relay + server pubkey come from the
seed; operator pubkey + fee config come from LNbits over the transport — so those
keys are no longer pre-seeded at all. VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY
are emitted only when the operator deliberately pins them via the Nix options (an
explicit override). Also drops the inert RELAY_URL/LNBITS_SERVER_PUBKEY lines from
/etc/bitspire/config.env (never loaded — EnvironmentFile is forced to .env).

Verified: built sintra-installed .env template is 5 lines, 0 maskable vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-07-02 21:13:40 +02:00
commit 1877959ab5
3 changed files with 35 additions and 29 deletions

View file

@ -143,11 +143,14 @@ in
"d ${cfg.dataDir}/branding 0755 bitspire bitspire -"
];
# Environment file for ATM configuration
# Descriptive-only ATM info at /etc/bitspire/config.env. NOTE: this is NOT
# the runtime environment — the systemd service's EnvironmentFile is
# mkForce'd to /var/lib/bitspire/.env, and the renderer reads only VITE_*
# vars. Relay + server pubkey are deliberately omitted here: they come from
# the pairing seed (aiolabs/bitspire#70), and duplicating them as non-VITE
# RELAY_URL/LNBITS_SERVER_PUBKEY only invited "looks authoritative" confusion.
environment.etc."bitspire/config.env".text = ''
# bitSpire ATM Configuration
RELAY_URL=${cfg.relayUrl}
LNBITS_SERVER_PUBKEY=${cfg.lnbitsServerPubkey}
# bitSpire ATM Configuration (descriptive; not the runtime env)
LOG_LEVEL=${cfg.logLevel}
DATA_DIR=${cfg.dataDir}

View file

@ -21,18 +21,17 @@ let
batm3 = "USD";
}.${machineModel} or "USD";
# .env template — runtime secrets are provisioned later via provision-atm.sh.
# Only non-secret defaults and display vars go here. VITE_SPIRE_SEED (the
# NIP-46 bunker pairing seed) is written at provision time; the dev-only
# VITE_ATM_PRIVATE_KEY fallback is omitted here on purpose.
# Minimal .env template (aiolabs/bitspire#70 remnant hygiene). Seed ONLY
# image-baked, non-maskable values. Relay + server pubkey come from the pairing
# SEED, operator pubkey + fee config come from LNbits over the transport — so we
# deliberately do NOT pre-seed those keys (a present-but-empty VITE_RELAY_URL /
# VITE_LNBITS_SERVER_PUBKEY / VITE_OPERATOR_PUBKEYS would win over the seed and
# mask its source). VITE_SPIRE_SEED is written by the wizard / provision-atm.sh;
# the dev-only VITE_ATM_PRIVATE_KEY fallback is omitted on purpose.
envTemplate = pkgs.writeText "bitspire-env" ''
VITE_RELAY_URL=
VITE_LNBITS_SERVER_PUBKEY=
VITE_SPIRE_SEED=
VITE_APP_ID=
VITE_OPERATOR_PUBKEYS=
VITE_LAMASSU_MACHINE_MODEL=${machineModel}
VITE_LAMASSU_FIAT_CODE=${fiatCodeForModel}
VITE_SPIRE_SEED=
ELECTRON_FORCE_PROD=1
DISPLAY=:0
'';

View file

@ -186,30 +186,34 @@
allowReboot = false;
};
# Env template — runtime secrets provisioned via provision-atm.sh.
# Identity fields are intentionally empty so a fresh disk image
# boots cleanly into the "needs provisioning" state; provision-
# atm.sh SSHes in and overwrites with real values.
# Minimal env template (aiolabs/bitspire#70 remnant hygiene).
# Seed ONLY image-baked, non-maskable values. Everything else the
# ATM needs comes from the pairing SEED (relay, lnbits_npub, bunker)
# or from LNbits over the transport (operator pubkey, fee config) —
# so we must NOT pre-seed those keys. A present-but-empty
# VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY / VITE_OPERATOR_PUBKEYS
# is a masking hazard: env WINS over the seed, and this activation
# only writes when .env is ABSENT, so any value written at first
# boot is frozen for the life of the disk. Leaving the keys out
# entirely lets the seed/transport be the sole source.
#
# VITE_RELAY_URL + VITE_LNBITS_SERVER_PUBKEY seed EMPTY by default
# (relayUrl defaults to ""), so the pairing seed drives the relay
# + server pubkey (aiolabs/bitspire#70). A non-empty `relayUrl`
# option pins a machine to a specific relay (seeded here, wins over
# the seed via env-first precedence) — otherwise leave it blank.
# VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY are emitted ONLY when
# the operator deliberately pins them via the Nix options (non-empty
# default ""), which is an explicit override that wins over the seed.
system.activationScripts.bitspire-env = ''
mkdir -p /var/lib/bitspire
if [ ! -f /var/lib/bitspire/.env ]; then
cp ${pkgs.writeText "bitspire-env-default" ''
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
VITE_LNBITS_SERVER_PUBKEY=
VITE_SPIRE_SEED=
VITE_APP_ID=
VITE_OPERATOR_PUBKEYS=
cp ${pkgs.writeText "bitspire-env-default" (''
VITE_LAMASSU_MACHINE_MODEL=${machineModel}
VITE_LAMASSU_FIAT_CODE=${fiatCode}
VITE_SPIRE_SEED=
ELECTRON_FORCE_PROD=1
DISPLAY=:0
''} /var/lib/bitspire/.env
'' + pkgs.lib.optionalString (config.services.bitspire.relayUrl != "") ''
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
'' + pkgs.lib.optionalString (config.services.bitspire.lnbitsServerPubkey != "") ''
VITE_LNBITS_SERVER_PUBKEY=${config.services.bitspire.lnbitsServerPubkey}
'')} /var/lib/bitspire/.env
chmod 600 /var/lib/bitspire/.env
chown bitspire:bitspire /var/lib/bitspire/.env
fi