Lightning Security
Security best practices for Lightning Network agents. Key management, channel security, operational security, and threat models.
Lightning Security
Security practices specific to Lightning Network operations. These build on Bitcoin security fundamentals but address Lightning-specific risks.
Threat Model for Agents
| Threat | Impact | Likelihood |
|---|---|---|
| Key compromise | Total fund loss | Medium |
| Channel breach | Partial/total loss | Low |
| Node downtime | Stuck funds, failed payments | Medium |
| Peer attacks | Forced closes, fee griefing | Low |
| Network issues | Payment failures | Medium |
Key Management
Key Types
| Key | Purpose | Exposure Risk |
|---|---|---|
| Seed phrase | Master key derivation | Critical—never expose |
| Node identity | Network identity | Medium—public by design |
| Channel keys | Per-channel operations | High—auto-derived |
| Macaroons (LND) | API authentication | High—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 Type | Permission | Agent Use |
|---|---|---|
readonly.macaroon | Read only | Monitoring |
invoice.macaroon | Create invoices | Receiving |
admin.macaroon | Full access | All 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 Leaks | To Whom |
|---|---|
| Node pubkey | Everyone |
| Channel partners | Network |
| Payment amounts | Direct peers |
| IP address | Direct connections |
Privacy Practices
- Use Tor: Hide IP address
- Private channels: Don’t announce
- Route hints: Minimal disclosure
- 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
| Metric | Warning | Critical |
|---|---|---|
| Balance change | >10% daily | >50% daily |
| Pending HTLCs | >50 | >200 |
| Failed payments | >10% | >50% |
| Offline peers | >20% | >50% |
Incident Response
Channel Breach Detected
- Verify breach (old state broadcast)
- Broadcast justice transaction (watchtower should do this)
- Document for records
- Review how breach occurred
Key Compromise
- Force close all channels immediately
- Sweep funds to new wallet
- Investigate compromise vector
- Set up new node with fresh keys
Node Failure
- Restore from SCB backup
- Wait for force closes to complete
- Verify all funds recovered
- Document lessons learned
Related Topics
- Wallets - Wallet security
- Watchtowers - Breach protection
- Node Types - Infrastructure choices
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"]
}