#!/bin/bash # ============================================================================= # officer-setup — the environment file # ============================================================================= # # Definitions only. # # ── Two secrets that must never be regenerated ── # # JWT_SECRET signs every session token. Minting a new one logs everybody out of # every device, silently — the symptom is people being signed out for no stated # reason. The original regenerated it on every run that answered "yes" to # regenerating .env. # # VAULT_STORE_KEY is worse, and the original never wrote it at all — so a # scripted install had no key and the vault and wallet refused to store anything. # It encrypts every upstream credential the platform holds (see docs/secret-store.md # for the full list) and, on top of the owner passphrase, the BIP39 seed envelope. # Changing it makes all of them unreadable, and for the seed that is unrecoverable: # the passphrase opens the inner envelope, and the outer one is gone. Unless the # mnemonic was written down offline, so are the coins. # # Both are read back from an existing .env and kept. Both are slated to move into # the secret store — docs/secret-store.md — which is what makes changing them an # operation rather than data loss. # # ── Derived, not asked ── # # DATA_PATH and OFFICER_ITEMS_DIR come from $OFFICER_ROOT. They were two separate # questions in the original, which had to agree with each other and with where the # app store looks. [[ -n "${OFFICER_SETUP_ENV_LOADED:-}" ]] && return 0 OFFICER_SETUP_ENV_LOADED=1 env_file() { echo "$(platform_dir)/.env"; } env_exists() { [[ -f "$(env_file)" ]]; } # One value out of an existing .env, without sourcing it — the file holds # secrets and arbitrary shell would run as root. env_get() { [[ -r "$(env_file)" ]] || return 0 awk -F= -v k="$1" ' $1 == k { v = substr($0, index($0, "=") + 1) gsub(/^"|"$/, "", v) print v exit }' "$(env_file)" } # Long enough to be worth having, and stripped of characters that would need # quoting in a file everything reads with a naive parser. generate_secret() { openssl rand -base64 48 | tr -d '/+=\n' | head -c 48; } # Origin checking is OFF unless this is explicitly false — CLAUDE.md is explicit # that the inversion is deliberate and is only defensible because the tailnet is # the perimeter. With no tailnet there is no perimeter, so the default stops # being defensible and the value has to be written the other way. tailnet_present() { ip link show tailscale0 &>/dev/null; } write_env() { local dest dest="$(env_file)" [[ -f "$dest" ]] && cp -a "$dest" "${dest}.before-officer-setup" # Restrictive from the moment it exists rather than chmod'd afterwards, so the # secrets are never briefly world-readable. Restored straight after: umask is # not scoped to a function, and leaving it at 077 would quietly make every file # a later section creates owner-only. local prior_umask prior_umask="$(umask)" umask 077 cat >"$dest" <