Built with Chinaski

Hardening

Hardening

A Chinaski deployment ships locked down by default: the service runs in a systemd sandbox, files have restrictive permissions, the daemon binds to loopback behind a reverse proxy, and the CMS adds strict security headers to every response. This page explains what those protections are and a few things you can do to keep them intact.

Systemd sandboxing

The production systemd unit runs each instance as a dedicated chinaski user in a strict sandbox:

  • The whole filesystem is read-only except for the instance’s own directory; an exploit in the app cannot reach anything else on the host.
  • The instance gets private /tmp, cannot see home directories, and cannot touch kernel settings or create privileged files.
  • Memory is capped at 1 GB (with a 768 MB soft ceiling), CPU at 200%, and 256 concurrent tasks, so a runaway job can’t starve the host.
  • On shutdown the service stops gracefully, giving in-flight requests and a running build time to finish.
  • Restarts are automatic but throttled, and all output lands in the journal: read it with journalctl -u chinaski@<instance>.

These limits come from the unit file; tune them on larger hosts, but few sites need more than the shipped values.

File permissions

The installer sets restrictive permissions at install time and you don’t need to change them:

  • The application directory is owned by the chinaski user and can only be read by that user and group members.
  • The database file (chinaski.db) is readable only by the chinaski user and group.
  • The directory holding the env file (which contains your secret) is owner-only (0700) and the env file itself is 0600.
  • The backup directory is owner-only (0700).

If ownership drifts (for example, a deploy was run as the wrong user), the deploy script re-applies correct ownership to the application directory on the next run.

Keep the daemon off the public network

  • Bind the CMS to a loopback address (http://127.0.0.1:3001 is the example in the systemd unit) and let the reverse proxy face the internet. The daemon trusts forwarded client IPs only from loopback connections; binding it to a public interface would let visitors spoof their IP and defeat the rate limits and login lockouts.
  • If your proxy talks to the daemon from a non-loopback address (for example a Docker bridge network), set CHINASKI_TRUSTED_PROXIES to that IP range (comma-separated addresses or IPv4 ranges) so the real client IP is still recognized. Loopback is always trusted; nothing else is, unless you list it.
  • In offline (“EOTW”) mode hypnotoad serves plain HTTP directly with no proxy. The CMS then reads the real connection address and ignores forwarded headers entirely, so rate limiting works as-is. But there is no TLS in this mode: don’t send passwords over an untrusted network; restrict access with a VPN or firewall rules.

Airgap mode

Set CHINASKI_AIRGAP=1 before starting the server on isolated hosts. It turns off every outbound request (build webhooks, form webhooks, and the bot-check verification), and hides the verification widget even if keys are stored in the database. The CMS keeps working otherwise.

Response headers

The application adds security headers to every response on its own; no proxy configuration is needed for the basics:

  • It strips the Server header and sets frame protection (SAMEORIGIN), content-type sniffing protection, a strict referrer policy, and one-year HSTS (TLS is expected to terminate at your reverse proxy).
  • Browser features the CMS never uses (camera, microphone, geolocation, payment, USB, and more) are disabled via Permissions-Policy.
  • Inline scripts and styles are protected by a per-request nonce in the Content Security Policy. Submissions and form embedding are locked to the site’s own origin.
  • Two deliberate relaxations, both restricted to non-public surfaces: content previews loosen the policy so theme previews render correctly (they require an admin session or a secret preview token), and public form pages allow being embedded by other sites since that is their designed use, while form submissions still only post to your server.

The proxy examples (Caddy and nginx in deploy/) mirror these headers as belt-and-braces so they’re set even when the proxy answers with a cached error.

Scanner honeypots

The proxy examples route common scanner probes (.env, .git/config, wp-login.php, wp-admin, phpmyadmin, xmlrpc.php, and friends) to the CMS instead of the static server. Hits are logged and deliberately slowed rather than quietly 404’d. If you use the shipped proxy examples, this works automatically; keep the honeypot block when you adapt the config.

Read this site in