Future: ATM ↔ operator pairing via seed-URL + WalletSharePermission (+ NIP-46 bunker deep-dive) #12

Closed
opened 2026-05-14 21:35:33 +00:00 by padreug · 3 comments
Owner

Context

Today (v2-bitspire branch, post-a86f8dc), the ATM-to-LNbits authentication path has a known architectural gap. bitSpire is built around the rule "ATM's nostr keypair IS its credential" — when the ATM signs a kind-21000 event with key X, LNbits' nostr_transport.auth.resolve_nostr_auth() either looks up the matching account or, with NEW_ACCOUNTS_ALLOWED=true, auto-creates one. There's no built-in "this ATM belongs to operator Y" linkage.

The v2 satmachineadmin design is operator-scoped: 1 LNbits user = 1 operator, with N wallets each owned by that operator. Each machine's settlements land on one of the operator's wallets. The wallet-IDOR fix (commit 3ede66f) enforces _assert_wallet_owned_by in the API.

The mismatch: if the ATM generates a fresh keypair on first boot (which is bitSpire's design), LNbits auto-creates a new user — not the operator. The wallet lands on the ATM's auto-created user, not the operator's. Operator's satmachineadmin then can't reach it (it's not "owned" by them in LNbits' model).

Current stopgap (option 1 in design discussion)

The operator's existing nostr keypair is copied onto the ATM during provision-atm.sh. The ATM signs as the operator. LNbits sees connections from the operator. Wallets live on the operator's account, satmachineadmin works as-is.

  • Zero LNbits/bitSpire changes; works for testing immediately.
  • Physical ATM compromise = full compromise of operator's LNbits identity.
  • Mitigatable only by using a dedicated LNbits account for ATM operations (no personal wallets on the same account), but still architecturally wrong long-term.

Use for v1 dev/staging only. This issue tracks the proper fix.

Proposed v2: seed-URL pairing + LNbits WalletSharePermission

Pattern lifted from aiolabs/lamassu-next#42 (fleet management + seed-URL onboarding), adapted for LNbits' WalletSharePermission infrastructure (added on the nostr-transport branch — see lnbits/core/models/wallets.py for the model).

Flow

  1. Operator opens satmachineadmin → Fleet → Add machine wizard.
  2. Operator picks: machine name, location, fiat code. The wizard:
    • (Optionally) creates a new wallet on the operator's account, OR uses an existing wallet
    • Mints a one-time pairing token scoped to that wallet_id, with short TTL (e.g. 1h)
    • Renders a seed URL as a QR code:
      bitspire+seed://<token>?relay=...&lnbits_pubkey=...&wallet_id=...&fiat=...
      
  3. ATM scans the QR on first boot (or operator pastes the URL via SSH for headless setup).
  4. ATM:
    • Generates its own nsec/npub locally — never leaves the device
    • Connects to the relay listed in the seed
    • Sends a kind-21000 pairing-claim event containing the token + its npub
  5. LNbits:
    • Validates the token (one-time, time-limited)
    • Auto-creates a fresh user for the ATM's npub (existing nostr_transport behavior)
    • Grants the ATM's user WalletSharePermission on the operator's wallet (the new bit)
    • Marks the pairing token consumed
  6. From then on, the ATM operates on the shared wallet — create_invoice, subscribe_payments, lnurlw_create_link all hit the operator's wallet through the wallet-share permission.

Why this is the right model

  • Operator-scoped invariant intact: the wallet lives on the operator's account. satmachineadmin._assert_wallet_owned_by works unchanged.
  • Revocable: operator can revoke the wallet share at any time without touching their own credentials. Compromised ATM → revoke and re-pair with a new key.
  • No operator-key exposure: the operator's signing key never touches the ATM. Only an ATM-specific key with scoped wallet access exists on the ATM.
  • Familiar UX: scan-a-QR-to-pair is the pattern everyone already understands (WalletConnect, NWC, Telegram TON, etc.).
  • Audit-friendly: every wallet operation from the ATM lands in dca_payments tagged with the ATM's npub; revocation is a one-click event.

Scope estimate

Component Where Size
Mint pairing-token endpoint LNbits core (nostr-transport branch) S — one POST + small token table
Pairing-claim handler (kind-21000 method) LNbits nostr_transport dispatcher S — new RPC method
Auto-grant WalletSharePermission on claim LNbits core S — wire to existing API
"Add machine" wizard with QR satmachineadmin (this repo) S — replaces today's wallet_id paste-in input
First-boot wizard on the ATM bitSpire (lamassu-next/dev) M — needs Electron UI, claim-send, persistence

Total ~1–2 weeks across both repos. The lamassu-next #42 issue already has groundwork for the bitSpire side; this issue is the LNbits-flavored implementation.

NIP-46 / Bunker deep-dive (rule in or out)

Before committing to the seed-URL+WalletSharePermission approach above, do a focused investigation of whether NIP-46 (Nostr Connect / Bunker) is the canonical pattern for this use case and we should adopt it instead.

Specific questions to answer

  1. Is there an established pattern in the Nostr ecosystem for "ephemeral client key with auto-approve scope"? NIP-46 is normally framed around interactive user approval. For an unattended ATM, the bunker would auto-sign everything in its policy scope — does that pattern have a name/precedent? Survey: nos2x-fox, Alby's bunker, Amethyst's signer, NWC connection strings (which have a similar shape).
  2. Does NIP-46's scoping language let the bunker enforce per-method / per-wallet authorization at the protocol level? Or is scope enforcement always client-side? If LNbits-as-bunker can refuse "anything other than create_invoice / subscribe_payments on wallet X," that's a real security gain over option 3.
  3. Does LNbits already act as / accept NIP-46 connections anywhere? If yes, we're closer to bunker-mode than we think.
  4. What's the cost of running a bunker for an always-on ATM? Specifically: where does the bunker run (LNbits process? separate service? operator's laptop?), and what's its uptime profile?
  5. Compared to option 3 (WalletSharePermission), does NIP-46 give us anything we can't get otherwise? Per-call rate limits, time-of-day restrictions, hardware-signer requirements, multi-machine fleet rotation are theoretical advantages — are any of them actually load-bearing for our threat model?

