coolify/.ai/patterns/security-patterns.md
Andras Bacsai 3f7c5fbdf9 Consolidate AI documentation into .ai/ directory
- Create .ai/ directory as single source of truth for all AI docs
- Organize by topic: core/, development/, patterns/, meta/
- Update CLAUDE.md to reference .ai/ files instead of embedding content
- Remove 18KB of duplicated Laravel Boost guidelines from CLAUDE.md
- Fix testing command descriptions (pest runs all tests, not just unit)
- Standardize version numbers (Laravel 12.4.1, PHP 8.4.7, Tailwind 4.1.4)
- Replace all .cursor/rules/*.mdc with single coolify-ai-docs.mdc reference
- Delete dev_workflow.mdc (non-Coolify Task Master content)
- Merge cursor_rules.mdc + self_improve.mdc into maintaining-docs.md
- Update .AI_INSTRUCTIONS_SYNC.md to redirect to new location

Benefits:
- Single source of truth - no more duplication
- Consistent versions across all documentation
- Better organization by topic
- Platform-agnostic .ai/ directory works for all AI tools
- Reduced CLAUDE.md from 719 to ~320 lines
- Clear cross-references between files
2025-11-18 14:58:59 +01:00

31 KiB

Coolify Security Architecture & Patterns

Security Philosophy

Coolify implements defense-in-depth security with multiple layers of protection including authentication, authorization, encryption, network isolation, and secure deployment practices.

Authentication Architecture

Multi-Provider Authentication

OAuth Integration

  • OauthSetting.php - OAuth provider configurations
  • Supported Providers:
    • Google OAuth
    • Microsoft Azure AD
    • Clerk
    • Authentik
    • Discord
    • GitHub (via GitHub Apps)
    • GitLab

Authentication Models

// User authentication with team-based access
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    
    protected $fillable = [
        'name', 'email', 'password'
    ];
    
    protected $hidden = [
        'password', 'remember_token'
    ];
    
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
    
    public function teams(): BelongsToMany
    {
        return $this->belongsToMany(Team::class)
            ->withPivot('role')
            ->withTimestamps();
    }
    
    public function currentTeam(): BelongsTo
    {
        return $this->belongsTo(Team::class, 'current_team_id');
    }
}

Authorization & Access Control

Enhanced Form Component Authorization System

Coolify now features a centralized authorization system built into all form components (Input, Select, Textarea, Checkbox, Button) that automatically handles permission-based UI control.

Component Authorization Parameters

// Available on all form components
public ?string $canGate = null;        // Gate name (e.g., 'update', 'view', 'delete')
public mixed $canResource = null;      // Resource to check against (model instance)
public bool $autoDisable = true;       // Auto-disable if no permission (default: true)

Smart Authorization Logic

// Automatic authorization handling in component constructor
if ($this->canGate && $this->canResource && $this->autoDisable) {
    $hasPermission = Gate::allows($this->canGate, $this->canResource);
    
    if (! $hasPermission) {
        $this->disabled = true;
        // For Checkbox: also disables instantSave
    }
}

Usage Examples

Recommended Pattern (Single Line):

<!-- Input with automatic authorization -->
<x-forms.input 
    canGate="update" 
    :canResource="$application" 
    id="application.name" 
    label="Application Name" />

<!-- Select with automatic authorization -->
<x-forms.select 
    canGate="update" 
    :canResource="$application" 
    id="application.build_pack" 
    label="Build Pack">
    <option value="nixpacks">Nixpacks</option>
    <option value="static">Static</option>
</x-forms.select>

<!-- Checkbox with automatic instantSave control -->
<x-forms.checkbox 
    instantSave 
    canGate="update" 
    :canResource="$application" 
    id="application.settings.is_static" 
    label="Is Static Site?" />

<!-- Button with automatic disable -->
<x-forms.button 
    canGate="update" 
    :canResource="$application" 
    type="submit">
    Save Configuration
</x-forms.button>

Old Pattern (Verbose, Deprecated):

<!-- DON'T use this repetitive pattern anymore -->
@can('update', $application)
    <x-forms.input id="application.name" label="Application Name" />
    <x-forms.button type="submit">Save</x-forms.button>
@else
    <x-forms.input disabled id="application.name" label="Application Name" />
@endcan

Advanced Usage with Custom Control

Custom Authorization Logic:

<!-- Disable auto-control, use custom logic -->
<x-forms.input 
    canGate="update" 
    :canResource="$application" 
    autoDisable="false"
    :disabled="$application->is_deployed || !Gate::allows('update', $application)"
    id="advanced.setting" 
    label="Advanced Setting" />

Multiple Permission Checks:

<!-- Complex permission requirements -->
<x-forms.checkbox 
    canGate="deploy" 
    :canResource="$application" 
    autoDisable="false"
    :disabled="!$application->canDeploy() || !auth()->user()->hasAdvancedPermissions()"
    id="deployment.setting" 
    label="Advanced Deployment Setting" />

Supported Gates and Resources

Common Gates:

  • view - Read access to resource
  • update - Modify resource configuration
  • deploy - Deploy/restart resource
  • delete - Remove resource
  • createAnyResource - Create new resources

Resource Types:

  • Application - Application instances
  • Service - Docker Compose services
  • Server - Server instances
  • Project - Project containers
  • Environment - Environment contexts
  • Database - Database instances

Benefits

🔥 Massive Code Reduction:

  • 90% less code for authorization-protected forms
  • Single line instead of 6-12 lines per form element
  • No more @can/@else blocks cluttering templates

🛡️ Consistent Security:

  • Unified authorization logic across all form components
  • Automatic disabling for unauthorized users
  • Smart behavior (like disabling instantSave on checkboxes)

🎨 Better UX:

  • Consistent disabled styling across all components
  • Proper visual feedback for restricted access
  • Clean, professional interface

Implementation Details

Component Enhancement:

// Enhanced in all form components
use Illuminate\Support\Facades\Gate;

public function __construct(
    // ... existing parameters
    public ?string $canGate = null,
    public mixed $canResource = null,
    public bool $autoDisable = true,
) {
    // Handle authorization-based disabling
    if ($this->canGate && $this->canResource && $this->autoDisable) {
        $hasPermission = Gate::allows($this->canGate, $this->canResource);
        
        if (! $hasPermission) {
            $this->disabled = true;
            // For Checkbox: $this->instantSave = false;
        }
    }
}

Backward Compatibility:

  • All existing form components continue to work unchanged
  • New authorization parameters are optional
  • Legacy @can/@else patterns still function but are discouraged

Custom Component Authorization Patterns

When dealing with custom Alpine.js components or complex UI elements that don't use the standard x-forms.* components, manual authorization protection is required since the automatic canGate system only applies to enhanced form components.

Common Custom Components Requiring Manual Protection

⚠️ Custom Components That Need Manual Authorization:

  • Custom dropdowns/selects with Alpine.js
  • Complex form widgets with JavaScript interactions
  • Multi-step wizards or dynamic forms
  • Third-party component integrations
  • Custom date/time pickers
  • File upload components with drag-and-drop

Manual Authorization Pattern

Proper Manual Authorization:

<!-- Custom timezone dropdown example -->
<div class="w-full">
    <div class="flex items-center mb-1">
        <label for="customComponent">Component Label</label>
        <x-helper helper="Component description" />
    </div>
    @can('update', $resource)
        <!-- Full interactive component for authorized users -->
        <div x-data="{
            open: false,
            value: '{{ $currentValue }}',
            options: @js($options),
            init() { /* Alpine.js initialization */ }
        }">
            <input x-model="value" @focus="open = true" 
                   wire:model="propertyName" class="w-full input">
            <div x-show="open">
                <!-- Interactive dropdown content -->
                <template x-for="option in options" :key="option">
                    <div @click="value = option; open = false; $wire.submit()"
                         x-text="option"></div>
                </template>
            </div>
        </div>
    @else
        <!-- Read-only version for unauthorized users -->
        <div class="relative">
            <input readonly disabled autocomplete="off"
                   class="w-full input opacity-50 cursor-not-allowed" 
                   value="{{ $currentValue ?: 'No value set' }}">
            <svg class="absolute right-0 mr-2 w-4 h-4 opacity-50">
                <!-- Disabled icon -->
            </svg>
        </div>
    @endcan
