Security
This page covers the security model of a Marketix instance: how roles are enforced, what authentication protections are in place, how to harden a production deployment, and how to report vulnerabilities.
Roles and access model
Section titled “Roles and access model”Marketix has three levels of access, enforced by middleware and model methods.
Super-admin
Section titled “Super-admin”Super-admin status is stored as a boolean flag (super_admin) on the users table. It is set when creating a user via the admin UI (see User and project management) or via the marketix:create-admin Artisan command.
Every route under /admin is protected by the EnsureSuperAdmin middleware, which aborts with HTTP 403 if $request->user()->super_admin is not true. Super-admins can access any project without being a member.
Project-admin
Section titled “Project-admin”Within a project, a user with the admin role (stored in the project_user pivot role column as ProjectRole::Admin) is a project admin. The EnsureProjectAdmin middleware is applied to the per-project Team routes and aborts with 403 if the user’s role in the current project is not admin (super-admins bypass this check via User::isProjectAdmin()).
Member
Section titled “Member”Users with the member role (ProjectRole::Member) can access all project resources (links, QR codes, domains, pixels, statistics) but cannot manage team membership.
Project membership
Section titled “Project membership”Membership is recorded in the project_user pivot table with two key columns:
| Column | Type | Purpose |
|---|---|---|
role | enum (admin, member) | Controls what actions the user can perform within the project. |
active | boolean | Whether the membership is active. Set to true on assignment. |
The ProjectBindingMiddleware resolves the {project} route segment on every project-scoped request and calls User::canAccessProject(), which returns true for super-admins and for users who have an entry in project_user for that project (regardless of active state — the canAccess check is based on existence). Role-specific checks use User::roleInProject().
Route protection summary
Section titled “Route protection summary”| Route prefix | Middleware | Required |
|---|---|---|
/admin/* | auth, super_admin (EnsureSuperAdmin) | users.super_admin = true |
/project/{project}/team | auth, ProjectBindingMiddleware, project_admin (EnsureProjectAdmin) | project role = admin or super-admin |
/project/{project}/* | auth, ProjectBindingMiddleware | any project membership or super-admin |
| Auth routes (login, 2FA, reset) | guest | unauthenticated |
Authentication security
Section titled “Authentication security”Password-based login
Section titled “Password-based login”Email and password login is handled by AuthController. After successful password verification, if the user has 2FA enabled, a session key (auth.2fa.pending_id) is written and the user is redirected to the 2FA challenge screen. The session is regenerated on successful authentication.
Two-factor authentication (TOTP)
Section titled “Two-factor authentication (TOTP)”Users can enable TOTP-based 2FA from their profile page (/profile). The flow:
- User initiates setup; a TOTP secret is generated and stored (encrypted) on the user record.
- User scans the QR code in an authenticator app and confirms by entering a valid code.
- On confirmation, recovery codes are generated, hashed with
Hash::make(), and stored (encrypted). The plain codes are shown once. - On subsequent logins, the TOTP challenge is presented at
/auth/two-factor-challenge. - The challenge accepts either a TOTP code or a one-time recovery code. Recovery codes are consumed on use.
The 2FA challenge endpoints (/auth/two-factor-challenge/...) are rate-limited to 6 attempts per minute (throttle:6,1).
Disabling 2FA or regenerating recovery codes requires confirming the current password.
Passkeys (WebAuthn)
Section titled “Passkeys (WebAuthn)”Users can register hardware passkeys as a second factor via PasskeyManagementController. Passkeys are implemented via the laravel/passkeys package.
Passkey configuration in config/passkeys.php:
| Setting | Value / Source |
|---|---|
| Relying party ID | Derived from APP_URL hostname |
| Allowed origins | [APP_URL] |
| User handle secret | PASSKEYS_USER_HANDLE_SECRET env var, falls back to APP_KEY |
| WebAuthn timeout | 60 000 ms (60 seconds) |
| Throttle | throttle:6,1 (6 attempts per minute) |
The passkey challenge at login is served through /auth/two-factor-challenge/passkey/options and /auth/two-factor-challenge/passkey, both rate-limited.
Passkey names can be updated via PATCH /user/passkeys/{passkey}/name.
Forced password change
Section titled “Forced password change”A super-admin can set the force_password_change flag on any user account (from the user create or edit page). When the flag is set:
ForcePasswordChangemiddleware intercepts all authenticated requests and redirects the user to/password/change.- The only routes the user can reach are the password-change form, the password-change submission, and logout.
- On successful password update, the flag is cleared and the user is redirected to
/.
Password reset
Section titled “Password reset”Password resets use Laravel’s built-in broker (Password::sendResetLink()). The reset email is delivered via the active mailer. Super-admins can trigger a reset on behalf of any user from the admin user-edit page.
Rate limiting
Section titled “Rate limiting”The following routes have explicit throttle middleware:
| Route | Limit |
|---|---|
POST /auth/two-factor-challenge | 6 per minute |
GET /auth/two-factor-challenge/passkey/options | 6 per minute |
POST /auth/two-factor-challenge/passkey | 6 per minute |
POST /project/{project}/team/invitations/{invitation}/resend | 20 per minute |
POST /{slug} (redirect password check) | 10 per minute |
Production hardening checklist
Section titled “Production hardening checklist”Application
Section titled “Application”- Set
APP_DEBUG=falsein production. Leaving debug mode on exposes stack traces and environment variables to unauthenticated visitors. - Set
APP_ENV=production. - Generate a strong, random
APP_KEY(php artisan key:generate). Back it up — losing it invalidates encrypted data (2FA secrets, recovery codes, stored SMTP/Postal/S3 credentials). - Set
BCRYPT_ROUNDS=12(the default). Increase to 13–14 on hosts with spare CPU capacity for stronger password hashing.
Network and TLS
Section titled “Network and TLS”- All HTTP traffic is redirected to HTTPS by Traefik’s entrypoint redirect configuration in
docker-compose.yml. - TLS certificates are issued by Let’s Encrypt via the
letsencryptcertificate resolver. Set a validLETSENCRYPT_EMAILin.envfor certificate expiry notifications. - The
dbandrediscontainers are on themarketix_internalnetwork and are not exposed to the host. Do not publish database or Redis ports in production.
Secrets
Section titled “Secrets”- Set strong random values for
REDIS_PASSWORDandMYSQL_ROOT_PASSWORDin production. - Do not commit
.envto source control. - The
APP_KEYis used to encrypt stored settings secrets (SMTP password, Postal API key, S3 secret). Rotate it only if you have a plan to re-enter all encrypted settings afterwards.
Horizon dashboard
Section titled “Horizon dashboard”The Horizon dashboard at /horizon is already restricted to super-admins. The viewHorizon gate is defined in App\Providers\HorizonServiceProvider and returns true only when $user->super_admin === true. No additional hardening is needed.
Session
Section titled “Session”- Set
SESSION_ENCRYPT=trueto encrypt session data at rest (requires a validAPP_KEY). The Docker Compose stack ships withSESSION_ENCRYPT: false; change this totruefor production. SESSION_LIFETIME=120(120 minutes). Reduce for high-security environments.SESSION_DRIVER=databaseis the default in Docker deployments; it is safer than file-based sessions in a multi-container setup.
Vulnerability reporting
Section titled “Vulnerability reporting”There is no formal public security policy file in the Marketix repository. If you discover a security vulnerability, please report it privately to the NoiX maintainers before disclosing it publicly. Contact them via the NoiXdev GitHub organisation using a private channel (e.g. GitHub’s private vulnerability reporting on the repository, or direct contact with the maintainers). Include a description of the issue, steps to reproduce, and any proof of concept. Allow reasonable time for a fix to be prepared and released before public disclosure.