Likely outcome (predict before the dive)

Option 3 wins because:

  • Bunker needs an always-on service somewhere (extra infra)
  • For unattended ATMs the bunker just auto-signs in scope → no real difference from a wallet-share grant
  • WalletSharePermission is LNbits-native and we'd be implementing it anyway

But the dive should prove this rather than assume it. If we find that NIP-46 has matured into the standard for "machine-to-server with scoped auth" and there's tooling/UX advantage, switch.

Action

Before starting implementation of option 3:

  • Read the NIP-46 spec end-to-end, including draft amendments
  • Survey 3–4 NIP-46 implementations (nos2x, Alby, Amethyst, nostr-connect bunker daemons) for "headless" / auto-approve patterns
  • Decision doc on this issue: option 3 vs. NIP-46 with concrete reasoning
  • If NIP-46: re-scope the implementation cost
  • aiolabs/satmachineadmin#9 — v2 epic
  • aiolabs/satmachineadmin#11 — v2 follow-up findings
  • aiolabs/lamassu-next#42 — fleet management + seed-URL bootstrap (bitSpire side; this is the upstream design we'd align with)
  • LNbits nostr-transport branch — WalletSharePermission model is already present
  • NIP-46 spec: https://github.com/nostr-protocol/nips/blob/master/46.md
