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.
What Is Protected and How
Section titled “What Is Protected and How”| Area | Mechanism | Proof |
|---|---|---|
| Staff and customer login | JWT: 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 permissions | module 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 createsuperuser | access concept.md; 488 @access-security scenarios, 0 failed |
| Access management | access.manage exists only in the built-in Administrator role; a custom role cannot hold it | access pytest, ACCESS_MANAGE_RESERVED |
| Admin API | one gate (AccessGateMiddleware) in front of every admin route; an unmapped route, an error inside the gate or an unknown mode all refuse | access tests/security/; manage.py access_routes --unmapped on the reference stack lists none |
| Django admin | superusers only; every role is refused, Administrator included. The CMS replaced it | access test_matrix.py |
| Superusers | bypass the permission check; every superuser write leaves an audit row | access test_matrix.py, gate.bypass rows |
| Kill switch | ACCESS_GATE_MODE (enforce, observe, off) is a setting only; no API or database row changes it | access install.md |
| OpenAPI | the schema, Swagger and ReDoc are staff-only unless API_SCHEMA_PUBLIC is set | service 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.
Application Tokens
Section titled “Application Tokens”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.
The Public Surface
Section titled “The Public Surface”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-Foronly as far asDRF_NUM_PROXIESallows. Unset, a client can spoof its address, socheck --deployfails withvolkanos.E001until it is set;0means no proxy.
Data Protection
Section titled “Data Protection”- 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.
Supply Chain and Process
Section titled “Supply Chain and Process”- 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.yamlorpackage-lock.json). masteranddeveloptake changes only through a pull request with one approval and thelint,testandsecret-scanchecks. 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 bddends with 1264 passed, 0 failed and 15 skipped, andmake bdd TAGS=@access-securitywith 507 passed, both with the gate inenforce(measured 2026-10-10).
Not Yet
Section titled “Not Yet”- 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.
Shared Responsibility
Section titled “Shared Responsibility”Self-hosted means the following are yours:
- TLS in front of the service.
DRF_NUM_PROXIES, required:check --deployfails withvolkanos.E001until it is set. Set it to the number of proxies in front of the app whoseX-Forwarded-Forentries you trust,0without a proxy. For example1when 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_SECRETand the wholesettings_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 (W010warns otherwise).API_SCHEMA_PUBLICstays 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
observefor an installation with its own Django apps: Upgrade.
Responsible Disclosure
Section titled “Responsible Disclosure”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