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.
Secrets Management
Section titled “Secrets Management”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.
| Platform | Recommended approach |
|---|---|
| Vercel | Project Settings → Environment Variables (encrypted at rest, team permission control) |
| AWS | Secrets Manager + IAM Role; inject via the ECS Task Definition’s secrets field |
| Google Cloud | Secret Manager + gcloud run services update --set-secrets=... |
| Docker / VPS | Docker 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.
Secret Admin URL
Section titled “Secret Admin URL”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.../loginThis 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.
Authentication
Section titled “Authentication”Baan uses a dual-token authentication system designed to balance security and convenience:
| Token | Expiry | Storage | Purpose |
|---|---|---|---|
| Access token | 60 minutes | HttpOnly cookie | Authenticates each request |
| Refresh token | 30 days | HttpOnly cookie | Issues new access tokens silently |
Cookie security attributes (production):
__Host-/__Secure-prefix — prevents subdomain theftHttpOnly— not accessible to JavaScriptSameSite=strict— blocks cross-site request forgerySecure— 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.
Cookie Security (HTTPS-Only)
Section titled “Cookie Security (HTTPS-Only)”Whether session cookies are issued with the Secure flag is controlled by a toggle in Admin → Settings → Security → Cookie Security (HTTPS).
| Deployment | Default | Toggle visible? |
|---|---|---|
| Serverless (Vercel / Cloud Run) | Always Secure | No (always on; HTTPS is guaranteed) |
| Self-hosted (Docker / VPS) | Not Secure on first install | Yes |
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:
- Run
docker compose up -don your host - Visit
http://<your-host>:3000/baan-admin/setupand create the admin account - Put your site behind TLS (Cloud Load Balancer, nginx + Let’s Encrypt, Cloudflare, etc.) — any approach that makes the site reachable at
https:// - Log in over
https://, then turn Require HTTPS for cookies ON - 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.
IP Allowlisting
Section titled “IP Allowlisting”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
Basic Auth (HTTP Authentication)
Section titled “Basic Auth (HTTP Authentication)”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:
| Scope | What it protects |
|---|---|
| Entire site | Both public pages and admin panel |
| Admin only | Admin panel only; public blog remains open |
| Public only | Public 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.
Bot Management
Section titled “Bot Management”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.
Security Checklist
Section titled “Security Checklist”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)
-
.envfile 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)
Built-in Security Protections
Section titled “Built-in Security Protections”Beyond the configuration options above, Baan ships with the following protections enabled by default — no configuration required.
Rate Limiting
Section titled “Rate Limiting”Selected public and admin endpoints enforce rate limits to reduce abuse:
| Endpoint | Limit |
|---|---|
| Login | 5 requests / 15 min per IP |
| Analytics event recording | 300 requests / hour per IP |
| Headless CMS API | 300 requests / hour per API key by default |
| Revalidation API | 10 requests / min per IP |
| AI Assistant | 50 requests / hour per user |
| OGP fetch | 30 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.
SSRF Protection
Section titled “SSRF Protection”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:
localhostand127.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.
Timing-Safe Secret Comparison
Section titled “Timing-Safe Secret Comparison”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.
Secure Cookie Attributes
Section titled “Secure Cookie Attributes”Session cookies are issued with secure defaults:
HttpOnly— inaccessible to JavaScriptSameSite=strictfor admin cookies; base session cookies useSameSite=laxSecure— 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
Reporting Security Vulnerabilities
Section titled “Reporting Security Vulnerabilities”If you discover a security vulnerability in Baan, please do not open a public GitHub Issue.
Report it privately via GitHub Private Vulnerability Reporting:
- Go to the Security tab of the repository
- Click “Report a vulnerability”
- 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.