Companion Guide: This guide belongs to the Laravel kits. Every Laravel kit ships these checks built in. Explore Kit #1 (Filament), Kit #2 (Livewire), and Kit #3 (MongoDB).
Track 06 Lesson 19

Secure Configuration

Curated by Umar Farooq • 13 min read
Quick Answer & Core Takeaway

Application configuration contains sensitive information that attackers target: - Database credentials - Access to data - API keys - Unauthorized service access - Encryption keys - Data decryption - Debug information - System reconnaissance - Server details - Attack planning

Why Configuration Security Matters

Application configuration contains sensitive information that attackers target:

Poor configuration security is responsible for many data breaches.

Environment Variables (.env files)

The Problem with .env Files

Laravel and many PHP frameworks use .env files to store configuration. These files are often:

Secure .env File Management

1. Never Commit .env Files

PHP / Laravel Code
# .gitignore should always include:
.env
.env.local
.env.production
.env.staging

# Never commit these files!

2. Secure File Permissions

PHP / Laravel Code
# Set restrictive permissions on .env files
chmod 600 .env
chown www-data:www-data .env

# Ensure web server cannot read .env directly
# Place .env outside web root if possible

3. Use Environment-Specific Configurations

PHP / Laravel Code
# Directory structure
project/
├── .env.example          # Template (safe to commit)
├── .env.local           # Local development
├── .env.staging         # Staging environment
└── .env.production      # Production environment

4. Encrypt Sensitive Values

PHP / Laravel Code
<?php
// Don't store plain text secrets
// BAD:
DB_PASSWORD=mysecretpassword
API_KEY=sk_live_123456789

// GOOD: Use encrypted values or environment variables
DB_PASSWORD=${DB_PASSWORD_ENCRYPTED}
API_KEY=${API_KEY_ENCRYPTED}

Laravel .env Security

Laravel Environment Detection

[SECURE] Hardened Pattern
<?php
// config/app.php - Secure environment detection
'env' => env('APP_ENV', 'production'),

// Ensure production environment is properly detected

Secure Laravel Configuration

[SECURE] Hardened Pattern
<?php
// config/app.php - Production settings
'debug' => env('APP_DEBUG', false), // NEVER true in production
'env' => env('APP_ENV', 'production'),

// config/database.php - Secure database config
'connections' => [
    'mysql' => [
        'host' => env('DB_HOST', '127.0.0.1'),
        'database' => env('DB_DATABASE'),
        'username' => env('DB_USERNAME'),
        'password' => env('DB_PASSWORD'),
        'charset' => 'utf8mb4',
        'options' => [
            PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_SSL_CA'), // Use SSL
            PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
        ],
    ],
],

Debug Mode Security

The Dangers of Debug Mode

Debug mode exposes sensitive information:

PHP / Laravel Code
// Debug enabled - DANGEROUS in production
APP_DEBUG=true

// Exposed information:
- Full file paths
- Database queries with parameters
- Session data
- Environment variables
- Stack traces with sensitive data

Secure Debug Configuration

Laravel Debug Management

PHP / Laravel Code
<?php
// config/app.php
'debug' => env('APP_DEBUG', false),

// Bootstrap/app.php - Environment-specific debug
$app->useEnvironmentPath(__DIR__.'/../');
$app->loadEnvironmentFrom('.env');

// Only enable debug for specific environments
if ($app->environment(['local', 'development'])) {
    $app->make('config')->set('app.debug', true);
}

PHP Error Reporting

PHP / Laravel Code
<?php
// Production error reporting - hide sensitive info
ini_set('display_errors', '0');          // Don't display errors
ini_set('display_startup_errors', '0');  // Hide startup errors
error_reporting(E_ALL & ~E_DEPRECATED); // Log all but deprecated

// Development error reporting - show everything
if (getenv('APP_ENV') === 'development') {
    ini_set('display_errors', '1');
    ini_set('display_startup_errors', '1');
    error_reporting(E_ALL);
}

Custom Error Handler

[SECURE] Hardened Pattern
<?php
class SecureErrorHandler
{
    public static function setup(): void
    {
        // Custom error handler
        set_error_handler([self::class, 'handleError']);

        // Custom exception handler
        set_exception_handler([self::class, 'handleException']);

        // Fatal error handler
        register_shutdown_function([self::class, 'handleShutdown']);
    }

    public static function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
    {
        // Log error securely
        self::logError('PHP Error', [
            'errno' => $errno,
            'errstr' => self::sanitizeErrorMessage($errstr),
            'errfile' => basename($errfile), // Don't expose full paths
            'errline' => $errline,
        ]);

        // Don't display errors in production
        if (getenv('APP_ENV') === 'production') {
            return true; // Prevent default error handler
        }

        return false; // Use default error handler in development
    }

