Deployment

Security Hardening

Secure your Glueful application for production

Best practices and configurations to secure your Glueful application in production.

Quick Start Checklist

  • Disable debug mode in production
  • Use HTTPS/SSL everywhere
  • Set strong TOKEN_SALT and session.jwt_key (env: JWT_KEY or SESSION_JWT_KEY)
  • Enable CSRF protection
  • Configure CORS properly
  • Use prepared statements (default)
  • Validate all input
  • Hash passwords properly (bcrypt)
  • Implement rate limiting
  • Set secure headers
  • Keep dependencies updated
  • Configure firewall
  • Disable unnecessary services
  • Set proper file permissions

Environment Configuration

Production .env

# NEVER enable debug in production
APP_ENV=production
APP_DEBUG=false

# Strong secrets (32+ chars, random)
TOKEN_SALT=your-strong-random-salt
# Framework reads config('session.jwt_key'); map from either env below as per your config loader
SESSION_JWT_KEY=your-strong-jwt-signing-key
JWT_ALGORITHM=HS256

# Token lifetimes (framework consumes config('session.*_token_lifetime'))
ACCESS_TOKEN_LIFETIME=3600        # 1 hour
REFRESH_TOKEN_LIFETIME=604800     # 7 days

# HTTPS only
APP_URL=https://api.example.com
FORCE_HTTPS=true

# Secure database
DB_HOST=localhost  # Not exposed
DB_PASSWORD=strong-random-password

# Secure Redis
REDIS_PASSWORD=strong-redis-password

# Security settings
SESSION_SECURE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=strict

Generate Secure Secrets

# Generate random secret
php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"

# Or use OpenSSL
openssl rand -hex 32

HTTPS/SSL

Force HTTPS

Prefer framework configuration over custom middleware:

FORCE_HTTPS=true
HSTS_HEADER="max-age=31536000; includeSubDomains"

SSL Headers

class SecureHeadersMiddleware
{
    public function handle(Request $request, callable $next)
    {
        $response = $next($request);

        $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

        return $response;
    }
}

Security Headers

Implement Security Headers

Use environment variables consumed by config/security.php:

X_FRAME_OPTIONS=DENY
X_CONTENT_TYPE_OPTIONS=nosniff
X_XSS_PROTECTION="1; mode=block"
HSTS_HEADER="max-age=31536000; includeSubDomains"
CSP_HEADER="default-src 'self'"

CSP Examples (safe patterns)

Prefer nonces or hashes over allowing inline scripts/styles. Generate a unique nonce per request and inject it into script/style tags.

# Strict baseline for APIs (no inline scripts)
CSP_HEADER="default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"

# If serving a frontend, use nonces and strict-dynamic
# Replace {nonce} at runtime with a per-request random value
CSP_HEADER="default-src 'self'; script-src 'self' 'nonce-{nonce}' 'strict-dynamic' https:; style-src 'self' https: 'nonce-{nonce}'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; img-src 'self' data:; connect-src 'self' https://api.example.com"

Notes:

  • Do not use unsafe-inline unless absolutely necessary; prefer 'nonce-...' or SHA256/384 hashes.
  • Generate the {nonce} server-side per request and add it to your <script>/<style> tags: <script nonce="$nonce">.
  • For SPAs loading from CDNs, explicitly allow only required hosts (e.g., https: or specific domains).

Permissions System

Glueful includes a permission system to protect sensitive actions and configuration.

ENABLE_PERMISSIONS=true

Sensitive config files are protected by default (see config/security.phpsensitive_config_files):

  • security, database, app, auth, services, mail

Enforce permissions within controllers for privileged endpoints:

// Example: require a permission before returning sensitive data
$this->requirePermission('system.health.detailed', 'health:detailed');

Authentication & Authorization

Secure Password Storage

// ✅ Good - use bcrypt
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

// Verify
if (password_verify($inputPassword, $hashedPassword)) {
    // Valid
}

// ❌ Bad - never use plain text or weak hashing
$password = md5($password);  // DON'T DO THIS
$password = sha1($password); // DON'T DO THIS

JWT Security

// Use TOKEN_SALT and JWT_KEY from session config
config('session.token_salt');
config('session.jwt_key');
config('session.jwt_algorithm');

// Token lifetimes via env
config('session.access_token_lifetime');    // ACCESS_TOKEN_LIFETIME
config('session.refresh_token_lifetime');   // REFRESH_TOKEN_LIFETIME