</div>

Implementation Checklist

When implementing authorization for custom components:

🔍 1. Identify Custom Components:

  • Look for Alpine.js x-data declarations
  • Find components not using x-forms.* prefix
  • Check for JavaScript-heavy interactions
  • Review complex form widgets

🛡️ 2. Wrap with Authorization:

  • Use @can('gate', $resource) / @else / @endcan structure
  • Provide full functionality in the @can block
  • Create disabled/readonly version in the @else block

🎨 3. Design Disabled State:

  • Apply readonly disabled attributes to inputs
  • Add opacity-50 cursor-not-allowed classes for visual feedback
  • Remove interactive JavaScript behaviors
  • Show current value or appropriate placeholder

🔒 4. Backend Protection:

  • Ensure corresponding Livewire methods check authorization
  • Add $this->authorize('gate', $resource) in relevant methods
  • Validate permissions before processing any changes

Real-World Examples

Custom Date Range Picker:

@can('update', $application)
    <div x-data="dateRangePicker()" class="date-picker">
        <!-- Interactive date picker with calendar -->
    </div>
@else
    <div class="flex gap-2">
        <input readonly disabled value="{{ $startDate }}" class="input opacity-50">
        <input readonly disabled value="{{ $endDate }}" class="input opacity-50">
    </div>
