Built with Chinaski

Security Model

Security Model

Chinaski’s security design is simple to describe: verify everything the server is told, sign everything the browser loads, and slow down anyone who behaves badly. This page explains the four pieces this rests on: subresource integrity, the content security policy, client-IP trust, and rate limits/lockouts.

Subresource Integrity (SRI)

The admin interface’s self-hosted scripts and stylesheets (the admin UI’s own CSS, fonts, the editor, and settings scripts) carry computed integrity hashes. When the browser loads one of these files, it verifies it against the hash published in the page: a tampered or swapped script is refused before it can run.

Hands-on details:

  • Hashes are computed when the app starts. If you update those asset files on disk, restart the application so fresh hashes are published.
  • If an asset file was unreadable at startup, the page loads that file without the integrity attribute (with a warning in the log) rather than breaking the page.

Content Security Policy (CSP)

Every response carries a strict Content Security Policy:

  • Page content is locked to the site’s own origin by default; scripts and inline styles only run if the server tagged them with a nonce generated fresh for that request.
  • Form submissions are restricted to your own server; framing by other sites is blocked.
  • Analytics/bot-check integration (Cloudflare Turnstile) is the only allowed external contact point, and disappears entirely in airgap mode.
  • Alongside the CSP, responses also carry frame protection, content-type sniffing protection, a strict referrer policy, one-year HSTS (TLS is expected to terminate at your reverse proxy), and a Permissions-Policy that disables browser features the CMS never uses (camera, microphone, geolocation, payment, USB, and more).

Two deliberate exceptions, both safe and intentional: content previews relax the policy because they render your real theme’s HTML (those URLs require an admin session or a secret preview token), and public form pages are intentionally embeddable by other sites; their submissions still go only to your server.

Client-IP trust (X-Forwarded-For)

Rate limits, login lockouts, and logs all need the visitor’s real IP address. The CMS decides it like this:

  • If the connection arrives from a trusted proxy (by default, only loopback, the standard setup where a Caddy or nginx proxy faces the internet), the CMS reads the forwarded headers and trusts them.
  • If the connection arrives from anywhere else, the forwarded headers are ignored entirely and the real connection address is used; this is why binding the server to a public interface would let anyone defeat your rate limits.
  • A proxy on a non-loopback network (for example a Docker bridge) needs to be added via the CHINASKI_TRUSTED_PROXIES setting (comma-separated IP addresses or IPv4 ranges) so forwarded headers are honored. The shipped proxy examples use loopback and need no setting.

Rate limits and lockouts

Chinaski rate-limits the surfaces most likely to be abused, by IP address:

Surface Limit
Admin login Lockouts escalate: 1 minute after 5 failed attempts, 5 minutes after 10, 30 minutes after 20. Stop trying for about 10 minutes and the count resets.
Self-registration 5 attempts from one IP in quick succession triggers a 1-hour lockout (and registration itself must be enabled in settings first)
Password recovery 5 requests from one IP triggers a 1-hour lockout
Public form submissions Max 5 per hour per form, and 30 per hour across all forms, per IP; beyond that you get HTTP 429 “Too many submissions”
REST API Each API key is limited per minute (default 100 requests), counted atomically across all server workers; excess requests get HTTP 429 with a Retry-After header telling you when to try again
Suspicious URLs Known scanner probes (.env, .git/config, wp-login.php, wp-admin, phpmyadmin, and friends) are logged with full request details and answered with a bare 404 that gives nothing away

A few details worth knowing:

  • High login-failure activity is written to the event log, so repeated attacks get noticed even when nobody is watching the logs live.
  • Form rate limiting keys on the real client IP, so it stays accurate even when the CMS omits visitors’ IPs for privacy reasons (Do Not Track / Global Privacy Control signals, or a per-form IP-storage setting).
  • Instead of outright rejecting, abusive paths may be tarpitted: the response is deliberately delayed by a few seconds. This is non-blocking; legitimate visitors are unaffected while automated attacks waste time.

What this adds up to

  1. Incoming traffic is authenticated by session keys your server signs, never by browser-supplied values alone.
  2. Everything the admin browser loads is hash-verified (SRI) and policy-checked (CSP).
  3. Trust boundaries are explicit: the daemon knows which networks it may believe and refuses forwarded headers from anyone else.
  4. Abuse triggers throttling, delaying, or lockouts instead of errors that leak useful information.
  5. Every failed attempt and probe is logged, so incidents are visible after the fact, in the audit log, event log, and the admin dashboards.
Read this site in