Security
what Kiln protects, and what stays yours.
This page lists what Kiln does to protect your install, your clients and their visitors, and what stays your responsibility. It describes only what the code does.
01Your responsibilities
- Run Kiln behind HTTPS (see Hosting) and set
TRUST_PROXY_HOPSto the number of proxies in front. - Keep
ADMIN_SECRET_KEYsecret and backed up, never in Git. - Turn on two-factor sign-in for every console account.
- Keep Node.js and the operating system updated, and back up the
data/folder. - Never set
KILN_DEMO,KILN_DEMO_PASSWORDorADMIN_DEMO_PASSWORDon a real install.
02Builder accounts
- Passwords are hashed with scrypt and a random salt per password. Passwords must have 10 to 200 characters.
- Sign-in takes the same time whether or not the account exists, and answers "Wrong e-mail or password." in both cases. Forgot password answers the same whether or not the e-mail is known.
- Sessions are random tokens. The database stores only their SHA-256 hash, so a copy of the database cannot be used to sign in. Sessions last 30 days; signing out deletes the session. A password reset signs out every session of that account, and so does disabling the account in the console.
- Cookies are
HttpOnly(scripts cannot read them) andSameSite=Lax. They get theSecureflag when the public URL (in.envor the console) starts withhttps://, orCOOKIE_SECURE=1is set. - Invite and reset links are random tokens stored only as hashes. Invites expire after 7 days, reset links after 2 hours (24 hours when created by the operator), and each works once. An existing account joins a workspace only while signed in as itself.
03Cross-site request forgery
Every request that changes something must carry the header x-kiln: 1. A browser cannot add a custom header to a request from another site without a CORS preflight, and Kiln's API never allows one. Together with SameSite cookies this blocks forged requests. The console's product pages use the same header.
04Rate limits
| What | Limit |
|---|---|
| Builder sign-in | 10 attempts per 15 minutes, per address and per e-mail |
| Sign-up (when open) | 5 per hour per address |
| Forgot password | 5 per hour per address |
| Contact form | 5 messages per 10 minutes per sender and site |
| Review decisions | 20 per 10 minutes per address |
| AI actions (plan, page, rewrite, chat, translate per page, blog draft) | 30 per minute per person |
| Rebuild from URL (site import) | 10 per minute per person |
| Console sign-in | Admin Kit lockout after repeated failures, with growing back-off |
Limits are kept in memory per process. Behind a proxy they need TRUST_PROXY_HOPS; only then does Kiln read the visitor's address from X-Forwarded-For (and the scheme from X-Forwarded-Proto). With 0 these headers are ignored, so nobody can fake them.
05Permissions
- Workspace members have a role (owner, editor, client, viewer) and the server checks it on every request. A user who is not a member gets "Site not found", not a hint that the site exists.
- Client role. On every save by a client, the server compares the new site with the stored one with all texts and images blanked out. Any other difference (sections, layouts, styles, link addresses, list length, theme, site settings, page addresses, languages, blog posts) is refused with a 403. Blog changes in a client's save are discarded. AI actions, publishing, review links, history restore and export need the editor role.
- Only agency staff create workspaces. Staff can open every workspace.
- Plan limits (sites, pages, languages, custom domains, Cloudflare, export) are enforced on the server.
06Published sites
- Escaping. Every text is HTML-escaped when the page is rendered.
- Rich text keeps only a whitelist of tags (
p,br,strong,b,em,i,u,a,ul,ol,li,h2,h3,h4,blockquote) and drops every attribute except a safehrefon links. - Links accept only
http(s):,mailto:,tel:, in-page anchors and site paths;javascript:and others are removed. Image sources accept http(s), site paths and raster data URLs, never SVG data URLs. - No user scripts by default. The
<head>code field exists only when the operator turns on Allow analytics code in site settings, and only workspace owners can save it. Even then, that code never runs on the builder's own address (/s/<site>/previews, the editor canvas and review links), only on the site's own domain. - Analytics ids (Google Analytics 4, Tag Manager, Plausible, Meta Pixel) are checked against their formats on every save; anything else is dropped, so these fields cannot carry script. The tracking code is Kiln's own and loads only the providers' standard scripts.
- Cookie consent. With the consent banner on, Google Analytics, Tag Manager and the Meta Pixel load only after the visitor clicks Accept; Decline is remembered and nothing loads. Plausible is cookieless and always loads. The site owner remains responsible for the banner text and the privacy policy.
- Third-party embeds (Google Maps, Cal.com or Calendly, YouTube, Vimeo) are not loaded with the page. The page shows a button; the visitor's browser contacts the provider only after a click. YouTube uses
youtube-nocookie.comand Vimeo itsdnt=1option. Booking embeds accept onlyhttps://cal.com,app.cal.comandcalendly.comaddresses; videos only YouTube and Vimeo ids. - Custom domains on the built-in hosting are served only after a DNS TXT record proves control of the domain, so nobody can point the server at a domain they do not own.
- Headers on hosted pages:
X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-originand aPermissions-Policythat turns off camera, microphone and geolocation. - Every AI answer is checked against the section schemas; unknown section types and fields are dropped. AI blog drafts are cleaned with the rich-text whitelist.
07Contact forms
A hidden honeypot field catches bots; at most 20 fields of up to 5,000 characters each are kept, and the whole message is limited to 20,000 characters. A valid e-mail and a message are required. The sender's address is stored only as a short hash. CSV exports neutralise cells starting with =, +, - or @, so the file cannot run formulas in a spreadsheet.
08Requests to outside addresses
Two features make the server fetch an address a user typed: Rebuild from URL and form webhooks. Both are guarded against server-side request forgery (being used to reach your private network):
- Only
httpandhttps(webhooks: onlyhttps), no user name or password in the address, and only ports 80, 443 and 8080. - The host name is resolved, and the request is refused if any address is private, loopback, link-local, carrier-grade NAT, multicast or reserved, in every IPv4 and IPv6 form (including IPv4-mapped, NAT64 and 6to4 addresses).
- The connection is pinned to the checked address, so a DNS answer that changes between the check and the request (DNS rebinding) cannot reach a private network.
- Rebuild from URL follows at most three redirects and checks every hop the same way. It reads at most 2 MB, waits at most 10 seconds, keeps only the page's text (scripts, styles and images are dropped) and is limited to 10 imports per minute per person.
- Form webhooks follow no redirects and time out after 5 seconds. An address that is not
https://is dropped when the site is saved. The call happens after the message is stored and never blocks the visitor; failures are not retried. The payload holds the site name and id, the page, the time and the form fields. Treat the webhook address like a secret: anyone who knows it can see what arrives there, and Kiln does not sign the request.
The AI endpoint URL (console, Any OpenAI-compatible endpoint) is not restricted this way, because only console owners and managers set it, and a local Ollama needs localhost.
09Images
Uploads are checked by their first bytes, not by the file name or the browser's claim. Only PNG, JPEG, WebP and GIF are accepted, up to 8 MB. SVG is refused. Files get random names. Stock photos are imported only from https://images.pexels.com, and redirects are refused, so an import cannot be turned into a request to another server.
10API keys
Keys saved in the console are encrypted at rest by the Admin Kit (AES-256-GCM) with ADMIN_SECRET_KEY. They are write-only: the console can replace or test them but never shows them again, and they never appear in the audit log or in settings exports. Only console owners can change them. Keys set in .env are read from the environment and not stored.
A workspace's own AI key (when the operator allows them) is stored in Kiln's database, encrypted with AES-256-GCM using a key derived from the same ADMIN_SECRET_KEY. If that key changes, workspaces must enter their own key again.
11Stripe
Webhooks are accepted only with a valid Stripe-Signature (HMAC-SHA256 with your signing secret) and a timestamp no older than 5 minutes. Each event is applied once. Payments are recorded only from these verified events.
12The operator console
The Admin Kit provides: owner, manager and viewer roles; two-factor sign-in with an authenticator app and recovery codes; sign-in lockout; an audit log of every change with who made it; settings history with rollback. Every change on the product pages (workspaces, credits, plans, packs, users) writes an audit entry.
13Read-only public demo
For a public demo only:
KILN_DEMO=1makes the builder read-only and seeds example sites from the templates (labelled as examples). Every change is refused with "This is a read-only demo. Nothing can be changed here."; generated sites stay in the visitor's browser tab; forms answer "This is a demo site: messages are not stored."KILN_DEMO_PASSWORD(8 or more characters) is the password of the demo builder accountdemo@kiln.example.ADMIN_DEMO_PASSWORDmakes the console a read-only demo with a published viewer account. There is no owner in that mode, and every write is refused.