Two-Agent Secure Setup

February 10, 2026 ยท View on GitHub

Using one agent to set up the remote signer and another to run the watch-only node.

The recommended production deployment splits key management across two machines: a signer that holds private keys and a node that handles payments and channels. Each machine can be operated by its own agent. The signer agent runs on the secure machine and exports a credentials bundle. The node agent runs on the agent-facing machine, imports that bundle, and starts a watch-only lnd instance that delegates all signing back to the signer.

This walkthrough covers the full flow from both sides.

Overview

sequenceDiagram
    participant SA as Signer Agent<br/>(secure machine)
    participant Transfer as Credentials Transfer
    participant NA as Node Agent<br/>(agent machine)

    SA->>SA: Install lnd
    SA->>SA: Create signer wallet
    SA->>SA: Start signer lnd
    SA->>SA: Export credentials bundle
    SA->>Transfer: credentials-bundle.tar.gz.b64

    Transfer->>NA: Copy bundle to agent machine

    NA->>NA: Install lnd
    NA->>NA: Import credentials bundle
    NA->>NA: Create watch-only wallet
    NA->>NA: Start watch-only lnd
    NA->>NA: Verify connection to signer

The credentials bundle contains three files:

FileWhat it isWhat it's for
accounts.jsonAccount xpubs (public keys only)Creating the watch-only wallet
tls.certSigner's TLS certificateAuthenticating the gRPC connection
admin.macaroonSigner's RPC tokenAuthorizing signing requests

No private keys cross the wire. The bundle contains only public keys and authentication material.

Docker is the default deployment method. All scripts auto-detect containers and route commands through docker exec.

Signer Machine

# Pull the lnd signer image.
skills/lightning-security-module/scripts/install.sh

# Create the signer wallet and export the credentials bundle.
# This launches the litd-signer container, creates the wallet via REST API,
# and exports the bundle to ~/.lnget/signer/credentials-bundle/.
skills/lightning-security-module/scripts/setup-signer.sh

# Start the signer container (if not already running from setup).
skills/lightning-security-module/scripts/start-signer.sh

Transfer the credentials bundle to the agent machine:

scp ~/.lnget/signer/credentials-bundle/credentials-bundle.tar.gz.b64 \
    agent-machine:~/credentials-bundle.tar.gz.b64

Agent Machine

# Pull the litd image.
skills/lnd/scripts/install.sh

# Import the signer's credentials bundle.
skills/lnd/scripts/import-credentials.sh --bundle ~/credentials-bundle.tar.gz.b64

# Create the watch-only wallet (auto-detects the litd container).
skills/lnd/scripts/create-wallet.sh

# Start in watch-only mode (launches litd + signer containers via Docker Compose).
skills/lnd/scripts/start-lnd.sh --watchonly

# Verify.
skills/lnd/scripts/lncli.sh getinfo
skills/lnd/scripts/lncli.sh newaddress p2tr

The --watchonly flag uses docker-compose-watchonly.yml, which starts both a litd container (watch-only) and connects it to the signer via the Docker network. The signer's gRPC address is pre-configured in the watch-only template (lnd.remotesigner.rpchost=signer:10012).

Container Lifecycle

# Stop containers.
skills/lnd/scripts/stop-lnd.sh

# Stop and remove volumes (destructive โ€” removes chain data).
skills/lnd/scripts/stop-lnd.sh --clean

# On signer machine.
skills/lightning-security-module/scripts/stop-signer.sh
skills/lightning-security-module/scripts/stop-signer.sh --clean

Scoping the Signer Macaroon (Container)

# On the signer machine, bake a scoped macaroon inside the container.
skills/macaroon-bakery/scripts/bake.sh --role signer-only --container litd-signer

# Re-export the credentials bundle with the scoped macaroon.
skills/lightning-security-module/scripts/export-credentials.sh --container litd-signer

Native Mode

