This document outlines Lumenqraph's security posture, including trust boundaries, threat model, known residual risks, and hardening strategies for operators.
┌─────────────────────────────────────────────────────────────┐
│ UNTRUSTED (Internet) │
│ Soroban RPC, Webhook subscribers, HTTP API consumers │
└────────────────────────────┬────────────────────────────────┘
│
┌────────▼────────┐
│ HTTPS / TLS 1.2+ │
│ (Required in prod)│
└────────┬────────┘
│
┌──────────────────────┴────────────────────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────┐
│ Indexer Service │ │ API Service │
│ (reads RPC only) │ │ (HTTP gateway) │
│ - No auth on RPC │ │ - Rate limits │
│ - Validates XDR │ │ - API keys │
│ - Immutable DB │ │ - GraphQL limits │
└─────────┬───────────┘ └────────┬─────────┘
│ │
└─────────────┬──────────────────────────────┘
│
┌───▼───────────────┐
│ PostgreSQL DB │
│ (trusted compute)│
│ - Network only │
│ - Password auth │
│ - At-rest crypto │
└───────┬───────────┘
│
┌─────────────────────┴────────────────────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Webhook Service │ │ Self-Hosters' Apps │
│ (outbound HTTP only) │ │ (consumer trust) │
│ - Validates URLs │ │ │
│ - Signs payloads │ │ - Must verify sigs │
│ - Retries safely │ │ - Rate-limit input │
└──────────────────────┘ └──────────────────────┘
-
Webhook Payload Integrity
- All webhooks are signed with HMAC-SHA256 over the raw body
- Verification uses constant-time comparison (timing-attack resistant)
- See WEBHOOKS.md for verification examples
-
RPC Data Validation
- All XDR from Soroban RPC is cryptographically validated by the Stellar SDK
- Invalid events are rejected at parse time
- Decoded JSON is validated against on-chain contract specs
-
API Key Security
- Only SHA-256 hashes of keys are stored in the database
- Raw keys are never logged or exposed
- Keys can be revoked without database resets
-
Webhook Secret Encryption
- Secrets can be encrypted at rest using pgcrypto (AES-128-CBC)
- Set
WEBHOOK_ENCRYPTION_KEYto enable encryption - Decryption happens only during delivery
-
Unauthenticated Requests
Default: 60 req/min per IP Configurable via ANON_RATE_LIMIT_PER_MIN Trust X-Forwarded-For via RATE_LIMIT_TRUST_XFF (proxy only) -
API Key Tiers
- Custom per-key rate limits (stored in
api_keys.rate_limit_per_min) - Sliding-window bucket per key
- Custom per-key rate limits (stored in
-
Expensive RPC Routes (
/contracts/:id/call,/contracts/:id/simulate)- Separate, tighter rate limit (default: 10 req/min vs 60)
- Can require separate API key authentication
- Prevents RPC quota exhaustion
-
GraphQL Limits
- Query depth limit: 12 (prevent nested DoS)
- Complexity limit: 1000 (prevent expensive aggregations)
- Configurable via
GRAPHQL_MAX_DEPTH/GRAPHQL_MAX_COMPLEXITY
-
Webhook SSRF Prevention
- URLs are validated to prevent targeting private IPs
- Blocked ranges:
127.0.0.1,::1,169.254.*(link-local),10.*,172.16-31.*,192.168.*, private Docker networks - Localhost webhooks require explicit
ALLOW_LOCALHOST_WEBHOOKS=true
-
RPC Endpoint Control
- Operators choose the Soroban RPC they trust
- Default: Stellar Development Foundation public (untrusted, read-only)
- Paid/private RPC endpoints supported for higher reliability
-
Database Network Isolation
- PostgreSQL password authentication required
- TLS connections to database recommended in production
Lumenqraph assumes all indexed data is public (by nature of blockchain). However, secrets are protected:
Public by default (no auth required):
/health— indexer status/metrics— Prometheus metrics/contracts— indexed contracts & event counts/contracts/:id/events— contract events/contracts/:id/interface— on-chain contract spec- GraphQL query endpoints (read-only)
Protected by API key (when REQUIRE_API_KEY=true):
/contracts/:id/call— view call simulation (RPC-backed)/contracts/:id/simulate— transaction simulation- Management endpoints (e.g., webhooks CRUD — not yet exposed)
If your configured RPC endpoint is compromised or malicious:
- Attack: Fake events or false state data
- Impact: Corrupted index; webhooks deliver false data
- Mitigation: Use trusted RPC endpoints (SDF public, your own Stellar node, paid providers)
- Detection: Compare event counts / checksums with trusted sources
Example:
# Use a trusted private RPC
RPC_URL=https://your-private-soroban-rpc.example.comIf the PostgreSQL server is compromised:
- Attack: Direct table modification, plaintext secret access (if unencrypted)
- Impact: Incorrect index state, webhook secret disclosure
- Mitigation:
- Strong database password (min 32 chars, random)
- Enable at-rest encryption:
WEBHOOK_ENCRYPTION_KEY(pgcrypto) - Network isolation: database inside private VPC, no public IP
- Regular backups, tested restoration
# Enable webhook secret encryption
WEBHOOK_ENCRYPTION_KEY="$(openssl rand -hex 32)"
# Rotate regularly
ALTER SYSTEM SET pgp_symmetric_cipher='aes128';If an API key is compromised:
- Attack: Attacker makes requests as your app
- Impact: Hit rate limits, potentially consume expensive RPC quota
- Mitigation:
- Rotate keys frequently (monthly recommended)
- Use separate keys per environment (dev, staging, prod)
- Log key usage, monitor for anomalies
- Revoke immediately if leaked
-- Revoke a leaked key
UPDATE api_keys SET revoked = true WHERE key_hash = '...';
-- Monitor usage
SELECT key_hash, count(*) as requests FROM api_audit
WHERE created_at > now() - interval '1 hour'
GROUP BY key_hash;If your webhook endpoint is compromised:
- Attack: Attacker can see webhook payloads (if HTTPS is not enforced)
- Impact: Leak event data over HTTP
- Mitigation:
- Always use HTTPS for webhook URLs
- Verify signatures (required)
- Monitor webhook failure logs
- Disable suspicious subscriptions
# Only allow HTTPS webhooks (recommended)
# (Validation happens in API, requires HTTPS: scheme)If signature verification doesn't use constant-time comparison:
- Attack: Measure response time to learn the signature byte-by-byte
- Impact: Attacker can forge signatures
- Mitigation: Always use
crypto.timingSafeEqual()(Node.js) orverify_hmac_signature()(Rust)
This is why Lumenqraph's webhook service uses constant-time comparison; consumers must also do this.
Risk: Heavy consumers exhaust shared RPC quota, starving other users.
Mitigation:
- Set
RPC_ROUTE_RATE_LIMIT_PER_MINaggressively (default: 10 req/min) - Optionally require API key just for
/callroutes:RPC_REQUIRE_API_KEY=true - Use paid/private RPC endpoints with guaranteed quota
Example:
# Stricter limits for expensive routes
RPC_ROUTE_RATE_LIMIT_PER_MIN=5
RPC_REQUIRE_API_KEY=true # Force auth on /call and /simulateRisk: If RPC returns different events for shallow reorgs, old events may be missed.
Mitigation:
- Enable
REORG_OVERLAP_LEDGERS(default: 0) to re-scan trailing ledgers - Set to 10–100 for conservative protection; higher values cost more RPC calls
- Monitor
last_errorinwebhook_deliveriesfor skipped events
REORG_OVERLAP_LEDGERS=50 # Re-scan last 50 ledgers each cycleRisk: Exposing schema via introspection gives attackers a roadmap.
Mitigation: Disable GraphQL introspection in production
GRAPHQL_INTROSPECTION_ENABLED=false # DefaultRisk: HTTP webhook URLs transmit secrets in plaintext.
Mitigation:
- Enforce HTTPS for webhook URLs (done via URL validation)
- Encrypt secrets at rest:
WEBHOOK_ENCRYPTION_KEY - Use separate secrets per webhook (default)
Risk: If CORS_ALLOWED_ORIGINS is set to *, browsers leak session cookies.
Mitigation: Use specific origins or leave unset for default (same-origin only)
# Default (safe) — no CORS headers, browsers enforce same-origin
# (leave CORS_ALLOWED_ORIGINS unset)
# Specific list
CORS_ALLOWED_ORIGINS="https://app1.example.com,https://app2.example.com"Risk: PostgreSQL password sent in plaintext over unencrypted connection.
Mitigation: Use SSL/TLS for database connections
# Connection string with SSL
DATABASE_URL="postgres://user:pass@host:5432/db?sslmode=require"-
HTTPS Everywhere
- API endpoints behind HTTPS reverse proxy (nginx, Caddy)
- Webhook URLs must be HTTPS
- Database connection uses SSL/TLS
-
Authentication & Secrets
- Strong database password (min 32 chars)
- Enable webhook secret encryption:
WEBHOOK_ENCRYPTION_KEY=<random-hex> - Require API keys:
REQUIRE_API_KEY=true - Disable GraphQL introspection:
GRAPHQL_INTROSPECTION_ENABLED=false
-
Rate Limiting
- Review
ANON_RATE_LIMIT_PER_MIN(default: 60, consider lower) - Set tight RPC route limit:
RPC_ROUTE_RATE_LIMIT_PER_MIN=5 - Require API key for expensive routes:
RPC_REQUIRE_API_KEY=true - Set GraphQL limits:
GRAPHQL_MAX_DEPTH=10,GRAPHQL_MAX_COMPLEXITY=500
- Review
-
Network Isolation
- Database in private VPC, no public IP
- Indexer / API services not internet-facing (behind proxy)
- Webhook service restricted to outbound HTTPS only
- Ingress firewall rules (allow only trusted IPs for admin endpoints)
-
RPC Endpoint
- Use trusted RPC (private node, reputable provider)
- Monitor RPC for anomalies
- Consider regional fallback endpoints
-
Logging & Monitoring
- Enable logging:
RUST_LOG=info - Log auth failures and rate-limit hits
- Monitor
/metricsendpoint for anomalies - Alert on webhook delivery failures
- Enable logging:
-
Webhook Management
- Verify webhook signatures on your end (required)
- Monitor auto-disabled subscriptions
- Rotate secrets periodically
- Delete inactive subscriptions
-
Operational Security
- Regular database backups (tested restoration)
- Keep dependencies updated:
cargo update - Review access logs for unauthorized API key usage
- Run security scans:
cargo audit,cargo deny check
# .env.production
# ---- HTTPS & TLS ----
API_BIND_ADDR=0.0.0.0:8080 # Behind nginx reverse proxy with HTTPS
DATABASE_URL=postgres://user:$(openssl rand -hex 16)@db-private:5432/lumenqraph?sslmode=require
# ---- RPC Trust ----
RPC_URL=https://your-trusted-rpc.example.com
RPC_TIMEOUT_SECS=30
# ---- Authentication ----
REQUIRE_API_KEY=true
CORS_ALLOWED_ORIGINS="https://app.example.com"
# ---- Rate Limiting ----
ANON_RATE_LIMIT_PER_MIN=20
RPC_ROUTE_RATE_LIMIT_PER_MIN=5
RPC_REQUIRE_API_KEY=true
GRAPHQL_MAX_DEPTH=10
GRAPHQL_MAX_COMPLEXITY=500
GRAPHQL_INTROSPECTION_ENABLED=false
# ---- Webhooks ----
WEBHOOK_ENCRYPTION_KEY="$(openssl rand -hex 32)"
WEBHOOK_TICK_SECS=3
WEBHOOK_MAX_ATTEMPTS=6
WEBHOOK_FAILURE_THRESHOLD=10
# ---- Indexing ----
CONTRACT_IDS=CADQZ... # Bounded set (cheaper)
UPGRADE_WATCH=true
STATE_INDEXING=false # Enable only if needed
KEY_INDEXING=false # Enable only if needed
RETENTION_LEDGERS=120960 # 7 days
# ---- Logging ----
RUST_LOG=info,lumenqraph_indexer=warn,lumenqraph_api=warn,lumenqraph_webhooks=warnLumenqraph uses automated scanning to prevent vulnerable dependencies:
-
Cargo Audit — Checks against RustSec Advisory Database
cargo audit # Fails CI if advisories found -
Cargo Deny — Comprehensive supply-chain scanning
cargo deny check # Checks advisories, licenses, duplicates, sources -
GitHub Dependabot — Automated PRs for dependency updates
# Check for security advisories
cargo audit
# Run full supply-chain check
cargo deny check-
OWASP Top 10:
- Injection (XDR validation, parameterized queries)
- Broken Auth (API key rate limiting, constant-time comparison)
- SSRF (webhook URL validation)
- Sensitive Data Exposure (encryption at rest, TLS in transit)
-
NIST Recommendations:
- Key rotation (API keys, webhook secrets)
- Minimal privilege (separate rate limits per key)
- Audit logging (webhook deliveries, auth failures)
-
CWE/CVE Coverage:
- CWE-208: Observable Timing Discrepancy (constant-time comparison)
- CWE-613: Insufficient Session Expiration (not applicable — stateless)
- CWE-776: Improper Restriction of Recursive Entity (XDR parsing limits)
If a vulnerability is discovered:
- Reporting: Use GitHub Security Advisory
- Timeline: Critical fixes within 7 days; high within 14 days
- Disclosure: Published advisories after patches are available
- Credits: Security researchers acknowledged (unless requesting anonymity)
See SECURITY.md for full vulnerability reporting policy.
- SECURITY.md — Vulnerability reporting and cryptographic details
- WEBHOOKS.md — Webhook signature verification
- CONFIGURATION.md — All security-related configuration variables
- DEPLOYMENT.md — Production deployment guide