@endcan

Multi-Select Component:

@can('update', $server)
    <div x-data="multiSelect({ options: @js($options) })">
        <!-- Interactive multi-select with checkboxes -->
    </div>
@else
    <div class="space-y-2">
        @foreach($selectedValues as $value)
            <div class="px-3 py-1 bg-gray-100 rounded text-sm opacity-50">
                {{ $value }}
            </div>
        @endforeach
    </div>
@endcan

File Upload Widget:

@can('update', $application)
    <div x-data="fileUploader()" @drop.prevent="handleDrop">
        <!-- Drag-and-drop file upload interface -->
    </div>
@else
    <div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center opacity-50">
        <p class="text-gray-500">File upload restricted</p>
        @if($currentFile)
            <p class="text-sm">Current: {{ $currentFile }}</p>
        @endif
    </div>
@endcan

Key Principles

🎯 Consistency:

  • Maintain similar visual styling between enabled/disabled states
  • Use consistent disabled patterns across the application
  • Apply the same opacity and cursor styling

🔐 Security First:

  • Always implement backend authorization checks
  • Never rely solely on frontend hiding/disabling
  • Validate permissions on every server action

💡 User Experience:

  • Show current values in disabled state when appropriate
  • Provide clear visual feedback about restricted access
  • Maintain layout stability between states

🚀 Performance:

  • Minimize Alpine.js initialization for disabled components
  • Avoid loading unnecessary JavaScript for unauthorized users
  • Use simple HTML structures for read-only states

Team-Based Multi-Tenancy

  • Team.php - Multi-tenant organization structure (8.9KB, 308 lines)
  • TeamInvitation.php - Secure team collaboration
  • Role-based permissions within teams
  • Resource isolation by team ownership

Authorization Patterns

// Team-scoped authorization middleware
class EnsureTeamAccess
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        $teamId = $request->route('team');
        
        if (!$user->teams->contains('id', $teamId)) {
            abort(403, 'Access denied to team resources');
        }
        
        // Set current team context
        $user->switchTeam($teamId);
        
        return $next($request);
    }
}

// Resource-level authorization policies
class ApplicationPolicy
{
    public function view(User $user, Application $application): bool
    {
        return $user->teams->contains('id', $application->team_id);
    }
    
