Skip to content

Security

Staff roles, the admin gate and application tokens come from entirius-django-access, released with Platform 4.0 and a major of each of the seven key modules. An installation on an earlier release still uses staff-only checks and per-module keys. The design: Access and API Tokens.

AreaMechanismProof
Staff and customer loginJWT: staff api/token/ through SimpleJWT (signed with SECRET_KEY), the accounts module’s tokens signed with JWT_SECRET; failed logins on api/token/ and on the CMS login route customer/tokens/ are limited per user and address, per address, and per login (50 an hour from any address)service apps/platform/auth_views.py, access services/login_guard.py
Staff permissionsmodule areas with read and write; four built-in roles (Administrator, Manager, Editor, Viewer); staff without a role get nothing beyond a small baseline (their own me, reference lists, the notifications inbox); Administrators create staff accounts in the CMS (Access → Staff), superusers still come from createsuperuseraccess concept.md; 488 @access-security scenarios, 0 failed
Access managementaccess.manage exists only in the built-in Administrator role; a custom role cannot hold itaccess pytest, ACCESS_MANAGE_RESERVED
Admin APIone gate (AccessGateMiddleware) in front of every admin route; an unmapped route, an error inside the gate or an unknown mode all refuseaccess tests/security/; manage.py access_routes --unmapped on the reference stack lists none
Django adminsuperusers only; every role is refused, Administrator included. The CMS replaced itaccess test_matrix.py
Superusersbypass the permission check; every superuser write leaves an audit rowaccess test_matrix.py, gate.bypass rows
Kill switchACCESS_GATE_MODE (enforce, observe, off) is a setting only; no API or database row changes itaccess install.md
OpenAPIthe schema, Swagger and ReDoc are staff-only unless API_SCHEMA_PUBLIC is setservice main/settings.py

The per-login limit has a price: anyone who knows a staff username can hold it at 429 for up to an hour.

Areas live on the views themselves (access_area), so a module declares what its own routes need. How to write a protected view: API v2 Standard.

Machines (storefronts, widgets, ERP jobs) call the key routes with an application token in X-API-KEY. A token is opaque, shown once and stored as a SHA-256 hash, and it never opens an admin route. No token expires unless someone sets an expiry or rotates it.

Scopes, the channel pin, expiry, rotation and revocation: Application Tokens. Why keys became tokens, and what that costs: Access and API Tokens.

Public by design: the catalogue and content (no credentials), and with a publishable token storefront carts and orders, contact forms, booking and newsletter subscription. What protects them is scope, channel pin and, where the module has them, throttles (checkout has none yet), not secrecy.

  • The gate does nothing outside the admin set: no authentication, no cache, no database query. The access tests assert zero queries on public routes.
  • Contact forms and booking throttle per token and visitor address, with a per-token ceiling (20 times the default visitor rate), so one visitor holding the public key cannot silence the form for everyone.
  • Every per-address throttle trusts X-Forwarded-For only as far as DRF_NUM_PROXIES allows. Unset, a client can spoof its address, so check --deploy fails with volkanos.E001 until it is set; 0 means no proxy.
  • GDPR erasure (accounts and checkout) opens only with a secret token. A token pinned to a channel erases only in that channel; an unpinned one erases in every channel.
  • Exports and downloads of personal data count as writes, so Viewer and Editor cannot export PII.
  • The audit trail records every access change (roles, grants, applications, tokens, legacy import and purge) and every superuser write. It records no reads and no refusals, and a token keeps only its last-use time. No audit row or log line holds a raw token or its hash, and no API response shows the raw value after the one that issues it; the release baseline scanned service and worker logs and the cache for token values and found none.
  • Legacy module keys are imported as tokens with the same secret, so no caller breaks on upgrade day. They have no automatic expiry, and the plaintext rows stay in the old tables until someone purges them: Legacy Keys.
  • Secret scanning: gitleaks in pre-commit and in CI over the full history, GitHub push protection on every push. See Secret Scanning.
  • Python packages publish to PyPI through trusted publishing (OIDC, id-token: write): no stored upload token. See CI/CD.
  • Each repository commits its lockfile (uv.lock, pnpm-lock.yaml or package-lock.json).
  • master and develop take changes only through a pull request with one approval and the lint, test and secret-scan checks. Repository admins can bypass the ruleset, and releases are merged that way. See Git Flow.
  • CodeQL runs on pull requests through GitHub’s default setup, and Dependabot alerts are on. Dependabot version updates are configured in the CMS repository only.
  • Known gaps are listed under Not Yet, and what an installation must set itself under Shared Responsibility. On the harness, make bdd ends with 1264 passed, 0 failed and 15 skipped, and make bdd TAGS=@access-security with 507 passed, both with the gate in enforce (measured 2026-10-10).
  • Staff MFA. Not built. The design: TOTP mandatory for Administrators and superusers, with recovery codes. Today a staff password alone signs in.
  • Staff SSO. Not in this release.
  • Checkout storefront throttling. Cart and order routes have no throttle of their own yet.
  • Audit retention. Audit rows are kept forever; there is no retention setting.

Self-hosted means the following are yours:

  • TLS in front of the service.
  • DRF_NUM_PROXIES, required: check --deploy fails with volkanos.E001 until it is set. Set it to the number of proxies in front of the app whose X-Forwarded-For entries you trust, 0 without a proxy. For example 1 when a CDN, a reverse proxy and an app-server proxy sit in front; the right value depends on how each hop writes the header. Measure it, never assume it.
  • Secrets: SECRET_KEY, JWT_SECRET and the whole settings_local.py, out of the repository.
  • A shared cache (Redis). With a per-process cache a revoked permission reaches other processes only after the timeout (django_access.W002).
  • ACCESS_GATE_MODE = "enforce" in production (W010 warns otherwise). API_SCHEMA_PUBLIC stays off.
  • People: who holds superuser (the only Django admin users) and who holds Administrator.
  • Token rotation: no token expires unless someone sets an expiry; rotate what the API and CMS flag rotation_due.
  • Legacy keys: decide when each key is rotated or expired, then purge the plaintext rows. Backups taken before the purge still hold them. The commands: Legacy Keys.
  • The upgrade preflight before the first deploy, and a first deploy in observe for an installation with its own Django apps: Upgrade.

Found a vulnerability? Report it privately, never in a public issue:

  • GitHub private vulnerability reporting (preferred): the repository’s Security tab, then Report a vulnerability.
  • E-mail: [email protected].

The policy is the organisation’s SECURITY.md, which GitHub shows on every public repository. Machine-readable contact: /.well-known/security.txt.

Application Tokens: create, send, rotate and revoke application tokens