Skip to content

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.

Marketix has three levels of access, enforced by middleware and model methods.

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.

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()).

Users with the member role (ProjectRole::Member) can access all project resources (links, QR codes, domains, pixels, statistics) but cannot manage team membership.

Membership is recorded in the project_user pivot table with two key columns:

ColumnTypePurpose
roleenum (admin, member)Controls what actions the user can perform within the project.
activebooleanWhether 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 prefixMiddlewareRequired
/admin/*auth, super_admin (EnsureSuperAdmin)users.super_admin = true
/project/{project}/teamauth, ProjectBindingMiddleware, project_admin (EnsureProjectAdmin)project role = admin or super-admin
/project/{project}/*auth, ProjectBindingMiddlewareany project membership or super-admin
Auth routes (login, 2FA, reset)guestunauthenticated

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.

Users can enable TOTP-based 2FA from their profile page (/profile). The flow:

  1. User initiates setup; a TOTP secret is generated and stored (encrypted) on the user record.
  2. User scans the QR code in an authenticator app and confirms by entering a valid code.
  3. On confirmation, recovery codes are generated, hashed with Hash::make(), and stored (encrypted). The plain codes are shown once.
  4. On subsequent logins, the TOTP challenge is presented at /auth/two-factor-challenge.
  5. 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.

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:

SettingValue / Source
Relying party IDDerived from APP_URL hostname
Allowed origins[APP_URL]
User handle secretPASSKEYS_USER_HANDLE_SECRET env var, falls back to APP_KEY
WebAuthn timeout60 000 ms (60 seconds)
Throttlethrottle: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.

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:

  • ForcePasswordChange middleware 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 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.

The following routes have explicit throttle middleware:

RouteLimit
POST /auth/two-factor-challenge6 per minute
GET /auth/two-factor-challenge/passkey/options6 per minute
POST /auth/two-factor-challenge/passkey6 per minute
POST /project/{project}/team/invitations/{invitation}/resend20 per minute
POST /{slug} (redirect password check)10 per minute
  • Set APP_DEBUG=false in 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.
  • 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 letsencrypt certificate resolver. Set a valid LETSENCRYPT_EMAIL in .env for certificate expiry notifications.
  • The db and redis containers are on the marketix_internal network and are not exposed to the host. Do not publish database or Redis ports in production.
  • Set strong random values for REDIS_PASSWORD and MYSQL_ROOT_PASSWORD in production.
  • Do not commit .env to source control.
  • The APP_KEY is 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.

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.

  • Set SESSION_ENCRYPT=true to encrypt session data at rest (requires a valid APP_KEY). The Docker Compose stack ships with SESSION_ENCRYPT: false; change this to true for production.
  • SESSION_LIFETIME=120 (120 minutes). Reduce for high-security environments.
  • SESSION_DRIVER=database is the default in Docker deployments; it is safer than file-based sessions in a multi-container setup.

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.