    public static function handleException(Throwable $exception): void
    {
        self::logError('Uncaught Exception', [
            'message' => self::sanitizeErrorMessage($exception->getMessage()),
            'file' => basename($exception->getFile()),
            'line' => $exception->getLine(),
            'trace' => self::sanitizeStackTrace($exception->getTraceAsString()),
        ]);

        // Show user-friendly error page
        if (getenv('APP_ENV') === 'production') {
            http_response_code(500);
            echo self::getErrorPage();
            exit;
        }

        // Re-throw in development
        throw $exception;
    }

    public static function handleShutdown(): void
    {
        $error = error_get_last();
        if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
            self::logError('Fatal Error', [
                'message' => self::sanitizeErrorMessage($error['message']),
                'file' => basename($error['file']),
                'line' => $error['line'],
            ]);

            if (getenv('APP_ENV') === 'production') {
                http_response_code(500);
                echo self::getErrorPage();
            }
        }
    }

    private static function sanitizeErrorMessage(string $message): string
    {
        // Remove sensitive information from error messages
        $patterns = [
            '/(password|passwd|pwd)[\'"]?\s*[:=]\s*[\'"][^\'"]*[\'"]/i',
            '/(key|token|secret)[\'"]?\s*[:=]\s*[\'"][^\'"]*[\'"]/i',
            '/(mysql|database).*?@/i',
        ];

        foreach ($patterns as $pattern) {
            $message = preg_replace($pattern, '[REDACTED]', $message);
        }

        return $message;
    }

    private static function sanitizeStackTrace(string $trace): string
    {
        // Remove full paths from stack trace
        $lines = explode("\n", $trace);
        $sanitized = [];

        foreach ($lines as $line) {
            // Replace full paths with relative paths
            $line = preg_replace('/\/var\/www\/[^)]*\//', '/app/', $line);
            $sanitized[] = $line;
        }

        return implode("\n", $sanitized);
    }

    private static function logError(string $type, array $data): void
    {
        $logMessage = sprintf(
            "[%s] %s in %s:%d - %s",
            date('Y-m-d H:i:s'),
            $type,
            $data['file'] ?? 'unknown',
            $data['line'] ?? 0,
            $data['message'] ?? 'No message'
        );

        error_log($logMessage);
    }

    private static function getErrorPage(): string
    {
        return <<<'HTML'
<!DOCTYPE html>
<html>
<head>
    <title>Server Error</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        .error { color: #d9534f; font-size: 24px; }
    </style>
</head>
<body>
    <h1 class="error">Oops! Something went wrong.</h1>
    <p>We're working to fix this issue. Please try again later.</p>
    <p>If the problem persists, contact support.</p>
</body>
</html>
HTML;
    }
}

// Setup secure error handling
SecureErrorHandler::setup();

PHP Version Hiding

Why Hide PHP Version?

PHP version disclosure helps attackers:

Hiding PHP Version

1. PHP Configuration

PHP / Laravel Code
; php.ini - Hide PHP version
expose_php = Off

2. Web Server Configuration

PHP / Laravel Code
# Apache .htaccess
<IfModule mod_headers.c>
    Header unset X-Powered-By
    Header unset Server
</IfModule>

# Nginx configuration
server {
    # Hide nginx version
    server_tokens off;

    # Hide PHP version
    fastcgi_hide_header X-Powered-By;
    fastcgi_hide_header Server;
}

3. PHP Code Headers

PHP / Laravel Code
<?php
// Remove PHP version from headers
header_remove('X-Powered-By');
header_remove('Server');

// Custom server header (optional)
header('Server: Web Server');

4. Laravel Specific

PHP / Laravel Code
<?php
// In Laravel middleware
class SecurityHeadersMiddleware
{
    public function handle($request, $next)
    {
        $response = $next($request);

        // Remove PHP/Laravel version headers
        $response->headers->remove('X-Powered-By');
        $response->headers->remove('Server');

        return $response;
    }
}

Production PHP Configuration

Secure php.ini Settings

[SECURE] Hardened Pattern
; php.ini - Production Security Settings

; Error Handling
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /var/log/php_errors.log

; File Uploads
file_uploads = On
upload_max_filesize = 10M
max_file_uploads = 5
post_max_size = 12M

; Session Security
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Strict"
session.use_only_cookies = 1
session.gc_maxlifetime = 1440
session.save_path = "/var/secure/sessions"