Rate Limiting

// Protect authentication endpoints
$router->post('/auth/login', [AuthController::class, 'login'])
    ->middleware('rate_limit:5,60'); // 5 attempts per minute

$router->post('/auth/register', [AuthController::class, 'register'])
    ->middleware('rate_limit:3,3600'); // 3 registrations per hour

Input Validation

Always Validate

class TaskController
{
    public function store(Request $request)
    {
        // ✅ Good - validate everything
        $validated = $request->validate([
            'title' => 'required|string|min:3|max:255',
            'description' => 'nullable|string|max:1000',
            'status' => 'nullable|in:pending,in_progress,completed',
            'due_date' => 'nullable|date|after:today',
        ]);

        $task = db()->table('tasks')->create($validated);

        return Response::success($task, 201);
    }
}

Sanitize Output

// Escape HTML output
function escape(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

// In views
echo escape($user->name);

SQL Injection Prevention

Use Prepared Statements (Default)

// ✅ Good - parameterized queries (default in Glueful)
$users = db()->table('users')
    ->where('email', $email)
    ->get();

// ❌ Bad - string concatenation
$users = db()->raw("SELECT * FROM users WHERE email = '{$email}'");

Validate Input Types

// Ensure IDs are integers
$userId = (int) $request->input('user_id');

// Validate UUIDs
if (!preg_match('/^[a-zA-Z0-9]{12}$/', $uuid)) {
    return Response::error('Invalid ID', 400);
}

CSRF Protection

Enable CSRF

CSRF_PROTECTION_ENABLED=true

Verify CSRF Tokens

class CsrfMiddleware
{
    public function handle(Request $request, callable $next)
    {
        if (in_array($request->method(), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
            $token = $request->header('X-CSRF-TOKEN') ?? $request->input('_token');
            $sessionToken = $request->session()->get('csrf_token');

            if (!hash_equals($sessionToken, $token)) {
                return Response::error('CSRF token mismatch', 403);
            }
        }

        return $next($request);
    }
}

CORS Security

Configure CORS Properly

# Only allow specific origins
CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com

# Don't use wildcard in production
# CORS_ALLOWED_ORIGINS=*  # DON'T DO THIS

The framework sets sensible defaults for methods/headers in config/security.php. Override in configuration if needed (credentials, max-age, etc.).

File Upload Security

CLI Security Tools

Use built-in commands to audit and harden your app:

# Production readiness and security checks
php vendor/bin/glueful system:production --check
php vendor/bin/glueful security:check
php vendor/bin/glueful security:scan
php vendor/bin/glueful security:vulnerabilities

# Emergency lockdown
php vendor/bin/glueful security:lockdown --enable --reason "Incident response"
# Later disable lockdown
php vendor/bin/glueful security:lockdown --disable --cleanup

# Token hygiene
php vendor/bin/glueful security:revoke-tokens --expired

Validate File Uploads

$request->validate([
    'avatar' => 'required|file|mimes:jpg,png|max:2048', // 2MB max
    'document' => 'required|file|mimes:pdf|max:10240', // 10MB max
]);

Secure File Storage

class FileUploadController
{
    public function upload(Request $request)
    {
        $file = $request->file('avatar');

        // Validate file type
        $allowedTypes = ['image/jpeg', 'image/png'];
        if (!in_array($file->getMimeType(), $allowedTypes)) {
            return Response::error('Invalid file type', 400);
        }

        // Generate random filename
        $filename = bin2hex(random_bytes(16)) . '.' . $file->getClientOriginalExtension();

        // Store outside public directory
        $path = storage_path('uploads/avatars/' . $filename);
        $file->move(dirname($path), $filename);

        return Response::success(['filename' => $filename]);
    }
}

Prevent Directory Traversal

function securePath(string $userPath): string
{
    // Remove any directory traversal attempts
    $userPath = str_replace(['../', '..\\'], '', $userPath);

    // Get real path
    $realPath = realpath($basePath . '/' . $userPath);

    // Ensure path is within base directory
    if (!$realPath || strpos($realPath, realpath($basePath)) !== 0) {
        throw new \Exception('Invalid path');
    }

    return $realPath;
}

Server Configuration

PHP Security Settings

/etc/php/8.2/fpm/php.ini:

; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source

; Hide PHP version
expose_php = Off

; Restrict file access
open_basedir = /var/www/app:/tmp

; Session security
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Strict
session.use_strict_mode = 1

; File upload restrictions
file_uploads = On
upload_max_filesize = 10M
max_file_uploads = 5

; Resource limits
max_execution_time = 30
max_input_time = 60
memory_limit = 256M

Nginx Security

/etc/nginx/sites-available/app:

server {
    # Hide Nginx version
    server_tokens off;

    # Deny access to hidden files
    location ~ /\. {
        deny all;
    }

    # Deny access to sensitive files
    location ~ \.(env|log|md)$ {
        deny all;
    }

    # Limit request size
    client_max_body_size 10M;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    location /api/ {
        limit_req zone=api burst=20 nodelay;
    }

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

Firewall Configuration

# Allow only necessary ports
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp   # SSH
sudo ufw allow 80/tcp   # HTTP
sudo ufw allow 443/tcp  # HTTPS
sudo ufw enable

# Limit SSH attempts
sudo ufw limit 22/tcp

Dependency Security

Keep Dependencies Updated

# Check for vulnerabilities
composer audit

# Update dependencies
composer update

# Lock dependencies
composer install --no-dev

Remove Dev Dependencies

# Production
composer install --no-dev --optimize-autoloader

Secrets Management

Use Environment Variables

// ✅ Good - from environment
$apiKey = env('STRIPE_SECRET_KEY');

// ❌ Bad - hardcoded
$apiKey = 'sk_live_...';

Encrypt Sensitive Data

class Encryptor
{
    private string $key;

    public function __construct()
    {
        $this->key = env('APP_KEY');
    }

    public function encrypt(string $data): string
    {
        $iv = random_bytes(16);
        $encrypted = openssl_encrypt($data, 'AES-256-CBC', $this->key, 0, $iv);
        return base64_encode($iv . $encrypted);
    }

    public function decrypt(string $data): string
    {
        $data = base64_decode($data);
        $iv = substr($data, 0, 16);
        $encrypted = substr($data, 16);
        return openssl_decrypt($encrypted, 'AES-256-CBC', $this->key, 0, $iv);
    }
}

Monitoring & Logging

Log Security Events

// Failed login attempts
logger()->warning('Failed login attempt', [
    'email' => $email,
    'ip' => $request->ip(),
    'user_agent' => $request->userAgent(),
]);

// Suspicious activity
logger()->critical('Potential SQL injection attempt', [
    'input' => $input,
    'ip' => $request->ip(),
]);

// Access to admin endpoints
logger()->info('Admin access', [
    'user_id' => $user->id,
    'action' => $action,
    'ip' => $request->ip(),
]);

Monitor Failed Attempts

class LoginAttemptTracker
{
    public function trackFailedLogin(string $email, string $ip): void
    {
        $key = "failed_logins:{$ip}";
        $attempts = (int) Cache::get($key, 0);
        $attempts++;

        Cache::set($key, $attempts, 3600); // 1 hour

        if ($attempts >= 5) {
            logger()->critical('Multiple failed login attempts', [
                'email' => $email,
                'ip' => $ip,
                'attempts' => $attempts,
            ]);

            // Block IP
            Cache::set("blocked:{$ip}", true, 86400); // 24 hours
        }
    }
}

Best Practices

1. Defense in Depth

Layer multiple security measures:

  • Input validation
  • Output encoding
  • Prepared statements
  • CSRF protection
  • Rate limiting
  • Security headers

2. Principle of Least Privilege

# Run app as non-root user
chown -R www-data:www-data /var/www/app
chmod -R 755 /var/www/app
chmod -R 775 storage/

3. Fail Securely

// ✅ Good - fail closed
if (!$this->isAuthorized($user, $resource)) {
    return Response::error('Unauthorized', 403);
}

// ❌ Bad - fail open
if ($this->isAuthorized($user, $resource)) {
    // Allow access
}
// No explicit denial - might allow through

4. Regular Security Audits

  • Review logs for suspicious activity
  • Update dependencies regularly
  • Test authentication flows
  • Scan for vulnerabilities
  • Review access controls

Troubleshooting

CORS errors?

  • Check CORS_ALLOWED_ORIGINS
  • Ensure credentials match configuration
  • Verify HTTPS in production

CSRF token mismatch?

  • Check token generation
  • Verify session persistence
  • Ensure same domain for API and client

Rate limiting too aggressive?

  • Adjust limits per endpoint
  • Implement whitelist for trusted IPs
  • Use Redis for distributed rate limiting

Next Steps