For environments without Docker, all scripts accept a --native flag to use locally installed binaries.

Part 1: Signer Agent (Secure Machine)

The signer agent runs on a machine with restricted access. This machine will hold all private keys and never connect to the Lightning Network directly.

Install lnd

skills/lightning-security-module/scripts/install.sh --source

This builds lnd from source with the signing-related build tags. The binary is the same as a regular lnd install, but the signer's configuration restricts it to signing operations only.

Create the signer wallet and export credentials

skills/lightning-security-module/scripts/setup-signer.sh --native

This does three things:

  1. Generates a new wallet with a random passphrase and 24-word seed mnemonic. Both are saved to ~/.lnget/signer/ with mode 0600.
  2. Starts the signer lnd temporarily to extract account xpubs.
  3. Exports the credentials bundle to ~/.lnget/signer/credentials-bundle/.

The bundle directory contains the three files listed above plus a base64-encoded tarball (credentials-bundle.tar.gz.b64) for easy transfer.

Start the signer

skills/lightning-security-module/scripts/start-signer.sh --native

The signer listens on port 10012 (gRPC) for signing requests from the watch-only node. In native mode, REST binds to localhost:10013 only. It does not connect to any peers and does not participate in the Lightning Network.

Transfer the bundle

The base64-encoded bundle needs to reach the agent machine. How you transfer it depends on your environment:

# Option 1: scp
scp ~/.lnget/signer/credentials-bundle/credentials-bundle.tar.gz.b64 \
    agent-machine:~/credentials-bundle.tar.gz.b64

# Option 2: Print to terminal and copy-paste
cat ~/.lnget/signer/credentials-bundle/credentials-bundle.tar.gz.b64

The bundle contains a macaroon (bearer token) and a TLS certificate. Treat it like a password during transfer.

Optional: scope the signer macaroon

The exported admin.macaroon grants full RPC access to the signer. For production, replace it with a signer-only macaroon that restricts the watch-only node to signing and key derivation:

skills/macaroon-bakery/scripts/bake.sh --role signer-only \
    --rpc-port 10012 --lnddir ~/.lnd-signer

Then re-export the credentials bundle with the scoped macaroon:

skills/lightning-security-module/scripts/export-credentials.sh

Part 2: Node Agent (Agent Machine)

The node agent runs on the machine where agents will operate. This machine will have no private keys.

Install lnd

skills/lnd/scripts/install.sh --source

Import the credentials bundle

skills/lnd/scripts/import-credentials.sh \
    --bundle ~/credentials-bundle.tar.gz.b64

This unpacks the bundle into ~/.lnget/lnd/signer-credentials/, placing accounts.json, tls.cert, and admin.macaroon where the watch-only node expects them.

Create the watch-only wallet

skills/lnd/scripts/create-wallet.sh --native \
    --signer-host <signer-ip>:10012

Replace <signer-ip> with the signer machine's IP address or hostname. This creates a wallet that imports the account xpubs from the credentials bundle. The wallet has no seed and no private keys. It generates a random passphrase stored at ~/.lnget/lnd/wallet-password.txt (mode 0600).

The --signer-host flag tells the wallet creation process where to find the signer for initial key verification.

Start the watch-only node

skills/lnd/scripts/start-lnd.sh --native \
    --signer-host <signer-ip>:10012

The node starts with remotesigner.enable=true in its config, pointing at the signer's gRPC address. It connects to the Bitcoin network via Neutrino, syncs headers, and begins normal operation. Any transaction that requires a signature (channel opens, closes, on-chain sends) is forwarded to the signer over the authenticated gRPC connection.

Verify

skills/lnd/scripts/lncli.sh getinfo

The node should report synced status. To confirm the signer connection is working, try generating a new address (which requires key derivation from the signer):

skills/lnd/scripts/lncli.sh newaddress p2tr

If this returns an address, the watch-only node and signer are communicating correctly.

What Each Agent Can See

After setup, the two agents have different views of the system:

