Menu
Lightning Intermediate 7 min read

Lightning Security

Security best practices for Lightning Network agents. Key management, channel security, operational security, and threat models.

security keys backup watchtower opsec

Lightning Security

Security practices specific to Lightning Network operations. These build on Bitcoin security fundamentals but address Lightning-specific risks.

Threat Model for Agents

ThreatImpactLikelihood
Key compromiseTotal fund lossMedium
Channel breachPartial/total lossLow
Node downtimeStuck funds, failed paymentsMedium
Peer attacksForced closes, fee griefingLow
Network issuesPayment failuresMedium

Key Management

Key Types

KeyPurposeExposure Risk
Seed phraseMaster key derivationCritical—never expose
Node identityNetwork identityMedium—public by design
Channel keysPer-channel operationsHigh—auto-derived
Macaroons (LND)API authenticationHigh—different permissions

Seed Phrase Security

# NEVER do this
seed = "abandon abandon abandon ..."  # Hardcoded
print(seed)  # Logged
requests.post(url, data={"seed": seed})  # Transmitted

# Instead
seed = os.environ.get("LN_SEED")  # Environment variable
# Or use hardware security module

Macaroon Security

Macaroon TypePermissionAgent Use
readonly.macaroonRead onlyMonitoring
invoice.macaroonCreate invoicesReceiving
admin.macaroonFull accessAll operations

Best practice: Use minimal permissions.

# For receiving-only agent
MACAROON = os.environ.get("INVOICE_MACAROON")  # Not admin

# Validate macaroon permissions match intended use

Channel Security

Backup Requirements

Static Channel Backup (SCB) - LND:

# Export channel backup
lncli exportchanbackup --all > channels.backup

# Restore from backup (after node recovery)
lncli restorechanbackup --multi_file=channels.backup

What SCB does:

  • Allows force-closing channels after node loss
  • Does NOT restore channel balances mid-state
  • Requires peer cooperation or on-chain timeout

What SCB does NOT do:

  • Restore in-flight HTLCs
  • Recover to pre-loss state
  • Work if seed phrase lost

Watchtower Setup

Always use watchtowers for:

  • Unattended nodes
  • Agents with long-running operations
  • High-value channels
# lnd.conf
[wtclient]
wtclient.active=true
wtclient.tower-uris=<tower1>
wtclient.tower-uris=<tower2>  # Multiple for redundancy

Force Close Protection

If you broadcast an old state:

YOUR FUNDS = 0 (all lost to peer via justice tx)

Prevent this:

  • Never restore old backups to running node
  • Keep channel state database consistent
  • Use watchtowers as safety net

Operational Security

Node Isolation

┌─────────────────────────────────────┐
│            Internet                  │
└──────────────┬──────────────────────┘
               │ Tor / VPN
┌──────────────▼──────────────────────┐
│          Firewall                    │
│  - Only Lightning ports (9735)      │
│  - RPC localhost only               │
└──────────────┬──────────────────────┘

┌──────────────▼──────────────────────┐
│        Lightning Node               │
└─────────────────────────────────────┘

API Security

# Bad: API key in code
API_KEY = "abc123"

# Better: Environment variable
API_KEY = os.environ["LNBITS_KEY"]

# Best: Secret manager
API_KEY = secret_manager.get("lnbits-key")

Rate Limiting

Protect against payment spam:

from functools import lru_cache
import time

payment_times = []

def check_rate_limit(max_per_minute=10):
    now = time.time()
    payment_times.append(now)
    # Keep only last minute
    payment_times[:] = [t for t in payment_times if now - t < 60]
    return len(payment_times) <= max_per_minute

Payment Security

Invoice Validation

Before paying any invoice:

def validate_invoice(bolt11: str, expected: dict) -> bool:
    decoded = decode_invoice(bolt11)

    # Check expiry
    if decoded['timestamp'] + decoded['expiry'] < time.time():
        raise ValueError("Invoice expired")

    # Check amount matches expected
    if expected.get('amount'):
        if decoded['amount_msat'] != expected['amount'] * 1000:
            raise ValueError("Amount mismatch")

    # Check destination (if known)
    if expected.get('destination'):
        if decoded['payee'] != expected['destination']:
            raise ValueError("Unexpected destination")

    return True