## Context Today (`v2-bitspire` branch, post-`a86f8dc`), the ATM-to-LNbits authentication path has a known architectural gap. bitSpire is built around the rule "ATM's nostr keypair IS its credential" — when the ATM signs a kind-21000 event with key X, LNbits' `nostr_transport.auth.resolve_nostr_auth()` either looks up the matching account or, with `NEW_ACCOUNTS_ALLOWED=true`, auto-creates one. There's no built-in "this ATM belongs to operator Y" linkage. The v2 satmachineadmin design is **operator-scoped**: 1 LNbits user = 1 operator, with N wallets each owned by that operator. Each machine's settlements land on one of the operator's wallets. The wallet-IDOR fix (commit `3ede66f`) enforces `_assert_wallet_owned_by` in the API. **The mismatch**: if the ATM generates a fresh keypair on first boot (which is bitSpire's design), LNbits auto-creates a *new* user — not the operator. The wallet lands on the ATM's auto-created user, not the operator's. Operator's `satmachineadmin` then can't reach it (it's not "owned" by them in LNbits' model). ## Current stopgap (option 1 in design discussion) The operator's existing nostr keypair is copied onto the ATM during `provision-atm.sh`. The ATM signs as the operator. LNbits sees connections from the operator. Wallets live on the operator's account, satmachineadmin works as-is. - ✅ Zero LNbits/bitSpire changes; works for testing immediately. - ❌ Physical ATM compromise = full compromise of operator's LNbits identity. - ❌ Mitigatable only by using a dedicated LNbits account for ATM operations (no personal wallets on the same account), but still architecturally wrong long-term. **Use for v1 dev/staging only.** This issue tracks the proper fix. ## Proposed v2: seed-URL pairing + LNbits WalletSharePermission Pattern lifted from `aiolabs/lamassu-next#42` (fleet management + seed-URL onboarding), adapted for LNbits' WalletSharePermission infrastructure (added on the `nostr-transport` branch — see `lnbits/core/models/wallets.py` for the model). ### Flow 1. Operator opens satmachineadmin → Fleet → **Add machine** wizard. 2. Operator picks: machine name, location, fiat code. The wizard: - (Optionally) creates a new wallet on the operator's account, OR uses an existing wallet - Mints a one-time **pairing token** scoped to that wallet_id, with short TTL (e.g. 1h) - Renders a seed URL as a QR code: ``` bitspire+seed://<token>?relay=...&lnbits_pubkey=...&wallet_id=...&fiat=... ``` 3. ATM scans the QR on first boot (or operator pastes the URL via SSH for headless setup). 4. ATM: - Generates its own nsec/npub locally — **never leaves the device** - Connects to the relay listed in the seed - Sends a kind-21000 *pairing-claim* event containing the token + its npub 5. LNbits: - Validates the token (one-time, time-limited) - Auto-creates a fresh user for the ATM's npub (existing nostr_transport behavior) - **Grants the ATM's user `WalletSharePermission` on the operator's wallet** (the new bit) - Marks the pairing token consumed 6. From then on, the ATM operates on the shared wallet — create_invoice, subscribe_payments, lnurlw_create_link all hit the operator's wallet through the wallet-share permission. ### Why this is the right model - **Operator-scoped invariant intact**: the wallet lives on the operator's account. `satmachineadmin._assert_wallet_owned_by` works unchanged. - **Revocable**: operator can revoke the wallet share at any time without touching their own credentials. Compromised ATM → revoke and re-pair with a new key. - **No operator-key exposure**: the operator's signing key never touches the ATM. Only an ATM-specific key with scoped wallet access exists on the ATM. - **Familiar UX**: scan-a-QR-to-pair is the pattern everyone already understands (WalletConnect, NWC, Telegram TON, etc.). - **Audit-friendly**: every wallet operation from the ATM lands in `dca_payments` tagged with the ATM's npub; revocation is a one-click event. ### Scope estimate | Component | Where | Size | |---|---|---| | Mint pairing-token endpoint | LNbits core (`nostr-transport` branch) | S — one POST + small token table | | Pairing-claim handler (kind-21000 method) | LNbits `nostr_transport` dispatcher | S — new RPC method | | Auto-grant `WalletSharePermission` on claim | LNbits core | S — wire to existing API | | "Add machine" wizard with QR | satmachineadmin (this repo) | S — replaces today's `wallet_id` paste-in input | | First-boot wizard on the ATM | bitSpire (`lamassu-next/dev`) | M — needs Electron UI, claim-send, persistence | Total ~1–2 weeks across both repos. The lamassu-next #42 issue already has groundwork for the bitSpire side; this issue is the LNbits-flavored implementation. ## NIP-46 / Bunker deep-dive (rule in or out) Before committing to the seed-URL+WalletSharePermission approach above, do a focused investigation of whether **NIP-46 (Nostr Connect / Bunker)** is the *canonical* pattern for this use case and we should adopt it instead. ### Specific questions to answer 1. **Is there an established pattern in the Nostr ecosystem for "ephemeral client key with auto-approve scope"?** NIP-46 is normally framed around interactive user approval. For an unattended ATM, the bunker would auto-sign everything in its policy scope — does that pattern have a name/precedent? Survey: nos2x-fox, Alby's bunker, Amethyst's signer, NWC connection strings (which have a similar shape). 2. **Does NIP-46's scoping language let the bunker enforce per-method / per-wallet authorization at the protocol level?** Or is scope enforcement always client-side? If LNbits-as-bunker can refuse "anything other than `create_invoice` / `subscribe_payments` on wallet X," that's a real security gain over option 3. 3. **Does LNbits already act as / accept NIP-46 connections anywhere?** If yes, we're closer to bunker-mode than we think. 4. **What's the cost of running a bunker for an always-on ATM?** Specifically: where does the bunker run (LNbits process? separate service? operator's laptop?), and what's its uptime profile? 5. **Compared to option 3 (WalletSharePermission), does NIP-46 give us anything we can't get otherwise?** Per-call rate limits, time-of-day restrictions, hardware-signer requirements, multi-machine fleet rotation are theoretical advantages — are any of them actually load-bearing for our threat model? ### Likely outcome (predict before the dive) Option 3 wins because: - Bunker needs an always-on service somewhere (extra infra) - For unattended ATMs the bunker just auto-signs in scope → no real difference from a wallet-share grant - WalletSharePermission is LNbits-native and we'd be implementing it anyway But the dive should *prove* this rather than assume it. If we find that NIP-46 has matured into the standard for "machine-to-server with scoped auth" and there's tooling/UX advantage, switch. ### Action Before starting implementation of option 3: - [ ] Read the NIP-46 spec end-to-end, including draft amendments - [ ] Survey 3–4 NIP-46 implementations (nos2x, Alby, Amethyst, nostr-connect bunker daemons) for "headless" / auto-approve patterns - [ ] Decision doc on this issue: option 3 vs. NIP-46 with concrete reasoning - [ ] If NIP-46: re-scope the implementation cost ## Related - `aiolabs/satmachineadmin#9` — v2 epic - `aiolabs/satmachineadmin#11` — v2 follow-up findings - `aiolabs/lamassu-next#42` — fleet management + seed-URL bootstrap (bitSpire side; this is the upstream design we'd align with) - LNbits `nostr-transport` branch — `WalletSharePermission` model is already present - NIP-46 spec: https://github.com/nostr-protocol/nips/blob/master/46.md
Author
Owner