    public function deploy(User $user, Application $application): bool
    {
        return $this->view($user, $application) && 
               $user->hasTeamPermission($application->team_id, 'deploy');
    }
    
    public function delete(User $user, Application $application): bool
    {
        return $this->view($user, $application) && 
               $user->hasTeamRole($application->team_id, 'admin');
    }
}

Global Scopes for Data Isolation

// Automatic team-based filtering
class Application extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope('team', function (Builder $builder) {
            if (auth()->check() && auth()->user()->currentTeam) {
                $builder->whereHas('environment.project', function ($query) {
                    $query->where('team_id', auth()->user()->currentTeam->id);
                });
            }
        });
    }
}

API Security

Token-Based Authentication

// Sanctum API token management
class PersonalAccessToken extends Model
{
    protected $fillable = [
        'name', 'token', 'abilities', 'expires_at'
    ];
    
    protected $casts = [
        'abilities' => 'array',
        'expires_at' => 'datetime',
        'last_used_at' => 'datetime',
    ];
    
    public function tokenable(): MorphTo
    {
        return $this->morphTo();
    }
    
    public function hasAbility(string $ability): bool
    {
        return in_array('*', $this->abilities) || 
               in_array($ability, $this->abilities);
    }
}

API Rate Limiting

// Rate limiting configuration
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

RateLimiter::for('deployments', function (Request $request) {
    return Limit::perMinute(10)->by($request->user()->id);
});

RateLimiter::for('webhooks', function (Request $request) {
    return Limit::perMinute(100)->by($request->ip());
});

API Input Validation

// Comprehensive input validation
class StoreApplicationRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Application::class);
    }
    
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255|regex:/^[a-zA-Z0-9\-_]+$/',
            'git_repository' => 'required|url|starts_with:https://',
            'git_branch' => 'required|string|max:100|regex:/^[a-zA-Z0-9\-_\/]+$/',
            'server_id' => 'required|exists:servers,id',
            'environment_id' => 'required|exists:environments,id',
            'environment_variables' => 'array',
            'environment_variables.*' => 'string|max:1000',
        ];
    }
    
    public function prepareForValidation(): void
    {
        $this->merge([
            'name' => strip_tags($this->name),
            'git_repository' => filter_var($this->git_repository, FILTER_SANITIZE_URL),
        ]);
    }
}

SSH Security

Private Key Management

  • PrivateKey.php - Secure SSH key storage (6.5KB, 247 lines)
  • Encrypted key storage in database
  • Key rotation capabilities
  • Access logging for key usage

SSH Connection Security

class SshConnection
{
    private string $host;
    private int $port;
    private string $username;
    private PrivateKey $privateKey;
    
    public function __construct(Server $server)
    {
        $this->host = $server->ip;
        $this->port = $server->port;
        $this->username = $server->user;
        $this->privateKey = $server->privateKey;
    }
    
    public function connect(): bool
    {
        $connection = ssh2_connect($this->host, $this->port);
        
        if (!$connection) {
            throw new SshConnectionException('Failed to connect to server');
        }
        
        // Use private key authentication
        $privateKeyContent = decrypt($this->privateKey->private_key);
        $publicKeyContent = decrypt($this->privateKey->public_key);
        
        if (!ssh2_auth_pubkey_file($connection, $this->username, $publicKeyContent, $privateKeyContent)) {
            throw new SshAuthenticationException('SSH authentication failed');
        }
        
        return true;
    }
    
    public function execute(string $command): string
    {
        // Sanitize command to prevent injection
        $command = escapeshellcmd($command);
        
        $stream = ssh2_exec($this->connection, $command);
        
        if (!$stream) {
            throw new SshExecutionException('Failed to execute command');
        }
        
        return stream_get_contents($stream);
    }
}

Container Security

Docker Security Patterns

