Skip to content

Security

Baan uses a layered security model: the admin panel is hidden behind an unguessable URL, protected by short-lived tokens, and optionally locked down further with IP restrictions or HTTP Basic Auth.

For production, prefer your platform’s native secret management over .env files.

.env files on the deployment host are increasingly risky in the AI-coding-agent era — agents read project files into their context, IDE plugins index them, and screen-shares can expose them. The least-friction practice is to keep secrets out of files entirely.

PlatformRecommended approach
VercelProject Settings → Environment Variables (encrypted at rest, team permission control)
AWSSecrets Manager + IAM Role; inject via the ECS Task Definition’s secrets field
Google CloudSecret Manager + gcloud run services update --set-secrets=...
Docker / VPSDocker Secrets, systemd EnvironmentFile= with chmod 600, or an external vault (HashiCorp Vault, etc.)

.env.local is appropriate for local development only — combine chmod 600 with full-disk encryption to limit exposure. The repository’s .env.example is intentionally minimal to discourage placing production secrets on disk.

For the application-layer guarantees Baan provides, see the security model in the source repo.


The admin panel does not live at a predictable path like /admin. During setup, Baan generates a 16+ character random alphanumeric path unique to your installation:

https://your-blog.com/baan-admin/xK9mP2qRv3nT8wYj.../login

This URL is never linked from your public site. Anyone who doesn’t know the path cannot even reach the login form.

To rotate the secret path: Go to Admin → Settings → Security → Secret Path and generate a new one. The old URL immediately stops working.


Baan uses a dual-token authentication system designed to balance security and convenience:

TokenExpiryStoragePurpose
Access token60 minutesHttpOnly cookieAuthenticates each request
Refresh token30 daysHttpOnly cookieIssues new access tokens silently

Cookie security attributes (production):

  • __Host- / __Secure- prefix — prevents subdomain theft
  • HttpOnly — not accessible to JavaScript
  • SameSite=strict — blocks cross-site request forgery
  • Secure — controlled by the Cookie Security toggle (always on for serverless; opt-in for self-hosted)

Login rate limiting: 5 failed attempts per 15 minutes per IP. After 5 failures, further attempts are blocked until the window resets.

Tokens are HMAC-SHA256 signed using a session secret that is auto-generated and stored in the database on first run — no environment variable needed.


Whether session cookies are issued with the Secure flag is controlled by a toggle in Admin → Settings → Security → Cookie Security (HTTPS).

DeploymentDefaultToggle visible?
Serverless (Vercel / Cloud Run)Always SecureNo (always on; HTTPS is guaranteed)
Self-hosted (Docker / VPS)Not Secure on first installYes

Why a toggle instead of forcing Secure? A fresh self-hosted install is typically reached over plain HTTP first (right after docker compose up, before TLS is set up). If the server insisted on the Secure flag, the browser would silently drop the cookie and the operator could never complete the initial setup. Baan therefore bootstraps with non-Secure cookies and shows a persistent warning banner in the admin until the operator turns the toggle on.

Recommended sequence:

  1. Run docker compose up -d on your host
  2. Visit http://<your-host>:3000/baan-admin/setup and create the admin account
  3. Put your site behind TLS (Cloud Load Balancer, nginx + Let’s Encrypt, Cloudflare, etc.) — any approach that makes the site reachable at https://
  4. Log in over https://, then turn Require HTTPS for cookies ON
  5. The warning banner disappears

The toggle can only be enabled from a page already loaded over HTTPS — both the UI and the API refuse the change otherwise. This prevents accidentally locking yourself out by flipping it on while still browsing the admin over HTTP.


Restrict admin panel access to specific IP addresses or ranges. Only requests from listed IPs can reach the admin panel — all others receive a 403 response before hitting the login page.

Configure in Admin → Settings → Security → IP Allowlist:

  • Enter individual IPs or CIDR ranges (e.g. 192.168.1.0/24)
  • Add multiple entries, one per line
  • Leave empty to allow access from any IP

Add an additional HTTP authentication layer using browser-native Basic Auth. This prompts for a username/password before Baan even processes the request.

Configure in Admin → Settings → Security → Basic Auth:

ScopeWhat it protects
Entire siteBoth public pages and admin panel
Admin onlyAdmin panel only; public blog remains open
Public onlyPublic pages only; useful during private beta

Basic Auth is useful for staging environments or pre-launch sites where you want to prevent public access without setting up complex access controls.


Control which bots can crawl your site. Baan pre-configures 20 known bots in two categories:

AI Crawlers (10): ChatGPT, Claude, Perplexity, Gemini, and others that train or power AI models and answer engines.

Search Engine Bots (10): Googlebot, Bingbot, and major search engine crawlers.

Configure in Admin → Settings → Bot Management:

  • Toggle each bot individually
  • Apply bulk presets: Allow all, Block all, Allow search engines only, Allow AI only
  • Add custom rules for unlisted bots

These settings generate a robots.txt file and/or X-Robots-Tag headers automatically. You can also edit the robots.txt content directly.


For a production deployment, verify:

  • Admin URL path is 16+ random characters (not guessable)
  • Site is served over HTTPS
  • Require HTTPS for cookies toggle is ON (self-hosted; see Cookie Security)
  • IP allowlist is configured if you have a static IP
  • Login rate limiting is active (it is, by default)
  • .env file has restricted permissions (chmod 600 .env)
  • Admin URL is stored in a password manager (it is generated by the setup wizard and saved in the database — copy it on first login)

Beyond the configuration options above, Baan ships with the following protections enabled by default — no configuration required.

Selected public and admin endpoints enforce rate limits to reduce abuse:

EndpointLimit
Login5 requests / 15 min per IP
Analytics event recording300 requests / hour per IP
Headless CMS API300 requests / hour per API key by default
Revalidation API10 requests / min per IP
AI Assistant50 requests / hour per user
OGP fetch30 requests / min per IP

Clients that exceed the limit receive a 429 Too Many Requests response with X-RateLimit-* headers indicating when the window resets.

Baan fetches external URLs in two places: the OGP preview API and the WordPress importer. Both validate URLs before making any outbound request.

Blocked targets include:

  • localhost and 127.0.0.1
  • AWS instance metadata (169.254.169.254)
  • GCP metadata (metadata.google.internal)
  • Private networks (10.x.x.x, 172.16–31.x.x, 192.168.x.x)
  • Non-HTTP protocols (file://, ftp://, etc.)

Attempts to fetch these addresses are silently blocked and logged server-side.

All secret token comparisons (revalidation token, test API tokens) use constant-time comparison to prevent timing attacks. An attacker cannot infer whether a token is correct by measuring response time.

XML Entity Expansion (XXE / Billion Laughs)

Section titled “XML Entity Expansion (XXE / Billion Laughs)”

The WordPress XML importer disables entity expansion (processEntities: false). Malicious XML files crafted to cause exponential memory growth (Billion Laughs attack) are rejected without being processed.

Session cookies are issued with secure defaults:

  • HttpOnly — inaccessible to JavaScript
  • SameSite=strict for admin cookies; base session cookies use SameSite=lax
  • Secure — gated by the Cookie Security toggle (always on for serverless; opt-in for self-hosted after TLS is in place)
  • __Host- / __Secure- prefix — prevents subdomain cookie injection

If you discover a security vulnerability in Baan, please do not open a public GitHub Issue.

Report it privately via GitHub Private Vulnerability Reporting:

  1. Go to the Security tab of the repository
  2. Click “Report a vulnerability”
  3. Describe the issue, steps to reproduce, and potential impact

We aim to acknowledge reports within 48 hours and release a fix within 14 days for confirmed vulnerabilities.