Update: LNbits already stores the operator's nostr prvkey

Discovered while wiring up option 1 for testing — the nostr-transport branch's accounts table has a prvkey column, and operator accounts created via the standard flow have it populated:

.schema accounts
-- CREATE TABLE accounts (
--   id TEXT PRIMARY KEY,
--   ...
--   pubkey TEXT,
--   prvkey TEXT
-- );

Confirmed by querying our local dev DB: the 5 operator-side accounts on the regtest stack all have both pubkey AND prvkey populated. LNbits is, in effect, already a custodial holder of every user's nostr identity key.

What this changes for the bunker deep-dive

The "bunker needs to live somewhere" objection I raised in the original framing of option 3-vs-NIP-46 partly dissolves: LNbits is the natural location for it, because LNbits already has the keys. A NIP-46 bunker endpoint on LNbits would be a thin wrapper around existing data — no new always-on service required.

Specifically, the bunker deep-dive should now also answer:

  1. Should LNbits expose a NIP-46 bunker endpoint as a first-class feature? Given it already holds prvkey for every user, the protocol-level cost of becoming a bunker is small. This isn't ATM-specific — any Nostr-enabled extension on LNbits could benefit (lightning addresses, NIP-98 auth flows, cross-instance comms). The ATM use case is one client of a more general capability.

  2. Does exposing prvkey for bunker-style remote signing widen the LNbits trust model in a way operators are NOT already accepting? Currently prvkey sits in the DB; if compromised, the attacker already has full custody of the operator's identity. Bunker access doesn't expose new attack surface — it just makes the existing custody explicit and scoped.

What this doesn't change for the v1 recommendation