class DockerSecurityService
{
    public function createSecureContainer(Application $application): array
    {
        return [
            'image' => $this->validateImageName($application->docker_image),
            'user' => '1000:1000', // Non-root user
            'read_only' => true,
            'no_new_privileges' => true,
            'security_opt' => [
                'no-new-privileges:true',
                'apparmor:docker-default'
            ],
            'cap_drop' => ['ALL'],
            'cap_add' => ['CHOWN', 'SETUID', 'SETGID'], // Minimal capabilities
            'tmpfs' => [
                '/tmp' => 'rw,noexec,nosuid,size=100m',
                '/var/tmp' => 'rw,noexec,nosuid,size=50m'
            ],
            'ulimits' => [
                'nproc' => 1024,
                'nofile' => 1024
            ]
        ];
    }
    
    private function validateImageName(string $image): string
    {
        // Validate image name against allowed registries
        $allowedRegistries = ['docker.io', 'ghcr.io', 'quay.io'];
        
        $parser = new DockerImageParser();
        $parsed = $parser->parse($image);
        
        if (!in_array($parsed['registry'], $allowedRegistries)) {
            throw new SecurityException('Image registry not allowed');
        }
        
        return $image;
    }
}

Network Isolation

# Docker Compose security configuration
version: '3.8'
services:
  app:
    image: ${APP_IMAGE}
    networks:
      - app-network
    security_opt:
      - no-new-privileges:true
      - apparmor:docker-default
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=100m
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETUID
      - SETGID

networks:
  app-network:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.20.0.0/16

SSL/TLS Security

Certificate Management

  • SslCertificate.php - SSL certificate automation
  • Let's Encrypt integration for free certificates
  • Automatic renewal and monitoring
  • Custom certificate upload support

SSL Configuration

class SslCertificateService
{
    public function generateCertificate(Application $application): SslCertificate
    {
        $domains = $this->validateDomains($application->getAllDomains());
        
        $certificate = SslCertificate::create([
            'application_id' => $application->id,
            'domains' => $domains,
            'provider' => 'letsencrypt',
            'status' => 'pending'
        ]);
        
        // Generate certificate using ACME protocol
        $acmeClient = new AcmeClient();
        $certData = $acmeClient->generateCertificate($domains);
        
        $certificate->update([
            'certificate' => encrypt($certData['certificate']),
            'private_key' => encrypt($certData['private_key']),
            'chain' => encrypt($certData['chain']),
            'expires_at' => $certData['expires_at'],
            'status' => 'active'
        ]);
        
        return $certificate;
    }
    
    private function validateDomains(array $domains): array
    {
        foreach ($domains as $domain) {
            if (!filter_var($domain, FILTER_VALIDATE_DOMAIN)) {
                throw new InvalidDomainException("Invalid domain: {$domain}");
            }
            
            // Check domain ownership
            if (!$this->verifyDomainOwnership($domain)) {
                throw new DomainOwnershipException("Domain ownership verification failed: {$domain}");
            }
        }
        
        return $domains;
    }
}

Environment Variable Security

Secure Configuration Management

class EnvironmentVariable extends Model
{
    protected $fillable = [
        'key', 'value', 'is_secret', 'application_id'
    ];
    
    protected $casts = [
        'is_secret' => 'boolean',
        'value' => 'encrypted' // Automatic encryption for sensitive values
    ];
    
    public function setValueAttribute($value): void
    {
        // Automatically encrypt sensitive environment variables
        if ($this->isSensitiveKey($this->key)) {
            $this->attributes['value'] = encrypt($value);
            $this->attributes['is_secret'] = true;
        } else {
            $this->attributes['value'] = $value;
        }
    }
    
    public function getValueAttribute($value): string
    {
        if ($this->is_secret) {
            return decrypt($value);
        }
        
        return $value;
    }
    