; Resource Limits
max_execution_time = 30
max_input_time = 60
memory_limit = 128M

; Security
expose_php = Off
allow_url_fopen = Off
allow_url_include = Off
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source

; OpenSSL
openssl.cafile = /etc/ssl/certs/ca-certificates.crt

; Timezone
date.timezone = "UTC"

PHP-FPM Configuration

PHP / Laravel Code
; /etc/php/7.4/fpm/php-fpm.conf
[www]

; Security settings
security.limit_extensions = .php .php3 .php4 .php5 .php7

; Process management
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35

; Logging
log_level = notice
access.log = /var/log/php-fpm/access.log

Opcache Security

PHP / Laravel Code
; opcache.ini - Production settings
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.max_accelerated_files = 7963
opcache.revalidate_freq = 0
opcache.validate_timestamps = 0 ; Set to 1 in development
opcache.save_comments = 0
opcache.enable_file_override = 0

Database Configuration Security

Secure Database Connections

[SECURE] Hardened Pattern
<?php
class SecureDatabaseConfig
{
    public static function getSecurePDO(string $env): PDO
    {
        $config = self::getDatabaseConfig($env);

        $dsn = sprintf(
            'mysql:host=%s;dbname=%s;charset=utf8mb4',
            $config['host'],
            $config['database']
        );

        $options = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci',
            PDO::MYSQL_ATTR_SSL_CA => $config['ssl_ca'] ?? null,
            PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
        ];

        return new PDO($dsn, $config['username'], $config['password'], $options);
    }

    private static function getDatabaseConfig(string $env): array
    {
        // Load from secure environment variables
        return [
            'host' => getenv('DB_HOST') ?: 'localhost',
            'database' => getenv('DB_NAME') ?: 'app_db',
            'username' => getenv('DB_USER'),
            'password' => getenv('DB_PASS'),
            'ssl_ca' => getenv('DB_SSL_CA'),
        ];
    }
}

Laravel Database Security

[SECURE] Hardened Pattern
<?php
// config/database.php - Secure configuration
return [
    'connections' => [
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE'),
            'username' => env('DB_USERNAME'),
            'password' => env('DB_PASSWORD'),
            'unix_socket' => env('DB_SOCKET'),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true, // Strict mode for security
            'engine' => null,
            'options' => [
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_SSL_CA'),
                PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => env('MYSQL_SSL_VERIFY_SERVER_CERT', true),
                PDO::ATTR_PERSISTENT => false, // Don't use persistent connections
            ],
        ],
    ],
];

Application Configuration Security

Secure API Keys Management

[SECURE] Hardened Pattern
<?php
class SecureApiKeyManager
{
    private static array $keys = [];

    public static function loadKeys(): void
    {
        // Load from secure environment variables
        self::$keys = [
            'stripe_secret' => getenv('STRIPE_SECRET_KEY'),
            'aws_access' => getenv('AWS_ACCESS_KEY_ID'),
            'aws_secret' => getenv('AWS_SECRET_ACCESS_KEY'),
            'jwt_secret' => getenv('JWT_SECRET'),
        ];

        // Validate required keys exist
        $required = ['stripe_secret', 'jwt_secret'];
        foreach ($required as $key) {
            if (empty(self::$keys[$key])) {
                throw new RuntimeException("Required configuration key missing: {$key}");
            }
        }
    }

    public static function getKey(string $name): ?string
    {
        return self::$keys[$name] ?? null;
    }

    public static function validateConfiguration(): array
    {
        $issues = [];

        // Check file permissions
        $envFile = __DIR__ . '/../.env';
        if (file_exists($envFile)) {
            $perms = fileperms($envFile) & 0777;
            if ($perms !== 0600) {
                $issues[] = '.env file has incorrect permissions (should be 600)';
            }
        }

        // Check debug mode
        if (getenv('APP_DEBUG') === 'true' && getenv('APP_ENV') === 'production') {
            $issues[] = 'Debug mode enabled in production';
        }

        // Check required environment variables
        $required = ['APP_KEY', 'DB_PASSWORD', 'REDIS_PASSWORD'];
        foreach ($required as $var) {
            if (empty(getenv($var))) {
                $issues[] = "Missing required environment variable: {$var}";
            }
        }

        return $issues;
    }
}

Deployment Configuration

Secure Deployment Script

[INSECURE] Vulnerable Pattern
#!/bin/bash
# secure-deploy.sh

set -e  # Exit on any error

echo " Starting secure deployment..."

# Check environment
if [ "$ENVIRONMENT" != "production" ]; then
    echo "[INSECURE] This script should only run in production"
    exit 1
