Repository navigation
Security Architecture
ɳSelf is designed with security as a core concern, not an afterthought. This document describes the security model and key protections built into the CLI and generated infrastructure.
All generated containers include hardened defaults:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL-
no-new-privileges, prevents privilege escalation via setuid binaries -
cap_drop: ALL, removes all Linux capabilities from containers by default
Nginx is the only service that retains NET_BIND_SERVICE (required for ports 80/443).
All internal services bind to 127.0.0.1 only. The Docker bridge network ({project}_default) is internal, services communicate via Docker DNS names (e.g., hasura:8080).
Only Nginx exposes external ports (80 and 443). All external traffic flows through Nginx, which handles TLS termination and proxying to internal services.
| Service | Binding | External Access |
|---|---|---|
| PostgreSQL | 127.0.0.1:5432 |
Nginx only (not exposed) |
| Hasura | 127.0.0.1:8080 |
Via Nginx proxy |
| Auth | 127.0.0.1:4000 |
Via Nginx proxy |
| Nginx | 0.0.0.0:80,443 |
Direct |
Secrets (passwords, API keys, JWT secrets) are managed through the .env cascade:
-
.env.secrets, sensitive values, never committed to git - Secrets are passed to containers as environment variables, not files
-
.env.secretsandssl/(certificate private keys) must be ignored by git.nself initadds both to a new project's.gitignore;nself doctor --deepasks git whether they are ignored or already tracked (SEC-GITIGNORE-01) - Secret values are redacted from all CLI output (
***REDACTED***)
Secret names containing SECRET, PASS, KEY, TOKEN, CREDENTIAL, WEBHOOK, PRIVATE,
DSN, SALT or BASICAUTH (any case) are automatically redacted. This shared rule
also hides Mailpit's MP_UI_BASICAUTH value in compose diffs, including literal
credentials from older generated files. See Config-Env-Vars.
Log redaction preserves loopback addresses (127.0.0.1 and ::1), including
port mappings such as 127.0.0.1:5432:5432, while masking other IP addresses.
ɳSelf's Nginx configuration includes security headers on all server blocks:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'" always;
server_tokens off;Auth endpoints are rate-limited to prevent brute-force attacks:
- Default: 30 requests/minute per IP
- Burst: 5 requests
- Configurable via
AUTH_RATE_LIMITenv var
All user inputs are validated via internal/sanitize before being used in generated configs:
| Input | Constraint |
|---|---|
| Project name | Lowercase, 2–30 chars, alphanumeric + hyphens |
| Base domain | Valid FQDN |
| Port numbers | 1024–65535, not in reserved list |
| Custom service names | Lowercase, alphanumeric + hyphens/underscores |
| Plugin versions | Anchored semver regex |
| Backup file paths | No path traversal (../) |
Plugin manifests are decoded with DisallowUnknownFields to prevent field injection attacks. All fields are validated after decode:
- Version strings must match anchored semver
- Plugin download URLs must use HTTPS from allowed domains
- Checksums are verified (SHA-256) before installation
- Release signatures (Ed25519) are verified against keys compiled into the CLI, before extraction (see Plugin-Security)
After the SHA-256 checksum passes (a mismatch is E124), the CLI verifies the registry entry's release_signature with Ed25519 against public keys compiled into the CLI, one set per plugin tier, and only then extracts anything. The old check against a key the plugin manifest named itself (author_public_key) certified nothing and is retired; no registry or manifest field can add a trusted key.
Failures: E121 (invalid), E122 (unknown key or wrong tier), E123 (revoked key). An unsigned plugin installs with a warning in v1.4 mode and is refused with E107 in v1.5 mode. Details, the signed-since floor and key rotation: Plugin-Security.
The one skip is the offline/development bypass (NSELF_LICENSE_SKIP_VERIFY=1 plus NSELF_LICENSE_SKIP_VERIFY_FORCE=1), refused in production and staging and written to the audit log. Per the Security-Always-Free Doctrine this verification is free, automatic, and applies to all plugin tiers.
ɳSelf applies least-privilege file permissions:
| File/Dir | Permission |
|---|---|
.env, .env.*
|
0600 (owner read/write only) |
| Backup directories | 0700 |
| SSL certificate directory | 0750 |
| Plugin cache files | 0600 |
The ɳSelf CLI itself enforces TLS 1.2 minimum for all outbound HTTPS connections (registry, license validation, update checks). Weaker protocol versions are rejected.
ɳSelf uses a layered authentication system:
The Auth service (nHost) issues JWTs on login. Tokens carry user ID, roles, and tenant claims. Hasura validates JWTs on every GraphQL request using a shared JWT secret (HASURA_JWT_KEY, minimum 32 characters).
Token lifecycle:
- Access tokens: short-lived (configurable, default 15 minutes)
- Refresh tokens: long-lived, stored server-side
- Token rotation: refresh tokens are single-use; each refresh issues a new refresh token
ɳClaw supports passkey-based authentication via ASWebAuthenticationSession on Apple platforms and WebAuthn on web. The pairing flow uses:
- Pair codes with TTL (410 Gone on expiry)
- Direct pairing via HMAC-SHA256 4-word phrases
- Pair attempt rate limiting: 10 failures per 15 minutes triggers 429 + Telegram alert
-
np_claw.pair_attemptsaudit table tracks all attempts
Auth supports 13+ OAuth providers (GitHub, Google, Apple, Microsoft, etc.). OAuth tokens for AI providers (Claude, GPT-4o) are stored with AES-256-GCM encryption in the database. Token refresh happens proactively (30-minute cycle).
Auth endpoints: 30 requests/minute per IP (burst: 5). Configurable via AUTH_RATE_LIMIT.
- ɳClaw pairing: 10 failed attempts per 15 minutes per IP, then 429 + Telegram alert
- AI caller tokens: per-namespace rate limiting with configurable limits per token
- Mux stall detector: exponential backoff on repeated failures (3 same-error cycles triggers stop + Telegram alert)
The AI plugin uses a 4-level priority queue for LLM requests. Higher-priority requests (user-facing chat) preempt lower-priority ones (background classification).
Access control operates at multiple levels:
Row-level security (RLS) enforced at the GraphQL layer. Each table defines insert/select/update/delete permissions per role. Tenant isolation uses RLS policies on tenant_id columns.
Each plugin gets its own PostgreSQL schema (np_{plugin_name}) with a dedicated role. Row-level security applies per schema. Zero cross-plugin data leakage by design.
ɳClaw persona tools_allowed whitelists restrict which tools each persona can invoke. Shell access requires explicit NCLAW_ALLOW_SHELL=true. Browser automation requires user consent (NClaw_BrowserEnabled user default).
CLAW_TG_ALLOWED_USERS restricts which Telegram user IDs can interact with the claw bot. Messages from unauthorized users are silently dropped.
The trust boundary between plugins is the deployment. Plugins in one project authenticate to each other with shared secrets over the Docker bridge network:
| Secret | Protects |
|---|---|
PLUGIN_INTERNAL_SECRET |
Generic inter-plugin X-Internal-Token header |
MUX_CLAW_SHARED_SECRET |
Mux-to-claw RPC calls (POST /internal/classify) |
NOTIFY_INTERNAL_SECRET |
Notify plugin internal API |
CLAW_WEB_SECRET |
Claw-web dashboard authentication |
PLUGIN_INTERNAL_SECRET is generated per project by the CLI at nself init and in the config defaults, and stored in the project's env files. When no env file holds it, the first nself build writes the generated value once to .env.secrets, and later builds reuse it. Every build writes it, and NOTIFY_INTERNAL_SECRET, to .nself/compose.env (0600), so a project whose .env lacks the key still gives plugins the same non-empty value. The value never appears in docker-compose.yml, on a command line or in build output. nself build injects it into every plugin container server-side. The secret is never sent to any nSelf service and never placed in a browser bundle. It travels with the project's own env files to that project's deploy target. One CLI client also reads it: nself doctor sends it as X-Internal-Token to the AI plugin daemon at PLUGIN_AI_INTERNAL_URL (default: the local daemon). That client attaches the token only when the URL host is local (localhost, a loopback or private IP literal, or plugin-ai resolving only to such addresses), sends it through a client that ignores proxy variables and never follows redirects, and otherwise sends the request without it and prints one warning to stderr. PLUGIN_INTERNAL_SECRET travels only in project env files, compose environment, and to the local ai daemon over loopback or a private network; nothing else.
ping.nself.org holds no plugin identity. It registers no plugin keys and issues no plugin-to-plugin tokens. The per-plugin identity client that earlier 1.4.x CLIs shipped (it posted the secret to ping on the first install of each plugin) is removed, and a repository test keeps it from returning.
Limits: one secret per deployment authenticates any component of that deployment, so it does not separate one plugin or app from another. App isolation stays in the data layer (source_account_id). Per-plugin caller identity is deferred and will be local to the deployment, with no network call.
All internal endpoints validate the token before processing. Missing or invalid tokens return 401 (not 500).
The browser plugin (Playwright/CDP) includes multiple safety layers:
- SSRF blocking. All RFC-1918 addresses, private IPs, and localhost are blocked by default.
-
URL allowlist.
np_claw.browser_allowlisttable controls which domains are accessible. -
Audit log. Every browser action is logged to
np_claw.browser_audit_log. -
Consent flow. Browser automation requires explicit user opt-in (
NClaw_BrowserEnableduser default on macOS). -
Stealth mode.
playwright-extraprevents bot detection on allowed sites.
The shell_dispatch_vps tool (when enabled via NCLAW_ALLOW_SHELL) logs every command to np_claw.shell_audit_log. Dangerous commands require confirmation via np_claw.pending_confirmations with a 2-minute expiry window.
-
CLAW_BLOCKED_RECIPIENTS: hard blocklist checked before any email pipeline processing. - Auto-reply guard rails: blocked senders, low-confidence replies, and sensitive topics are escalated to Telegram for manual approval.
-
np_mux_auto_reply_audittracks every auto-reply decision (sent, blocked_sender, blocked_confidence, blocked_topic).
The following security improvements are planned for a future release:
- Non-root container users for Postgres, Auth, Functions
- Docker socket proxy for Admin service
- Binary signature verification (GPG)
- Automatic ACME certificate renewal
- Security-Policy -- how to report vulnerabilities
- Security-Hardening -- production checklist
- Guide-Security-Hardening -- step-by-step hardening guide
- API-Reference -- plugin endpoint authentication
- Configuration -- environment variable reference for secrets
Getting Started
Commands
- Commands, Overview
- Lifecycle: cmd-init · cmd-build · cmd-start · cmd-stop · cmd-restart · cmd-dev
- Monitoring: cmd-status · cmd-logs · cmd-health · cmd-urls · cmd-doctor · cmd-monitor · cmd-alerts · cmd-sentry · cmd-watchdog
- Data: cmd-db · cmd-backup · cmd-dr · cmd-queue · cmd-webhooks
- Config: cmd-config · cmd-service · cmd-env · cmd-promote
- Networking: cmd-ssl · cmd-trust · cmd-dns-setup
- Security: cmd-access · cmd-security · cmd-secrets
- Tenancy: cmd-tenant · cmd-billing
- Plugins: cmd-plugin · cmd-license · cmd-dogfood (extracted, CLI-R11) · cmd-k8s (extracted, CLI-R11) · cmd-encryption (extracted, CLI-R11) · cmd-waf (extracted, CLI-R11) · cmd-federation (extracted, CLI-R11) · cmd-mail (extracted, CLI-R11) · cmd-dlq (extracted, CLI-R11)
- AI: cmd-ai · cmd-claw · cmd-model
- Templates: cmd-template
- Utilities: cmd-exec · cmd-clean · cmd-reset · cmd-update · cmd-upgrade · cmd-version · cmd-admin · cmd-migrate · cmd-migrate-firebase · cmd-migrate-supabase · cmd-completion
Features
- Features, Overview
- Feature-Auth
- Feature-Storage
- Feature-Search
- Feature-Functions
- Feature-Email
- Feature-Monitoring
- Feature-Plugins
- Feature-nClaw, AI Assistant
- Feature-nChat, Messaging
- Feature-nFamily, Family Social
- Feature-nCloud, Managed Hosting
- Feature-Memory-Rooms, Knowledge Organization
- Feature-Agent-Dashboard, Agent Metrics
- Feature-Image-Generation, AI Image Generation
Configuration
- Configuration, Overview
- Config-Env-Vars
- Config-Postgres
- Config-Hasura
- Config-Auth
- Config-Nginx
- Shared-Proxy
- Config-Optional-Services
- Config-Custom-Services
- Config-System
Plugins (87 + 10 monitoring)
- Plugin-Overview
- Plugin-Catalog
- Plugin-Install
- Plugin-Licensing
- Plugin-Architecture
- Plugin-Dev-Guide
- Plugin-Migrations-Contract
Free (25)
- plugin-backup
- plugin-content-acquisition
- plugin-content-progress
- plugin-cron
- plugin-donorbox
- plugin-feature-flags
- plugin-github
- plugin-github-runner
- plugin-invitations
- plugin-jobs
- plugin-link-preview
- plugin-mdns
- plugin-mlflow
- plugin-monitoring
- plugin-notifications
- plugin-notify
- plugin-paypal
- plugin-search
- plugin-shopify
- plugin-stripe
- plugin-subtitle-manager
- plugin-tokens
- plugin-torrent-manager
- plugin-vpn
- plugin-webhooks
Pro (62)
- plugin-access-controls
- plugin-activity-feed
- plugin-admin-api
- plugin-analytics
- plugin-auth
- plugin-bots
- plugin-browser
- plugin-calendar
- plugin-cdn
- plugin-chat
- plugin-claw
- plugin-claw-budget
- plugin-claw-news
- plugin-claw-web
- plugin-cloudflare
- plugin-cms
- plugin-compliance
- plugin-ddns
- plugin-devices
- plugin-documents
- plugin-entitlements
- plugin-file-processing
- plugin-game-metadata
- plugin-geocoding
- plugin-geolocation
- plugin-google
- plugin-home
- plugin-idme
- plugin-knowledge-base
- plugin-linkedin
- plugin-livekit
- plugin-media-processing
- plugin-meetings
- plugin-moderation
- plugin-mux
- plugin-object-storage
- plugin-observability
- plugin-photos
- plugin-podcast
- plugin-post
- plugin-realtime
- plugin-retro-gaming
- plugin-rom-discovery
- plugin-social
- plugin-sports
- plugin-support
- plugin-tmdb
- plugin-voice
- plugin-web3
- plugin-workflows
Planned (26)
plugin-auditplugin-blogplugin-checkoutplugin-commerceplugin-drmplugin-exportplugin-flowplugin-importplugin-ldapplugin-mailgunplugin-mediaplugin-oauth-providersplugin-pagesplugin-postmarkplugin-rate-limitplugin-reportsplugin-samlplugin-schedulerplugin-sendgridplugin-ssoplugin-subscriptionplugin-thumbplugin-transcoderplugin-twilioplugin-wafplugin-watermark
Guides
- Guide-Production-Deployment
- Guide-SSL-Setup
- Guide-Multi-Tenancy
- Guide-Security-Hardening
- Guide-Monitoring-Setup
- Guide-Backup-Restore
- Guide-Custom-Services
- Guide-Migration-from-v1
Architecture
- Architecture
- Architecture-Guardrails
- Service-Graph
- Compose-Generation
- Nginx-Generation
- Plugin-Architecture
Reference
Machine interface
-
JSON-Output,
--jsonenvelope -
Command-Registry,
nself help --json - Exit-Codes
- error-codes, Error Codes
-
Compat-V15,
NSELF_V15gate
Licensing
Security
Brand
Operations
- operations/release-cascade, Release Cascade
- operations/self-healing, Self-Healing Schema
- operations/redis-tuning, Redis Pool Tuning
- operations/meilisearch-warmup, MeiliSearch Warm-Up
- operations/jwt-rotation, JWT Key Rotation
- operations/windows-wsl2-setup, Windows / WSL2 Setup
- operations/gemini-oauth-reauth, Gemini OAuth Reauth
Contributing
Admin
- USER-ACTION-QUEUE, Pending Admin Actions
All commands (21)
- A: cmd-add · cmd-admin
- B: cmd-backup · cmd-build
- C: cmd-clean · cmd-config
- D: cmd-db · cmd-deploy · cmd-doctor
- E: cmd-exec
- I: cmd-init
- L: cmd-license · cmd-logs
- M: cmd-mcp
- R: cmd-remove · cmd-reset · cmd-restart
- S: cmd-start · cmd-status · cmd-stop
- U: cmd-update