    private function isSensitiveKey(string $key): bool
    {
        $sensitivePatterns = [
            'PASSWORD', 'SECRET', 'KEY', 'TOKEN', 'API_KEY',
            'DATABASE_URL', 'REDIS_URL', 'PRIVATE', 'CREDENTIAL',
            'AUTH', 'CERTIFICATE', 'ENCRYPTION', 'SALT', 'HASH',
            'OAUTH', 'JWT', 'BEARER', 'ACCESS', 'REFRESH'
        ];
        
        foreach ($sensitivePatterns as $pattern) {
            if (str_contains(strtoupper($key), $pattern)) {
                return true;
            }
        }
        
        return false;
    }
}

Webhook Security

Webhook Signature Verification

class WebhookSecurityService
{
    public function verifyGitHubSignature(Request $request, string $secret): bool
    {
        $signature = $request->header('X-Hub-Signature-256');
        
        if (!$signature) {
            return false;
        }
        
        $expectedSignature = 'sha256=' . hash_hmac('sha256', $request->getContent(), $secret);
        
        return hash_equals($expectedSignature, $signature);
    }
    
    public function verifyGitLabSignature(Request $request, string $secret): bool
    {
        $signature = $request->header('X-Gitlab-Token');
        
        return hash_equals($secret, $signature);
    }
    
    public function validateWebhookPayload(array $payload): array
    {
        // Sanitize and validate webhook payload
        $validator = Validator::make($payload, [
            'repository.clone_url' => 'required|url|starts_with:https://',
            'ref' => 'required|string|max:255',
            'head_commit.id' => 'required|string|size:40', // Git SHA
            'head_commit.message' => 'required|string|max:1000'
        ]);
        
        if ($validator->fails()) {
            throw new InvalidWebhookPayloadException('Invalid webhook payload');
        }
        
        return $validator->validated();
    }
}

Input Sanitization & Validation

XSS Prevention

class SecurityMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        // Sanitize input data
        $input = $request->all();
        $sanitized = $this->sanitizeInput($input);
        $request->merge($sanitized);
        
        return $next($request);
    }
    
    private function sanitizeInput(array $input): array
    {
        foreach ($input as $key => $value) {
            if (is_string($value)) {
                // Remove potentially dangerous HTML tags
                $input[$key] = strip_tags($value, '<p><br><strong><em>');
                
                // Escape special characters
                $input[$key] = htmlspecialchars($input[$key], ENT_QUOTES, 'UTF-8');
            } elseif (is_array($value)) {
                $input[$key] = $this->sanitizeInput($value);
            }
        }
        
        return $input;
    }
}

SQL Injection Prevention

// Always use parameterized queries and Eloquent ORM
class ApplicationRepository
{
    public function findByName(string $name): ?Application
    {
        // Safe: Uses parameter binding
        return Application::where('name', $name)->first();
    }
    
    public function searchApplications(string $query): Collection
    {
        // Safe: Eloquent handles escaping
        return Application::where('name', 'LIKE', "%{$query}%")
            ->orWhere('description', 'LIKE', "%{$query}%")
            ->get();
    }
    
    // NEVER do this - vulnerable to SQL injection
    // public function unsafeSearch(string $query): Collection
    // {
    //     return DB::select("SELECT * FROM applications WHERE name LIKE '%{$query}%'");
    // }
}

Audit Logging & Monitoring

Activity Logging

// Using Spatie Activity Log package
class Application extends Model
{
    use LogsActivity;
    
    protected static $logAttributes = [
        'name', 'git_repository', 'git_branch', 'fqdn'
    ];
    
    protected static $logOnlyDirty = true;
    
    public function getDescriptionForEvent(string $eventName): string
    {
        return "Application {$this->name} was {$eventName}";
    }
}

// Custom security events
class SecurityEventLogger
{
    public function logFailedLogin(string $email, string $ip): void
    {
        activity('security')
            ->withProperties([
                'email' => $email,
                'ip' => $ip,
                'user_agent' => request()->userAgent()
            ])
            ->log('Failed login attempt');
    }
    
    public function logSuspiciousActivity(User $user, string $activity): void
    {
        activity('security')
            ->causedBy($user)
            ->withProperties([
                'activity' => $activity,
                'ip' => request()->ip(),
                'timestamp' => now()
            ])
            ->log('Suspicious activity detected');
    }
}

