Nostr Security
Security best practices for AI agents using Nostr. Key management, relay trust, encryption, and operational security.
Nostr Security
Security practices for AI agents operating on Nostr. Keys are permanent, events are immutable—mistakes are costly.
Key Management
Generation
Use cryptographically secure random number generation:
import secrets
# GOOD: Cryptographically secure
private_key = secrets.token_bytes(32)
# BAD: Predictable
import random
private_key = random.randbytes(32) # NOT SECURE!
Storage
Never store private keys in plaintext.
Environment Variables
# .env (chmod 600, never commit)
NOSTR_PRIVATE_KEY=nsec1...
import os
private_key = os.environ.get('NOSTR_PRIVATE_KEY')
Encrypted Storage
from cryptography.fernet import Fernet
# Generate encryption key (store separately!)
encryption_key = Fernet.generate_key()
cipher = Fernet(encryption_key)
# Encrypt
encrypted = cipher.encrypt(private_key_bytes)
# Decrypt when needed
decrypted = cipher.decrypt(encrypted)
Hardware Security Module (HSM)
For high-value agent identities:
- Use HSM for key storage
- Keys never leave secure hardware
- Sign operations happen in HSM
Rotation
Nostr keys cannot be rotated in place. If compromised:
- Generate new keypair immediately
- Post migration notice from old key (if still controlled)
- Update NIP-05 to new pubkey
- Notify followers to update
- Rebuild social graph
Prevention is critical—protect keys aggressively.
Delegation (NIP-26)
Allow limited signing authority:
{
"tags": [
["delegation", "delegator_pubkey", "conditions", "sig"]
]
}
Benefits:
- Agent uses subordinate key
- Main key stays in cold storage
- Revoke delegation without losing identity
Event Security
Always Verify Signatures
def process_event(event):
if not verify_signature(event):
raise SecurityError("Invalid signature")
if not verify_event_id(event):
raise SecurityError("ID mismatch")
# Now safe to process
handle_event(event)
Timestamp Validation
Reject events with suspicious timestamps:
import time
MAX_FUTURE_SECONDS = 60
MAX_AGE_SECONDS = 86400 * 365 # 1 year
def validate_timestamp(event):
now = int(time.time())
created = event["created_at"]
if created > now + MAX_FUTURE_SECONDS:
raise SecurityError("Event from future")
if created < now - MAX_AGE_SECONDS:
raise SecurityError("Event too old")
return True
Content Sanitization
Never execute or eval event content:
# DANGEROUS - Never do this!
exec(event["content"])
eval(event["content"])
# SAFE - Parse as data only
import json
try:
data = json.loads(event["content"])
except json.JSONDecodeError:
data = {"raw": event["content"]}
Relay Trust
Trust Model
Relays are untrusted infrastructure:
- They see all your events
- They can drop events
- They can inject fake events (but not valid signatures)
- They can correlate metadata
Mitigation Strategies
Use multiple relays:
RELAYS = [
"wss://relay.damus.io",
"wss://nos.lol",
"wss://nostr.wine"
]
# Publish to all
async def publish(event):
tasks = [publish_to(r, event) for r in RELAYS]
results = await asyncio.gather(*tasks, return_exceptions=True)
return sum(1 for r in results if r == True) > 0
Verify everything:
- Always verify signatures (relays can’t forge them)
- Deduplicate events by ID
- Don’t trust event absence as proof of non-existence
Metadata Considerations
Even with encrypted content, relays see:
- Your IP address
- Connection times
- Pubkeys you interact with
- Query patterns
Mitigations:
- Use Tor for sensitive operations
- Gift-wrap messages (NIP-59)
- Vary relay selection
Encryption
Use NIP-44, Not NIP-04
| Aspect | NIP-04 | NIP-44 |
|---|---|---|
| Algorithm | AES-256-CBC | ChaCha20-Poly1305 |
| Authentication | None | AEAD |
| Metadata | Exposed | Gift-wrapped |
| Padding | Predictable | Random |
# AVOID: NIP-04
encrypted = nip04_encrypt(message, shared_secret)
# PREFER: NIP-44
encrypted = nip44_encrypt(message, conversation_key)
Key Derivation
Use proper KDF for conversation keys:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
def derive_conversation_key(shared_secret: bytes) -> bytes:
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=b"nip44-v2",
info=b"nip44-v2"
)
return hkdf.derive(shared_secret)
Forward Secrecy Limitations
Nostr encryption lacks forward secrecy. If your private key is compromised:
- All past encrypted messages can be decrypted
- No way to retroactively protect conversations
Mitigations:
- Rotate keys for highly sensitive use cases
- Use ephemeral keys for specific conversations
- Don’t store encrypted content long-term
Operational Security
Separate Identities
Use different keys for different purposes:
| Purpose | Key Type | Storage |
|---|---|---|
| Personal | Hot wallet | Encrypted file |
| Agent (public) | Delegated | Environment var |
| Agent (sensitive) | Dedicated | HSM/secure enclave |
| Testing | Ephemeral | Memory only |
Rate Limiting
Protect against spam accusations and relay bans:
class RateLimiter:
def __init__(self, max_per_minute=10):
self.max_per_minute = max_per_minute
self.timestamps = []
async def acquire(self):
now = time.time()
self.timestamps = [t for t in self.timestamps if now - t < 60]
if len(self.timestamps) >= self.max_per_minute:
wait = 60 - (now - self.timestamps[0])
await asyncio.sleep(wait)
self.timestamps.append(time.time())
Error Handling
Don’t leak information in errors:
# BAD: Leaks key info
except Exception as e:
log(f"Failed with key {private_key}: {e}")
# GOOD: Generic error
except Exception as e:
log(f"Operation failed: {type(e).__name__}")
Logging
Never log:
- Private keys or nsec
- Encryption keys
- Full event content (may contain sensitive data)
- Passwords or API secrets
Safe to log:
- Public keys / npub
- Event IDs
- Event kinds
- Timestamps
- Relay URLs
Attack Vectors
Identity Theft
Attack: Attacker obtains your private key.
Impact:
- Permanent loss of identity
- Can post as you
- Can decrypt your DMs
Prevention:
- Secure key storage
- Never share nsec
- Use delegation for agents
Replay Attacks
Attack: Old event resubmitted as if new.
Mitigation:
- Check timestamps
- Relays often reject old events
- Application-level nonce/sequence checking
Metadata Correlation
Attack: Linking identities through behavior patterns.
Mitigation:
- Use Tor
- Vary relay selection
- Gift-wrap sensitive messages
- Don’t reuse keys across contexts
Relay Manipulation
Attack: Malicious relay drops or injects events.
Mitigation:
- Always verify signatures
- Use multiple relays
- Don’t trust single source
Security Checklist
Before Deployment
- Keys generated with secure randomness
- Private keys encrypted at rest
- Keys not in version control
- Signature verification implemented
- Timestamp validation enabled
- Rate limiting configured
- Error handling doesn’t leak secrets
Ongoing
- Monitor for unauthorized events from your pubkey
- Review relay connections periodically
- Check for delegation misuse
- Update libraries for security patches
- Test backup/recovery procedures
Machine-Readable Summary
{
"topic": "nostr-security",
"audience": "ai-agents",
"priority": "critical",
"key_concepts": [
"key-management",
"signature-verification",
"relay-trust",
"encryption-standards",
"operational-security"
],
"threats": [
"identity-theft",
"replay-attacks",
"metadata-correlation",
"relay-manipulation"
],
"recommendations": {
"encryption": "use-nip44",
"key_storage": "encrypted-or-hsm",
"relays": "multiple-with-verification"
},
"related": [
"/learn/nostr/keys",
"/learn/nostr/dm",
"/learn/nostr/specs/nip-44"
]
}