95% of Cloud Security Failures Are Misconfigurations: A Laravel Deployment Checklist
95% of cloud security failures stem from human error, not platform vulnerabilities. This checklist covers the most common misconfigurations in Laravel deployments: exposed storage buckets, overly permissive IAM, missing network segmentation, database exposure, Redis security, leaked .env files, and debug endpoints left open in production.
Every major cloud provider, AWS, GCP, Azure, operates on a shared responsibility model. They secure the physical infrastructure, the hypervisors, and the network backbone. You secure everything you put on top: your application code, your configuration, your access controls.
The uncomfortable truth is that 95% of cloud security failures come from the customer side of that line. Not from zero-days in the platform. Not from sophisticated nation-state attacks. From misconfigurations. Human error. A checkbox left unchecked. A security group rule that is too permissive. An .env file that is publicly accessible.
For Laravel teams, this is both a challenge and an opportunity. The framework gives you excellent defaults, but those defaults only protect you if you deploy them correctly. This guide is a practical checklist for the most common cloud misconfigurations that affect Laravel deployments.
1. Exposed Storage Buckets
This is the number one cause of cloud data breaches, and it keeps happening because it is so easy to get wrong.
The Problem
You create an S3 bucket for user uploads. During development, you set it to public so you can test easily. You forget to change it before production. Now every file your users upload, profile photos, documents, invoices, is accessible to anyone who can guess or enumerate the URL.
The Laravel Checklist
Check your filesystem configuration:
// config/filesystems.php
'disks' => [
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'visibility' => 'private', // THIS IS CRITICAL
],
],
Always use private visibility by default. When users need to download files, generate temporary pre-signed URLs:
use Illuminate\Support\Facades\Storage;
// Generate a URL that expires in 5 minutes
$url = Storage::disk('s3')->temporaryUrl(
'invoices/invoice-2026-001.pdf',
now()->addMinutes(5)
);
AWS bucket policy hardening:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPublicAccess",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalAccount": "YOUR_ACCOUNT_ID"
}
}
}
]
}
Enable S3 Block Public Access at the account level, not just the bucket level. This acts as a safety net even if someone misconfigures an individual bucket:
aws s3control put-public-access-block \
--account-id YOUR_ACCOUNT_ID \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
2. Overly Permissive IAM Policies
The Problem
IAM (Identity and Access Management) policies control what your application, your deployment pipeline, and your team members can do in your cloud account. The most common mistake is using overly broad policies because they "just work."
A Laravel application running on EC2 or ECS with AdministratorAccess or AmazonS3FullAccess has far more permissions than it needs. If the application is compromised, the attacker inherits all of those permissions.
The Checklist
Create purpose-specific IAM roles for your Laravel application:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-app-uploads/*"
},
{
"Effect": "Allow",
"Action": [
"sqs:SendMessage",
"sqs:ReceiveMessage",
"sqs:DeleteMessage"
],
"Resource": "arn:aws:sqs:us-east-1:YOUR_ACCOUNT:your-app-queue"
},
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"ses:FromAddress": "noreply@yourdomain.com"
}
}
}
]
}
Key principles:
- Specify exact resources (bucket names, queue ARNs) instead of using wildcards.
- Limit actions to exactly what your application needs. If your app only reads and writes to S3, do not grant ListBucket or DeleteBucket.
- Use conditions to further restrict access. The SES example above limits sending to a specific From address.
- Never use AdministratorAccess or PowerUserAccess for application roles.
For Laravel Vapor
If you use Vapor, review the IAM role it creates. Vapor needs broad permissions for deployment, but your application's runtime role should be scoped down:
# vapor.yml
environments:
production:
runtime: 'php-8.3:al2'
region: 'us-east-1'
memory: 1024
role: 'arn:aws:iam::YOUR_ACCOUNT:role/your-scoped-app-role'
3. Missing Network Segmentation
The Problem
Your database, Redis instance, and application server are all in the same network with no restrictions between them. If any one component is compromised, the attacker has unrestricted access to everything else.
The Checklist
Security group rules for a typical Laravel stack:
Application Server (EC2/ECS):
Inbound: TCP 443 from 0.0.0.0/0 (HTTPS)
Inbound: TCP 80 from 0.0.0.0/0 (HTTP, redirect to HTTPS)
Outbound: TCP 3306 to database-sg (MySQL)
Outbound: TCP 6379 to redis-sg (Redis)
Outbound: TCP 443 to 0.0.0.0/0 (External APIs)
Database (RDS):
Inbound: TCP 3306 from app-server-sg ONLY
Outbound: None required
Redis (ElastiCache):
Inbound: TCP 6379 from app-server-sg ONLY
Outbound: None required
Critical rules:
- Databases should never have public IP addresses or be in public subnets.
- Redis should never be accessible from the internet.
- Use private subnets for databases and caches, with NAT gateways only if they need outbound internet access.
- SSH access (port 22) should be restricted to a bastion host or VPN, never open to 0.0.0.0/0.
For Laravel Forge
Forge provisions servers with sensible defaults, but verify:
# Check what ports are open on your Forge-provisioned server
sudo ufw status
# Expected output should look like:
# To Action From
# -- ------ ----
# 22 ALLOW Anywhere (should be restricted)
# 80 ALLOW Anywhere
# 443 ALLOW Anywhere
If MySQL port 3306 is listed as ALLOW from Anywhere, fix it immediately:
sudo ufw delete allow 3306
sudo ufw allow from 10.0.0.0/8 to any port 3306
4. Database Exposure
The Problem
Your database is accessible from the public internet. This happens more often than you think, especially on DigitalOcean, Hetzner, or self-managed servers where the database runs on the same machine as the application.
The Checklist
MySQL configuration hardening:
# /etc/mysql/mysql.conf.d/mysqld.cnf
# Bind to localhost or private network only
bind-address = 127.0.0.1
# Disable local file loading (prevents LOAD DATA INFILE attacks)
local-infile = 0
# Require secure transport for remote connections
require-secure-transport = ON
# Disable symbolic links
symbolic-links = 0
Laravel database configuration:
// config/database.php
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true, // Enable strict mode
'options' => extension_loaded('pdo_mysql') ? [
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
] : [],
],
Create a dedicated database user with minimal privileges:
-- Do NOT use the root user for your application
CREATE USER 'laravel_app'@'10.0.%.%' IDENTIFIED BY 'strong_random_password';
-- Grant only the permissions your app needs
GRANT SELECT, INSERT, UPDATE, DELETE ON your_database.* TO 'laravel_app'@'10.0.%.%';
-- If you need migrations from the app (not recommended for production)
-- GRANT CREATE, ALTER, DROP, INDEX ON your_database.* TO 'laravel_app'@'10.0.%.%';
FLUSH PRIVILEGES;
For production, run migrations from a separate deployment user or CI pipeline, not from the application's runtime database user.
5. Redis and Queue Security
The Problem
Redis has no authentication enabled by default. If it is accessible on the network, anyone can connect and read your session data, cached application data, and queued jobs (which often contain serialised objects with sensitive data).
The Checklist
Redis configuration:
# /etc/redis/redis.conf
# Bind to localhost only
bind 127.0.0.1
# Require a password
requirepass your_strong_redis_password_here
# Disable dangerous commands
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
rename-command DEBUG ""
rename-command SHUTDOWN ""
# Enable TLS if Redis is on a separate server
# tls-port 6379
# port 0
# tls-cert-file /path/to/redis.crt
# tls-key-file /path/to/redis.key
# tls-ca-cert-file /path/to/ca.crt
Laravel Redis configuration:
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'read_timeout' => 60,
],
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
'session' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_SESSION_DB', '2'),
],
],
Using separate databases for cache, session, and queue data means a FLUSHDB on one does not wipe the others.
6. Leaked .env Files
The Problem
Your .env file contains database credentials, API keys, encryption keys, and mail server passwords. If it is accessible via your web server, you have handed an attacker everything they need.
This is one of the most common vulnerabilities StackShield detects in free scans.
The Checklist
Nginx configuration to block dotfiles:
# /etc/nginx/sites-available/your-app.conf
server {
# Block access to ALL dotfiles
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Extra explicit block for .env
location = /.env {
deny all;
}
location = /.env.backup {
deny all;
}
# Block common backup file patterns
location ~* \.(sql|bak|old|orig|save|swp|swo)$ {
deny all;
}
}
Verify your .gitignore:
.env
.env.backup
.env.production
.env.staging
*.sql
Check that .env is not in your deployment artifact. If you use a CI/CD pipeline, make sure the .env file is not included in the build output. Environment variables should be injected at runtime, not shipped with the code.
Test your configuration by running a scan. StackShield's free scan checks for exposed .env files, or use the header check tool to verify your security headers are set correctly.
7. Debug Mode and Development Endpoints in Production
The Problem
APP_DEBUG=true in production exposes full stack traces, database queries, environment variables, and file paths. Laravel's debug page (powered by Ignition) is incredibly helpful during development and incredibly dangerous in production.
The Checklist
Ensure APP_DEBUG is false:
APP_DEBUG=false
APP_ENV=production
Remove or protect development routes:
// routes/web.php
// NEVER do this in production
if (app()->environment('local')) {
Route::get('/debug-info', function () {
return phpinfo();
});
}
// Better: use middleware that checks the environment
Route::middleware('dev-only')->group(function () {
// Development-only routes
});
Block common debug endpoints in nginx:
# Block PHP info, debug, and test endpoints
location ~* ^/(phpinfo|info|test|debug|adminer|phpmyadmin) {
deny all;
}
# Block Telescope in production (if not using authentication)
location /telescope {
deny all;
}
# Block Horizon access (use authentication instead)
# location /horizon {
# deny all;
# }
Laravel Telescope and Horizon should use authentication gates:
// app/Providers/TelescopeServiceProvider.php
protected function gate(): void
{
Gate::define('viewTelescope', function ($user) {
return in_array($user->email, [
'admin@yourdomain.com',
]);
});
}
Check your application for exposed debug endpoints with StackShield's security checks. The free scan will flag APP_DEBUG=true and other common misconfigurations.
8. Missing HTTPS and Security Headers
The Problem
Your application runs over HTTPS, but is it enforced? Are you redirecting HTTP to HTTPS? Are security headers set correctly?
The Checklist
Force HTTPS in Laravel:
// app/Providers/AppServiceProvider.php
public function boot(): void
{
if ($this->app->environment('production')) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
}
Nginx HTTPS configuration:
# Redirect HTTP to HTTPS
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Modern TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}
Test your security headers with the StackShield Header Check tool.
The Complete Deployment Checklist
Print this out. Run through it before every production deployment.
Storage and Files
- S3/storage buckets set to private visibility
- Block Public Access enabled at account level
- Pre-signed URLs used for file downloads
- .env file blocked by web server configuration
- .env file in .gitignore and not in deployment artifacts
- Backup files (.sql, .bak) not accessible via web
Access Control
- Application IAM role follows least privilege
- No wildcard (*) resources in IAM policies
- Database user has minimal permissions (no GRANT, no CREATE on production app user)
- Redis requires authentication
- Dangerous Redis commands are disabled
Network
- Database not accessible from public internet
- Redis not accessible from public internet
- Security groups restrict traffic to required ports and sources
- SSH restricted to bastion host or VPN
- No unnecessary ports open (check with
nmapor StackShield free scan)
Application
- APP_DEBUG=false in production
- APP_ENV=production
- Debug endpoints (phpinfo, Telescope, Horizon) are authenticated or removed
- HTTPS enforced with redirect from HTTP
- Security headers present (HSTS, X-Content-Type-Options, X-Frame-Options)
- Session cookies marked Secure, HttpOnly, SameSite
Monitoring
- Error logging configured (do not log to local files that fill up)
- Failed login attempts are logged and monitored
- Security headers monitored for changes. Set up continuous monitoring.
Cloud security is configuration security. The platform is not going to fail you. Your configuration might. Run through this checklist, automate what you can, and use tools like StackShield to continuously verify that your deployment matches your intentions.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.
Frequently Asked Questions
What percentage of cloud security breaches are caused by misconfigurations?
Industry research consistently finds that around 95% of cloud security failures result from customer misconfigurations rather than vulnerabilities in the cloud platform itself. This includes misconfigured storage buckets, overly permissive IAM policies, exposed databases, and missing network segmentation. Cloud providers operate on a shared responsibility model. They secure the infrastructure; you secure your configuration. Most breaches happen because teams get the configuration wrong, not because AWS, GCP, or Azure have security flaws.
How do I check if my Laravel .env file is exposed publicly?
The quickest test is to visit yourdomain.com/.env in a browser. If you see your environment variables, your web server is not configured correctly. Your nginx or Apache configuration should block access to dotfiles. In nginx, add a location block: location ~ /\. { deny all; }. You should also ensure .env is in your .gitignore and never committed to version control. For ongoing monitoring, StackShield automatically checks for exposed .env files and alerts you if one becomes accessible. Run a free scan at /free-scan to test your site.
Should I use Laravel Forge or Vapor for secure deployments?
Both are solid options with different security trade-offs. Forge provisions traditional servers on providers like AWS, DigitalOcean, or Hetzner. You get full server access, which means more control but also more responsibility for hardening. Vapor deploys to AWS Lambda (serverless), which eliminates server management but adds complexity around IAM roles, VPC configuration, and AWS service permissions. Neither is inherently more secure. The security depends on your configuration. This guide covers the common misconfigurations to watch for in both approaches.
How do I secure Redis in a Laravel production deployment?
Redis should never be accessible from the public internet. Bind it to 127.0.0.1 or a private network interface. Set a strong password using the requirepass directive in redis.conf, and configure the REDIS_PASSWORD in your Laravel .env file. Disable dangerous commands like FLUSHALL, FLUSHDB, CONFIG, and DEBUG using the rename-command directive. If Redis is on a separate server, use TLS for the connection and restrict network access with security group rules or firewall rules. Laravel supports TLS connections to Redis natively through the config/database.php Redis configuration.
What is the most common cloud misconfiguration that leads to data breaches?
Publicly accessible storage buckets remain the single most common misconfiguration leading to data breaches. Teams create S3 buckets, Azure Blob containers, or GCS buckets with public access for convenience during development and forget to lock them down before production. A single publicly readable bucket containing database backups, user uploads, or application logs can expose millions of records. Always set storage buckets to private by default and use pre-signed URLs for temporary access when users need to download files.
Related Articles
Laravel Session Security: Cookies, Hijacking & config/session.php
A deep dive into Laravel session security. Learn how cookie flags, session drivers, and config/session.php settings protect against hijacking, fixation, and sidejacking attacks.
SecurityAutomated Security Testing in Laravel CI/CD Pipelines
How to add security gates to your Laravel CI/CD pipeline with GitHub Actions. Covers dependency scanning, static analysis, secret detection, and automated security monitoring.
SecurityLaravel Content Security Policy: Configure CSP Without Breaking Your App
Only 22% of Laravel apps have a Content Security Policy. Learn how to implement CSP with spatie/laravel-csp, handle Livewire and Vite nonces, and avoid the mistakes that break production.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.