fi

# Validate configuration
echo " Validating configuration..."
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Check for security issues
echo " Running security checks..."
if php artisan tinker --execute="echo app()->environment()" | grep -q "production"; then
    echo "[SECURE] Environment correctly set to production"
else
    echo "[INSECURE] Environment not set to production"
    exit 1
fi

# Set secure permissions
echo " Setting secure permissions..."
find . -type f -name "*.php" -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;
chmod 600 .env
chmod 600 storage/logs/*.log

# Clear sensitive caches
echo " Clearing sensitive data..."
php artisan config:clear
php artisan cache:clear
php artisan view:clear

# Health check
echo " Running health checks..."
curl -f -s http://localhost/health-check > /dev/null
if [ $? -eq 0 ]; then
    echo "[SECURE] Application health check passed"
else
    echo "[INSECURE] Application health check failed"
    exit 1
fi

echo " Secure deployment completed successfully!"

Configuration Validation

[INSECURE] Vulnerable Pattern
<?php
// Laravel command to validate configuration
namespace App\Console\Commands;

use Illuminate\Console\Command;

class ValidateConfig extends Command
{
    protected $signature = 'config:validate';
    protected $description = 'Validate production configuration security';

    public function handle()
    {
        $this->info(' Validating configuration security...');

        $issues = [];

        // Check debug mode
        if (config('app.debug') && app()->environment('production')) {
            $issues[] = 'Debug mode enabled in production';
        }

        // Check session security
        $sessionConfig = config('session');
        if (!$sessionConfig['secure']) {
            $issues[] = 'Session cookies not marked as secure';
        }
        if (!$sessionConfig['http_only']) {
            $issues[] = 'Session cookies not marked as http-only';
        }

        // Check database SSL
        $dbConfig = config('database.connections.mysql');
        if (empty($dbConfig['options'][PDO::MYSQL_ATTR_SSL_CA])) {
            $issues[] = 'Database not configured to use SSL';
        }

        // Report issues
        if (empty($issues)) {
            $this->info('[SECURE] All configuration checks passed!');
            return 0;
        }

        foreach ($issues as $issue) {
            $this->error("[INSECURE] {$issue}");
        }

        return 1;
    }
}

Monitoring and Alerting

Configuration Change Monitoring

[SECURE] Hardened Pattern
<?php
class ConfigurationMonitor
{
    private static string $configHashFile = '/var/secure/config_hash.txt';

    public static function checkConfigurationChanges(): void
    {
        $currentHash = self::calculateConfigHash();

        if (file_exists(self::$configHashFile)) {
            $storedHash = trim(file_get_contents(self::$configHashFile));

            if ($storedHash !== $currentHash) {
                // Configuration changed - alert administrators
                self::alertConfigChange($storedHash, $currentHash);

                // Update stored hash
                file_put_contents(self::$configHashFile, $currentHash);
            }
        } else {
            // First run - store initial hash
            file_put_contents(self::$configHashFile, $currentHash);
        }
    }

    private static function calculateConfigHash(): string
    {
        $configFiles = [
            __DIR__ . '/../.env',
            __DIR__ . '/../config/app.php',
            __DIR__ . '/../config/database.php',
            '/etc/php/7.4/fpm/php-fpm.conf',
        ];

        $hashes = [];
        foreach ($configFiles as $file) {
            if (file_exists($file)) {
                $hashes[] = hash_file('sha256', $file);
            }
        }

        return hash('sha256', implode('', $hashes));
    }

    private static function alertConfigChange(string $oldHash, string $newHash): void
    {
        $message = sprintf(
            "Configuration change detected!\nOld hash: %s\nNew hash: %s\nTime: %s",
            $oldHash,
            $newHash,
            date('Y-m-d H:i:s')
        );

        // Log alert
        error_log("SECURITY ALERT: " . $message);

        // Send email alert (implement based on your needs)
        // mail('admin@example.com', 'Configuration Change Alert', $message);
    }
}

Configuration Security Checklist

Environment Variables

Debug and Error Handling

PHP Configuration

Application Security

Deployment Security

Next Steps

Now that you understand secure configuration, explore:

Remember: Secure configuration is the foundation of application security. Regular audits and monitoring are essential to maintain security posture!

AI Assistant Shortcuts
Ask AI about this topic
← Prev: Model Output Validation
Next: Security Headers & CSP Nonces →
UF

Curated by Umar Farooq

Senior Full-Stack & Security Engineer with 5+ years of production experience in Laravel, PHP, and modern web application security. Author of the Laravel AI Security Masterclass.