Docs

Security & Shield

Explore Niral security defaults, Shield request protection, release integrity checks and encrypted snapshots. Learn recovery tools and their limits.

On this page 8 sections

Niral is hardened by default — and with the Shield, a niral app also watches itself, evicts attackers, and detects tampering. All in-process, no external service, no second machine.

> Honest scope: this protects your app and narrows an attacker's options on the > box. It cannot stop a volumetric DDoS (that needs network-edge capacity) or > someone who already has root. Niral is *self-healing under attack* — never > *unhackable*, and we will never claim otherwise.

Hardened by default

Every production response and request already carries:

  • CSP with per-request nonces, X-Frame-Options: DENY, nosniff, referrer policy
  • Signed, HttpOnly, SameSite sessions (+ Secure with NIRAL_SECURE=1)
  • CSRF blocked structurally — RPC is JSON-only and requires an x-niral-rpc header
  • XSS — templates auto-escape; SQLi — node:sqlite is parameterized
  • Rate limiting, body/upload/argument caps, path-traversal blocks, private-file 404s
  • Secrets that cannot reach the browser (reading process.env in client code is a compile error)

The Shield

The Shield inspects every request *before routing* and responds in-process:

  • Scanner bans — a request for something a niral app never has (/wp-admin,

.php, /.env, /.git) is a scanner. A few of those and the IP is banned.

  • Brute-force & flood detection — repeated 401/403/404s from one IP raise

strikes; past the threshold, a temporary ban (fail2ban-style, in memory).

  • Injection heuristics — traversal, reflected-XSS and SQLi shapes in the URL

are blocked and counted.

  • Lockdown — a *sustained* attack freezes writes: POST/PUT/PATCH/DELETE get

503, the site stays up read-only, the attack surface closes.

  • Tamper-evident audit log — every event is hash-chained in

data/shield.log.jsonl; altering a past entry breaks the chain (niral shield verify).

  • Owner alerts — with NIRAL_SMTP_URL + NIRAL_ALERT_TO set, the first ban,

