Security¶
Do not open a public issue for a security problem. Report it privately through GitHub's vulnerability reporting (Security → Report a vulnerability) or by email to the maintainer with "SECURITY" in the subject; the address is in the repository's SECURITY.md, together with the disclosure timeline (acknowledged within 3 working days, assessed within 10, a fix as soon as practical, an advisory with the fix).
Security fixes go into the latest minor release of the 0.x line; upgrade with
karni upgrade. What counts as a Karni vulnerability — access enforcement,
soft delete and scopes, authentication, module webhooks, insecure scaffold
defaults, karni commands writing more than they say — is listed there too.
What Karni does by default¶
The baseline a project gets from karni_settings() and the scaffold, with
nothing to remember. Each line names the setting or check that owns it.
Rows and fields. Every BaseViewSet has an access declaration (a
missing one fails karni check and denies every request); the declaration
is the request gate and the row filter, so a row outside the scope answers
404, never 403. Relation fields and ?expand= follow the target's read
scope. Roles are data (KARNI_ROLES), checks are cached per request, and
assert_isolated proves the isolation for every resource. See
access control and roles.
Authentication. Email login with Django's password validators; short-lived JWT access tokens (15 minutes) with refresh rotation and blacklisting on logout and password reset, or cookie sessions with the CSRF bootstrap; one-time tokens stored hashed, single-use, per purpose, with TTLs; an email-verification gate; optional TOTP 2FA and social login. See accounts.
Abuse. Per-IP throttles on every auth endpoint plus a per-account
throttle on login (KARNI_AUTH_THROTTLE_RATES); NUM_PROXIES set for the
shipped Caddy (KARNI_NUM_PROXIES, default 1) — set it to 0 whenever
Django is exposed without a proxy that rewrites X-Forwarded-For, or a
spoofed header defeats the per-IP throttles; the password-reset endpoint never reveals whether an address
exists; a registration honeypot (KARNI_ACCOUNTS_HONEYPOT_FIELD); and
karni.W501 warns when the throttle counters would live in a per-process
cache in production.
Observability. Every auth outcome is a structlog event on
karni.accounts with a user id or an address fingerprint, never an address
— see bots and security events.
AuditedModel records who created and last changed a row; models with
class Karni: audit = True also keep a field-level change history
(change history).
Transport and headers. The production profile sets HSTS (one year,
subdomains, preload), SECURE_SSL_REDIRECT behind the proxy's
X-Forwarded-Proto, secure and HttpOnly cookies, nosniff, a same-origin
referrer policy and X-Frame-Options: DENY; CORS is closed unless
CORS_ALLOWED_ORIGINS lists an origin. Startup validation refuses a missing
SECRET_KEY or empty ALLOWED_HOSTS with DEBUG off.
Admin. Mounted at KARNI_ADMIN_PATH (move it off /admin/). Django's
model permissions decide which screens a staff user may open; the roles'
grants decide which rows those screens show and accept (row scope, ADR 0002
P2): a staff user with permissions but no grant sees nothing, and an admin
that opts out needs a reason that karni check lists. No logo unless
KARNI_ADMIN_LOGO says so.
Welcome page. / on first boot (karni.welcome) is served only while
DEBUG is on and only until the project defines its own root route; with
DEBUG off the route is not mounted, the view answers 404, and system check
karni.E505 fails the boot if it is reachable anyway. It shows the presence
or absence of configuration (a database, Redis, a worker), never a value.
Modules. karni-payments verifies every webhook signature before it
touches a payment and treats replays as no-ops.
Supply chain. Dependabot (weekly, grouped), pip-audit and npm audit
per package, and gitleaks over the whole history run in CI
(.github/workflows/security.yml); releases publish through trusted
publishing with attestations, no stored tokens. The scaffold's
.env.example holds placeholders only.
What it does not do yet: field-level visibility and change history,
per-field encryption (deliberately out of scope), a content security policy
header (set one at the proxy), and upload content validation beyond Django's
ImageField — validate uploads in the model's clean() until that lands.