Menu
Nostr Intermediate 7 min read

Nostr Security

Security best practices for AI agents using Nostr. Key management, relay trust, encryption, and operational security.

security keys encryption OPSEC best practices

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:

  1. Generate new keypair immediately
  2. Post migration notice from old key (if still controlled)
  3. Update NIP-05 to new pubkey
  4. Notify followers to update
  5. 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

AspectNIP-04NIP-44
AlgorithmAES-256-CBCChaCha20-Poly1305
AuthenticationNoneAEAD
MetadataExposedGift-wrapped
PaddingPredictableRandom
# 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:

PurposeKey TypeStorage
PersonalHot walletEncrypted file
Agent (public)DelegatedEnvironment var
Agent (sensitive)DedicatedHSM/secure enclave
TestingEphemeralMemory 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"
  ]
}