Security Monitoring

class SecurityMonitoringService
{
    public function detectAnomalousActivity(User $user): bool
    {
        // Check for unusual login patterns
        $recentLogins = $user->activities()
            ->where('description', 'like', '%login%')
            ->where('created_at', '>=', now()->subHours(24))
            ->get();
        
        // Multiple failed attempts
        $failedAttempts = $recentLogins->where('description', 'Failed login attempt')->count();
        if ($failedAttempts > 5) {
            $this->triggerSecurityAlert($user, 'Multiple failed login attempts');
            return true;
        }
        
        // Login from new location
        $uniqueIps = $recentLogins->pluck('properties.ip')->unique();
        if ($uniqueIps->count() > 3) {
            $this->triggerSecurityAlert($user, 'Login from multiple IP addresses');
            return true;
        }
        
        return false;
    }
    
    private function triggerSecurityAlert(User $user, string $reason): void
    {
        // Send security notification
        $user->notify(new SecurityAlertNotification($reason));
        
        // Log security event
        activity('security')
            ->causedBy($user)
            ->withProperties(['reason' => $reason])
            ->log('Security alert triggered');
    }
}

Backup Security

Encrypted Backups

class SecureBackupService
{
    public function createEncryptedBackup(ScheduledDatabaseBackup $backup): void
    {
        $database = $backup->database;
        $dumpPath = $this->createDatabaseDump($database);
        
        // Encrypt backup file
        $encryptedPath = $this->encryptFile($dumpPath, $backup->encryption_key);
        
        // Upload to secure storage
        $this->uploadToSecureStorage($encryptedPath, $backup->s3Storage);
        
        // Clean up local files
        unlink($dumpPath);
        unlink($encryptedPath);
    }
    
    private function encryptFile(string $filePath, string $key): string
    {
        $data = file_get_contents($filePath);
        $encryptedData = encrypt($data, $key);
        
        $encryptedPath = $filePath . '.encrypted';
        file_put_contents($encryptedPath, $encryptedData);
        
        return $encryptedPath;
    }
}

Security Headers & CORS

Security Headers Configuration

// Security headers middleware
class SecurityHeadersMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);
        
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('X-XSS-Protection', '1; mode=block');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
        
        if ($request->secure()) {
            $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
        }
        
        return $response;
    }
}

CORS Configuration

// CORS configuration for API endpoints
return [
    'paths' => ['api/*', 'webhooks/*'],
    'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    'allowed_origins' => [
        'https://app.coolify.io',
        'https://*.coolify.io'
    ],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

Security Testing

Security Test Patterns

// Security-focused tests
test('prevents SQL injection in search', function () {
    $user = User::factory()->create();
    $maliciousInput = "'; DROP TABLE applications; --";
    
    $response = $this->actingAs($user)
        ->getJson("/api/v1/applications?search={$maliciousInput}");
    
    $response->assertStatus(200);
    
    // Verify applications table still exists
    expect(Schema::hasTable('applications'))->toBeTrue();
});

test('prevents XSS in application names', function () {
    $user = User::factory()->create();
    $xssPayload = '<script>alert("xss")</script>';
    
    $response = $this->actingAs($user)
        ->postJson('/api/v1/applications', [
            'name' => $xssPayload,
            'git_repository' => 'https://github.com/user/repo.git',
            'server_id' => Server::factory()->create()->id
        ]);
    
    $response->assertStatus(422);
});

test('enforces team isolation', function () {
    $user1 = User::factory()->create();
    $user2 = User::factory()->create();
    
    $team1 = Team::factory()->create();
    $team2 = Team::factory()->create();
    
    $user1->teams()->attach($team1);
    $user2->teams()->attach($team2);
    
    $application = Application::factory()->create(['team_id' => $team1->id]);
    
    $response = $this->actingAs($user2)
        ->getJson("/api/v1/applications/{$application->id}");
    
    $response->assertStatus(403);
});