WalletSharePermission (option 3) is still the right answer for ATM ↔ operator pairing because:

  • The ATM's compromise should only expose the ATM's wallet share, not the operator's full identity.
  • With NIP-46-via-LNbits-as-bunker, a compromised ATM that has bunker access can still sign arbitrary ops for the operator unless the bunker policy is bulletproof.
  • WalletSharePermission has a smaller blast radius by design (it's wallet-scoped, not identity-scoped).

But this revelation makes NIP-46 a strictly more interesting general-purpose feature than I framed it. The deep-dive should explicitly evaluate "expose LNbits as a NIP-46 bunker" as a separate decision from "use NIP-46 for ATM pairing."

cc: the original ATM pairing decision is unchanged — go with WalletSharePermission. But this discovery should make the deep-dive worth doing as a standalone investigation regardless of ATM outcome.

## Update: LNbits already stores the operator's nostr `prvkey` Discovered while wiring up option 1 for testing — the `nostr-transport` branch's `accounts` table has a `prvkey` column, and operator accounts created via the standard flow have it populated: ```sql .schema accounts -- CREATE TABLE accounts ( -- id TEXT PRIMARY KEY, -- ... -- pubkey TEXT, -- prvkey TEXT -- ); ``` Confirmed by querying our local dev DB: the 5 operator-side accounts on the regtest stack all have both `pubkey` AND `prvkey` populated. LNbits is, in effect, **already a custodial holder of every user's nostr identity key**. ### What this changes for the bunker deep-dive The "bunker needs to live somewhere" objection I raised in the original framing of option 3-vs-NIP-46 partly dissolves: LNbits is the natural location for it, because LNbits already has the keys. A NIP-46 bunker endpoint on LNbits would be a thin wrapper around existing data — no new always-on service required. Specifically, the bunker deep-dive should now also answer: 6. **Should LNbits expose a NIP-46 bunker endpoint as a first-class feature?** Given it already holds `prvkey` for every user, the protocol-level cost of becoming a bunker is small. This isn't ATM-specific — any Nostr-enabled extension on LNbits could benefit (lightning addresses, NIP-98 auth flows, cross-instance comms). The ATM use case is one client of a more general capability. 7. **Does exposing `prvkey` for bunker-style remote signing widen the LNbits trust model in a way operators are NOT already accepting?** Currently `prvkey` sits in the DB; if compromised, the attacker already has full custody of the operator's identity. Bunker access doesn't expose new attack surface — it just makes the existing custody explicit and scoped. ### What this doesn't change for the v1 recommendation WalletSharePermission (option 3) is still the right answer for ATM ↔ operator pairing because: - The ATM's compromise should only expose **the ATM's wallet share**, not the operator's full identity. - With NIP-46-via-LNbits-as-bunker, a compromised ATM that has bunker access can still sign arbitrary ops for the operator unless the bunker policy is bulletproof. - WalletSharePermission has a smaller blast radius by design (it's wallet-scoped, not identity-scoped). But this revelation makes NIP-46 a strictly more interesting *general-purpose* feature than I framed it. The deep-dive should explicitly evaluate "expose LNbits as a NIP-46 bunker" as a separate decision from "use NIP-46 for ATM pairing." cc: the original ATM pairing decision is unchanged — go with WalletSharePermission. But this discovery should make the deep-dive worth doing as a standalone investigation regardless of ATM outcome.
Author
Owner

Superseded in part by the new security pathway epic at #13. The seed-URL pairing + WalletSharePermission idea folds into S0 (pairing) + S2 (NIP-26 delegation enforcement); the NIP-46 bunker deep-dive folds into S7 (with aiolabs/lnbits#9 as the LNbits-side enabler).

Keeping this issue open as the conversational record of the original analysis — content from here informed the structured plan. Close as duplicate once the relevant sub-issues land.

Superseded in part by the new security pathway epic at #13. The seed-URL pairing + WalletSharePermission idea folds into S0 (pairing) + S2 (NIP-26 delegation enforcement); the NIP-46 bunker deep-dive folds into S7 (with `aiolabs/lnbits#9` as the LNbits-side enabler). Keeping this issue open as the conversational record of the original analysis — content from here informed the structured plan. Close as duplicate once the relevant sub-issues land.
Author
Owner

Closing as superseded by the security-pathway epic at #13.

This issue's content has been split across the epic's phased sub-issues:

  • Stopgap (Option 1) — documented in #14 (S0) as "today's stopgap" until the bunker direction lands.
  • Seed-URL + WalletSharePermission — superseded by the NIP-46 pivot. Per aiolabs/lnbits#9 (reframed 2026-05-25) and aiolabs/lnbits#18 (sidecar nsecbunkerd integration), the canonical pattern is now NIP-46 connection tokens issued by a bunker, not WalletSharePermission. See #14 (S0 — pairing payload swap), #16 (S2 — NIP-46 enforcement), #21 (S7 — consume the bunker).
  • NIP-46 deep-dive (item 5 of this issue) — done. The bunker direction is the answer. NIP-26 is dead.

No content lost; consolidating to keep the tree clean.

Closing as **superseded by the security-pathway epic at #13**. This issue's content has been split across the epic's phased sub-issues: - **Stopgap (Option 1)** — documented in #14 (S0) as "today's stopgap" until the bunker direction lands. - **Seed-URL + WalletSharePermission** — superseded by the NIP-46 pivot. Per `aiolabs/lnbits#9` (reframed 2026-05-25) and `aiolabs/lnbits#18` (sidecar `nsecbunkerd` integration), the canonical pattern is now NIP-46 connection tokens issued by a bunker, not WalletSharePermission. See #14 (S0 — pairing payload swap), #16 (S2 — NIP-46 enforcement), #21 (S7 — consume the bunker). - **NIP-46 deep-dive (item 5 of this issue)** — done. The bunker direction is the answer. NIP-26 is dead. No content lost; consolidating to keep the tree clean.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/satmachineadmin#12
No description provided.