# 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.

**Author:** Matt King | **Published:** July 2, 2026 | **Category:** Security

---

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:**

```php
// 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:

```php
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:**

```json
{
    "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:

```bash
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:**

```json
{
    "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:

```yaml
# 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:

```bash
# 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:

```bash
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:**

```ini
# /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:**

```php
// 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:**

```sql
-- 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:**

```ini
# /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:**

```php
// 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](/free-scan).

### The Checklist

**Nginx configuration to block dotfiles:**

```nginx
# /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:**

```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](/free-scan) checks for exposed .env files, or use the [header check tool](/tools/header-check) 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:**

```env
APP_DEBUG=false
APP_ENV=production
```

**Remove or protect development routes:**

```php
// 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:**

```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:**

```php
// 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](/security-checks/debug-mode). The [free scan](/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:**

```php
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    if ($this->app->environment('production')) {
        \Illuminate\Support\Facades\URL::forceScheme('https');
    }
}
```

**Nginx HTTPS configuration:**

```nginx
# 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](/tools/header-check).

---

## 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 `nmap` or [StackShield free scan](/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](/pricing).

---

*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](/free-scan) to continuously verify that your deployment matches your intentions.*

---

## 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.