graph LR
    subgraph Signer["Signer Agent"]
        S_seed["Wallet seed (24 words)"]
        S_keys["All private keys"]
        S_pass["Wallet passphrase"]
        S_mac["Signer macaroons"]
    end

    subgraph Node["Node Agent"]
        N_xpubs["Account xpubs (public)"]
        N_cert["Signer TLS cert"]
        N_smac["Signer macaroon (scoped)"]
        N_pass["Node wallet passphrase"]
        N_mac["Node macaroons"]
        N_state["Channel state, balances, peers"]
    end

    subgraph Neither["Neither Agent Has"]
        X1["Cross-machine shell access"]
        X2["Each other's wallet passphrase"]
    end

The node agent can see balances, channel state, and payment history. It can initiate payments and open channels (the signing is handled transparently by the signer). It cannot extract private keys because they are not on its machine.

The signer agent can see its own wallet state but has no visibility into the watch-only node's channels or payment activity. It responds to signing requests but does not initiate any network activity.

Network Requirements

The watch-only node needs to reach the signer on port 10012 (gRPC over TLS). This is the only network path between the two machines.

graph LR
    WO["Watch-Only Node"] -->|"TCP :10012<br/>gRPC/TLS"| S["Signer"]
    WO -->|"TCP :9735<br/>Lightning P2P"| LN["Lightning Network"]
    WO -->|"TCP :18333<br/>Neutrino"| BTC["Bitcoin Peers"]

    S -.-x|"No outbound<br/>connections"| LN
    S -.-x|"No outbound<br/>connections"| BTC

The signer should be firewalled to accept connections only from the watch-only node's IP on port 10012. It makes no outbound connections.

Ongoing Operations

Once both sides are running, day-to-day operations happen on the node agent's machine:

# Check node status
skills/lnd/scripts/lncli.sh getinfo

# Fund the wallet
skills/lnd/scripts/lncli.sh newaddress p2tr
skills/lnd/scripts/lncli.sh walletbalance

# Open channels
skills/lnd/scripts/lncli.sh connect <pubkey>@<host>:9735
skills/lnd/scripts/lncli.sh openchannel --node_key=<pubkey> --local_amt=1000000

# Send payments
skills/lnd/scripts/lncli.sh sendpayment --pay_req=<bolt11>

# Use lnget for L402
lnget --max-cost 500 https://api.example.com/data

The signer agent's role is maintenance: keeping the signer process running, rotating macaroons periodically, and backing up the seed.

# On the signer machine (container mode)
skills/lightning-security-module/scripts/start-signer.sh    # delegates to docker-start.sh
skills/lightning-security-module/scripts/stop-signer.sh     # delegates to docker-stop.sh

# Rotate the signer macaroon (container)
skills/macaroon-bakery/scripts/bake.sh --role signer-only --container litd-signer
skills/lightning-security-module/scripts/export-credentials.sh --container litd-signer

# Rotate the signer macaroon (native)
skills/macaroon-bakery/scripts/bake.sh --role signer-only \
    --rpc-port 10012 --lnddir ~/.lnd-signer
skills/lightning-security-module/scripts/export-credentials.sh --native

# Then transfer the new bundle to the node agent and re-import

Credential Scoping for the Node Agent

After the watch-only node is running, bake scoped macaroons for the node agent's specific tasks. Do not leave the node agent using admin.macaroon:

# On the node agent's machine

# For a buyer agent (pays for L402 resources)
skills/macaroon-bakery/scripts/bake.sh --role pay-only

# For a seller agent (generates invoices via aperture)
skills/macaroon-bakery/scripts/bake.sh --role invoice-only

# For a monitoring agent
skills/macaroon-bakery/scripts/bake.sh --role read-only

This gives you two layers of credential scoping: the signer macaroon limits what the watch-only node can ask the signer to do, and the node macaroon limits what the agent can ask the watch-only node to do.