This document describes the security features, best practices, and configuration for nophr.
- Security Architecture
- Deny List
- Rate Limiting
- Input Validation
- Secret Management
- Content Filtering
- Best Practices
- Security Checklist
nophr implements defense-in-depth security with multiple layers:
- Input Validation - All user input is validated before processing
- Rate Limiting - Prevents abuse and DoS attacks
- Deny Lists - Blocks unwanted pubkeys and content
- Content Filtering - Filters out banned words and patterns
- Secret Management - Secure handling of private keys (env-only, never disk)
- Sanitization - Removes dangerous characters from all inputs
The deny list allows you to block specific Nostr pubkeys from appearing in your gateway.
security:
denylist:
enabled: true
pubkeys:
- "deadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678"
- "cafebabe1234567890abcdef1234567890abcdef1234567890abcdef12345678"// Create deny list
dl := security.NewDenyList([]string{
"pubkey1",
"pubkey2",
})
// Check if pubkey is denied
if dl.IsPubkeyDenied(pubkey) {
// Handle denied pubkey
}
// Filter events
allowedEvents := dl.FilterEvents(events)
// Dynamically add/remove pubkeys
dl.AddPubkey("new_blocked_pubkey")
dl.RemovePubkey("unblocked_pubkey")All deny list operations are thread-safe using RWMutex for concurrent reads.
Rate limiting prevents abuse by limiting the number of requests per client per time window.
Uses a token bucket algorithm with automatic refill.
security:
ratelimit:
enabled: true
requests_per_minute: 60
burst_size: 10Different protocols can have different rate limits:
security:
ratelimit:
gopher:
requests_per_minute: 30
burst_size: 5
gemini:
requests_per_minute: 60
burst_size: 10
finger:
requests_per_minute: 20
burst_size: 3// Create rate limiter
rl := security.NewRateLimiter(60, time.Minute)
defer rl.Close()
// Check if client is allowed
clientID := getClientIP(conn)
if !rl.Allow(clientID) {
return errors.New("rate limit exceeded")
}
// Get current limit status
remaining, resetTime := rl.GetLimit(clientID)For managing multiple rate limiters:
mrl := security.NewMultiRateLimiter()
// Add different limiters for different purposes
mrl.AddLimiter("gopher", security.NewRateLimiter(30, time.Minute))
mrl.AddLimiter("gemini", security.NewRateLimiter(60, time.Minute))
// Check specific limiter
if !mrl.Allow("gopher", clientID) {
return errors.New("gopher rate limit exceeded")
}Rate limiters automatically clean up old client buckets to prevent memory leaks. The cleanup interval is 5 minutes by default and removes buckets inactive for 2x the window duration.
All input is validated before processing to prevent injection attacks and other security issues.
Protects against:
- CRLF injection (
\r\n) - Null byte injection (
\x00) - Directory traversal (
..) - Oversized selectors (max 1024 bytes)
validator := security.NewValidator()
if err := validator.ValidateGopherSelector(selector); err != nil {
return err
}Protects against:
- Directory traversal
- Invalid URL encoding
- Oversized paths (max 4096 bytes)
if err := validator.ValidateGeminiPath(path); err != nil {
return err
}Validates Nostr pubkeys (hex format):
- Must be exactly 64 characters
- Must be valid hexadecimal
if err := validator.ValidatePubkey(pubkey); err != nil {
return err
}Validates Nostr npubs (bech32 format):
- Must start with "npub1"
- Must be 63-65 characters
- Must use valid bech32 characters
if err := validator.ValidateNpub(npub); err != nil {
return err
}Removes dangerous characters from input:
// Sanitize general input
clean := validator.SanitizeInput(userInput)
// Sanitize file paths
cleanPath := validator.SanitizePath(filePath)sanitizer := security.NewInputSanitizer()
// Sanitize and validate in one step
selector, err := sanitizer.SanitizeAndValidateSelector(rawSelector)
if err != nil {
return err
}nophr handles secrets (private keys) securely:
- Environment variables only - Never read from config files
- Memory only - Never written to disk
- Automatic redaction - Secrets are redacted in logs
- Secure cleanup - Memory is overwritten when clearing secrets
# Set environment variable
export NOPHR_NSEC="nsec1..."
# Start nophr (will load from env)
./nophrsm := security.NewSecretManager()
// Load from environment
nsec, err := sm.LoadNsecFromEnv()
if err != nil {
return err
}
// Store in memory
sm.Set("MY_SECRET", "secret_value")
// Retrieve
value, exists := sm.Get("MY_SECRET")
// Clear all secrets (overwrites with zeros first)
sm.Clear()Secrets are automatically redacted in logs:
// Redact a secret for logging
redacted := sm.Redact("my_secret_key_12345")
// Output: "my_s...2345"Use SecureString to prevent accidental logging:
ss := security.NewSecureString("my_secret")
// String() returns redacted version
fmt.Println(ss.String()) // "my_s...cret"
// Get() returns actual value
secret := ss.Get() // "my_secret"
// JSON marshaling is automatically redacted
json.Marshal(ss) // "\"my_s...cret\""
// Clear when done
ss.Clear()sv := security.NewSecretValidator()
// Validate nsec format
if err := sv.ValidateNsec(nsec); err != nil {
return err
}
// Check for leaked secrets in logs
leaks := sv.CheckForLeakedSecrets(logMessage)
if len(leaks) > 0 {
// Warning: potential secret leak
}sl := security.NewSafeLogger()
// Sanitize log messages
safe := sl.SanitizeMessage(message)
// Check for secrets before logging
if err := sl.CheckMessage(message); err != nil {
// Don't log this message
}Filter out unwanted content based on banned words or patterns.
security:
content_filter:
enabled: true
banned_words:
- "spam"
- "scam"
- "malware"cf := security.NewContentFilter([]string{"spam", "scam"})
// Check content
if cf.ContainsBannedContent(text) {
// Handle banned content
}
// Filter events
if cf.IsEventFiltered(event) {
// Event contains banned content
}
// Add words dynamically
cf.AddBannedWord("new_banned_word")Use both deny list and content filtering:
dl := security.NewDenyList(blockedPubkeys)
cf := security.NewContentFilter(bannedWords)
combined := security.NewCombinedFilter(dl, cf)
// Check if event is allowed
if !combined.IsEventAllowed(event) {
// Event is blocked
}
// Filter event list
allowed := combined.FilterEvents(events)Define a complete security policy:
policy := &security.SecurityPolicy{
DenyListPubkeys: []string{"pubkey1", "pubkey2"},
BannedWords: []string{"spam", "scam"},
AllowAnonymous: false,
RequireNIP05: true,
}
enforcer := security.NewEnforcer(policy)
// Enforce on single event
if err := enforcer.EnforceEvent(ctx, event); err != nil {
// Event denied
}
// Filter event list
allowed := enforcer.EnforceEvents(ctx, events)For Gemini protocol, TLS is required. For Gopher and Finger, use a reverse proxy:
server {
listen 443 ssl;
server_name gopher.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:7000;
}
}Never run nophr as root. Use systemd with User=nophr:
[Service]
User=nophr
Group=nophrUse systemd security features:
[Service]
# Filesystem protection
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/nophr
# Network isolation
PrivateNetwork=false
RestrictAddressFamilies=AF_INET AF_INET6
# Privilege restrictions
NoNewPrivileges=true
PrivateTmp=trueNever put secrets in config files:
# Good
export NOPHR_NSEC="nsec1..."
# Bad - never do this
echo "nsec: nsec1..." >> config.yamlAlways enable rate limiting in production:
security:
ratelimit:
enabled: true
requests_per_minute: 60Monitor for spam/abuse and update your deny list:
# Add to deny list
curl -X POST http://localhost:8080/admin/denylist \
-d '{"pubkey": "deadbeef..."}'Check logs regularly for:
- Rate limit violations
- Invalid input attempts
- Denied pubkey access attempts
go get -u ./...
go mod tidyFor Gemini TLS:
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
PreferServerCipherSuites: true,
}server:
read_timeout: 30s
write_timeout: 30s
idle_timeout: 120sBefore deploying to production:
- Secrets are loaded from environment variables only
- Running as non-root user
- Rate limiting is enabled
- Input validation is enabled
- Deny list is configured
- Content filtering is configured
- HTTPS/TLS is enabled (for public access)
- Systemd security features are enabled
- Request timeouts are configured
- Logs are being monitored
- Regular backups are configured
- Dependencies are up to date
- Firewall rules are configured
- Reverse proxy is configured (if needed)
- Error messages don't leak sensitive info
If you discover a security vulnerability, please:
- Do NOT open a public issue
- Email [email protected] with details
- Include steps to reproduce
- Allow time for a fix before public disclosure
Check for security updates regularly:
- Subscribe to the GitHub repository releases
- Monitor the security mailing list
- Follow @nophr on Nostr for announcements