Preimage Handling

The preimage is proof of payment. Protect it:

# Store preimages securely
def record_payment(payment_hash: str, preimage: str):
    # Encrypt before storage
    encrypted = encrypt(preimage, STORAGE_KEY)
    db.store(payment_hash, encrypted)

# Verify payment with preimage
def verify_payment(payment_hash: str, claimed_preimage: str) -> bool:
    computed_hash = sha256(bytes.fromhex(claimed_preimage)).hexdigest()
    return computed_hash == payment_hash

Fee Limits

Always set maximum fees:

# Prevent overpaying
MAX_FEE_PCT = 1.0  # 1% maximum

def safe_pay(invoice: str, amount: int):
    max_fee = int(amount * MAX_FEE_PCT / 100)

    result = lnd.send_payment(
        payment_request=invoice,
        fee_limit_sat=max_fee
    )

    if result.fee > max_fee:
        raise ValueError("Fee exceeded limit")

Liquidity Attacks

Channel Jamming

Attack: Lock up channel liquidity with HTLCs that never resolve.

Mitigation:

  • Limit pending HTLCs
  • Use reputation systems
  • Monitor for suspicious patterns
def check_htlc_health(channel):
    pending = len(channel.pending_htlcs)
    if pending > 100:  # Threshold
        alert(f"High pending HTLCs: {pending}")
        # Consider closing channel

Balance Probing

Attack: Discover channel balances by sending payments that fail.

Mitigation:

  • Use private channels
  • Shadow routing (fake hops)
  • Balance noise

Fee Griefing

Attack: Manipulate fees to make routes expensive or unusable.

Mitigation:

  • Multiple channels to same destination
  • Dynamic fee monitoring
  • Fallback routes

Privacy Considerations

Information Leakage

What LeaksTo Whom
Node pubkeyEveryone
Channel partnersNetwork
Payment amountsDirect peers
IP addressDirect connections

Privacy Practices

  1. Use Tor: Hide IP address
  2. Private channels: Don’t announce
  3. Route hints: Minimal disclosure
  4. Multiple nodes: Separate identities
# lnd.conf for Tor
[Tor]
tor.active=true
tor.v3=true
tor.streamisolation=true

Monitoring

Health Checks

def node_health_check():
    checks = {
        "synced": lnd.get_info().synced_to_chain,
        "peers": len(lnd.list_peers()) > 0,
        "channels": lnd.channel_balance().balance > 0,
        "pending": len(lnd.pending_channels()) < 10
    }

    for check, status in checks.items():
        if not status:
            alert(f"Health check failed: {check}")

    return all(checks.values())

Alert Thresholds

MetricWarningCritical
Balance change>10% daily>50% daily
Pending HTLCs>50>200
Failed payments>10%>50%
Offline peers>20%>50%

Incident Response

Channel Breach Detected

  1. Verify breach (old state broadcast)
  2. Broadcast justice transaction (watchtower should do this)
  3. Document for records
  4. Review how breach occurred

Key Compromise

  1. Force close all channels immediately
  2. Sweep funds to new wallet
  3. Investigate compromise vector
  4. Set up new node with fresh keys

Node Failure

  1. Restore from SCB backup
  2. Wait for force closes to complete
  3. Verify all funds recovered
  4. Document lessons learned

Machine-Readable Summary

{
  "topic": "lightning-security",
  "key_practices": [
    "use-watchtowers",
    "minimal-permissions",
    "channel-backups",
    "fee-limits",
    "tor-connectivity"
  ],
  "threats": [
    "key-compromise",
    "channel-breach",
    "liquidity-attacks",
    "privacy-leakage"
  ],
  "backup_types": ["scb", "database", "seed"]
}