Laravel versions covered: Laravel 10, 11, and 12. Most steps apply to all versions. Version-specific commands are noted where relevant.

Why Default Laravel Isn't Production-Ready

Laravel's defaults are designed to help developers move fast. APP_DEBUG=true in .env.example, permissive CORS by default, no rate limiting on auth routes out of the box — these make sense during development. They're dangerous in production. This guide covers what to change before you push to a live environment.

Step 1: Lock Down Your .env File

Your .env file contains every secret your application has. Three things are non-negotiable: set APP_DEBUG=false and APP_ENV=production before any deployment, verify your web server cannot serve the .env file directly (test with curl https://yourdomain.com/.env — it should return 404, not file contents), and never commit .env to version control.

For Nginx, add this to your server block:

location ~ /\.env {
    deny all;
    return 404;
}

Step 2: Configure Proper Security Headers

Create a middleware that adds security headers to every response. Add it to the global middleware stack in bootstrap/app.php (Laravel 11+):

// In your SecurityHeadersMiddleware handle() method:
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline';");

Step 3: Enable and Configure Rate Limiting

Laravel's built-in rate limiter is excellent but needs to be applied to the right routes. In AppServiceProvider (or a dedicated RateLimitServiceProvider), define limiters for your auth routes:

RateLimiter::for('login', function (Request $request) {
    return Limit::perMinute(5)->by($request->ip());
});
RateLimiter::for('password-reset', function (Request $request) {
    return Limit::perHour(3)->by($request->ip());
});

Then apply the throttle middleware to your auth routes: ->middleware('throttle:login'). Don't forget to rate limit your API routes as well — Laravel Sanctum doesn't do this automatically.

Step 4: Harden Session Configuration

In config/session.php, set: 'secure' => true (HTTPS only), 'http_only' => true (no JavaScript access), 'same_site' => 'strict' (CSRF protection), and 'encrypt' => true (encrypt session data at rest). Also set a reasonable lifetime — the default 120 minutes is often too long for sensitive applications.

Step 5: Use HTTPS Everywhere with HSTS

Add this to your AppServiceProvider::boot():

if (app()->environment('production')) {
    URL::forceScheme('https');
}

Step 6: Restrict File Upload Types and Sizes

Never trust file type validation based solely on the MIME type or extension. Use a combination: validate the extension against an allowlist, validate the MIME type, and use a library like league/mime-type-detection to inspect file content directly. Set explicit max file sizes in both your validation rules and php.ini/nginx.conf.

Store uploaded files outside the public directory or in S3/cloud storage. Never store user uploads in public/ — they become directly accessible at a predictable URL.

Step 7: Enable Laravel's Built-In CSRF Protection

Laravel's CSRF middleware is enabled by default, but developers often accidentally exclude routes. Audit your VerifyCsrfToken middleware's $except array — every route listed there is CSRF-unprotected. Only exclude routes that genuinely need it (typically webhook endpoints), and protect those with signature verification instead.

Step 8: Configure Database User Permissions Correctly

Create a dedicated database user for your Laravel application with only the permissions it needs: SELECT, INSERT, UPDATE, DELETE on the application database. Run migrations with a separate user that has ALTER and CREATE TABLE permissions — never use the same credentials for migrations and application runtime.

Step 9: Log Security Events

Use Laravel's event system to log authentication events. Listen for Login, Failed, Logout, and PasswordReset events and write them to a dedicated security log channel. This creates an audit trail that's invaluable during incident investigation.

Configure a separate log channel in config/logging.php that writes to a file your application can write to but cannot delete (append-only file permissions on the OS level).

Step 10: Protect Sensitive Routes with Additional Verification

For routes that perform sensitive operations (changing email, changing password, deleting account, exporting data), require password confirmation using Laravel's built-in password.confirm middleware. This prevents session hijacking from resulting in account takeover.

Step 11: Remove Debug Routes and Telescope in Production

Laravel Telescope, Debugbar, and similar development tools must never be accessible in production. Gate Telescope behind an email allowlist at minimum, or disable it entirely in production. Check your routes with php artisan route:list and remove any debug or development routes.

Step 12: Set Up Cloudflare WAF Rules

Put Cloudflare in front of every production Laravel application. Enable: Bot Fight Mode, OWASP Core Ruleset (at Medium sensitivity to start), rate limiting rules for your auth paths, and a custom rule to block requests with suspicious payloads to your API endpoints. Set your origin server to only accept connections from Cloudflare IP ranges — this ensures attackers can't bypass the WAF by hitting your origin directly.

Want Us to Audit Your Laravel App?

We run all 12 checks above plus a full OWASP Top 10 assessment on production Laravel applications. You receive a detailed report within 5 business days.