lockdown, and any tampering mail you (throttled — an attack can't flood your inbox).

The Shield is on by default. Tune or disable it:

NIRAL_SHIELD=off              # disable entirely
NIRAL_SHIELD_STRIKES=6        # strikes before a ban (probes weigh 3)
NIRAL_SHIELD_BAN_MS=900000    # ban duration (15 min)
NIRAL_SHIELD_LOCKDOWN=8       # distinct bans in 5 min → lockdown
NIRAL_TRUST_PROXY=loopback    # accept a single visitor IP only from a local proxy

Behind local nginx, use NIRAL_TRUST_PROXY=loopback and have nginx overwrite X-Forwarded-For with $remote_addr, not append client-supplied headers. With Cloudflare in front, configure nginx's real-IP module to accept CF-Connecting-IP only from Cloudflare's published proxy networks, then forward the resolved visitor address. Keep the app port inaccessible from the internet. Without correct visitor identity, a scanner can cause a shared proxy-IP ban. The legacy NIRAL_TRUST_PROXY=1 trusts forwarded headers from any peer; do not use it on an app port reachable by untrusted clients.

Public documentation can stay open to readers, search engines and AI crawlers. Shield checks request behavior, not user-agent names: normal page reads are allowed, while attack probes and repeated error responses accumulate strikes. Claiming to be Googlebot or an AI crawler does not bypass protection.

Release integrity — tamper detection

A niral release is content-hashed, so tamper detection is nearly free. niral build writes integrity.json — a sha256 of every file in the release. The running server re-hashes itself on a timer; if a served file changed on disk (a defaced page, an injected script, a swapped server module), it's logged and mailed.

niral shield integrity        # re-hash the current release vs its build manifest
niral shield verify           # confirm the audit log's hash chain is unbroken
niral shield log              # recent bans, probes and lockdowns

Because deploys are atomic and the last releases are kept, recovering from a tampered release is a one-symlink rollback.

Recover — snapshots, restore, rollback, eviction

Detection is half the story. Niral also revives — all on the same box:

niral snapshot                # back up every data/*.db right now
niral snapshot list           # what's available (newest first)
niral restore latest          # roll databases back to a snapshot (undoable —
                              # the live state is snapshotted first)
niral rollback                # flip dist/current to the previous release
niral rollback --to <hash>    # …or a specific one
niral rotate-secret           # new NIRAL_SECRET — every session dies at once

Snapshots are automatic at the moments data is most at risk: hourly, before every migration, and before every deploy (the generated deploy.sh snapshots first). They use SQLite's VACUUM INTO — a consistent copy even while the app is writing. The newest 24 are kept; older ones prune.

Encrypted off-box snapshots

Local snapshots protect against bad deploys and corruption. Remote snapshots survive the entire server being lost. Niral supports any S3-compatible object store with no SDK and no plaintext upload:

  1. package the local snapshot databases
  2. gzip the package
  3. derive a 256-bit key with scrypt
  4. encrypt with AES-256-GCM (tamper authenticated)
  5. sign the request with native AWS Signature V4
NIRAL_SNAPSHOT_REMOTE_URL=https://s3.example.com
NIRAL_SNAPSHOT_REMOTE_BUCKET=my-backups
NIRAL_SNAPSHOT_REMOTE_REGION=us-east-1
NIRAL_SNAPSHOT_REMOTE_PREFIX=niral/my-app
NIRAL_SNAPSHOT_REMOTE_ACCESS_KEY=…
NIRAL_SNAPSHOT_REMOTE_SECRET_KEY=…
NIRAL_SNAPSHOT_REMOTE_KEY=a-separate-long-encryption-passphrase
NIRAL_SNAPSHOT_REMOTE_KEEP=30
niral snapshot --remote              # local snapshot + encrypted upload
niral snapshot push latest           # upload an existing local snapshot
niral snapshot list --remote         # remote inventory
niral restore latest --remote        # download, authenticate, decrypt, restore

The generated deploy script automatically pushes the newest pre-deploy snapshot when NIRAL_SNAPSHOT_REMOTE_URL is configured in server-side app.env. The encryption key is independent of the object-store credentials: losing the bucket credentials does not reveal database contents, but losing NIRAL_SNAPSHOT_REMOTE_KEY makes remote backups intentionally unrecoverable.

Auto-rollback: set NIRAL_AUTO_ROLLBACK=1 and a tampered release reverts to the previous good one and restarts itself — the site heals without you awake.

Session eviction: niral rotate-secret writes a fresh signing key, so every existing cookie stops verifying instantly — an attacker who stole a session is kicked out on the next request.

The watchdog — an independent guardian

The Shield runs inside the app. But an app can't report its own death, and a compromised app could silence its own checks. So Niral also ships a watchdog — a SEPARATE process (its own systemd unit) that guards the app from outside:

  • probes /@niral/health — down repeatedly → alerts you (and catches a

crash-loop systemd can't)

  • re-hashes the release independently — catches tampering even if the app's

own integrity check was disabled or the app process was compromised

  • verifies the audit chain from outside the app
  • with NIRAL_AUTO_ROLLBACK=1, rolls back a tampered release and restarts the

app unit

If an attacker kills the app, the watchdog survives to act; if they kill the watchdog, systemd brings it back. The two processes guard each other and share no memory — only the files on disk. niral deploy generates the watchdog unit and setup.sh enables it automatically.

> On-box snapshots are the fast rollback path. Encrypted remote snapshots are > the disaster-recovery path when the machine itself is gone.

Audit your setup

niral doctor --security       # is the Shield on? proxy IP trusted? cookies
                              # Secure? alerts wired? integrity + audit intact?

What no other framework ships

Every serious framework hardens your app. Niral is the first that also watches it, evicts attackers, restores it, and heals itself — because it's the only one that owns the server, the deploy, and the database in one zero-dependency codebase.