--- description: globs: alwaysApply: false --- # 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 - **[Laravel Fortify](mdc:config/fortify.php)** - Core authentication scaffolding (4.9KB, 149 lines) - **[Laravel Sanctum](mdc:config/sanctum.php)** - API token authentication (2.4KB, 69 lines) - **[Laravel Socialite](mdc:config/services.php)** - OAuth provider integration ### OAuth Integration - **[OauthSetting.php](mdc:app/Models/OauthSetting.php)** - OAuth provider configurations - **Supported Providers**: - Google OAuth - Microsoft Azure AD - Clerk - Authentik - Discord - GitHub (via GitHub Apps) - GitLab ### Authentication Models ```php // 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 ### Team-Based Multi-Tenancy - **[Team.php](mdc:app/Models/Team.php)** - Multi-tenant organization structure (8.9KB, 308 lines) - **[TeamInvitation.php](mdc:app/Models/TeamInvitation.php)** - Secure team collaboration - **Role-based permissions** within teams - **Resource isolation** by team ownership ### Authorization Patterns ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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](mdc:app/Models/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 ```php 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 ```php 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 ```yaml # 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](mdc:app/Models/SslCertificate.php)** - SSL certificate automation - **Let's Encrypt** integration for free certificates - **Automatic renewal** and monitoring - **Custom certificate** upload support ### SSL Configuration ```php 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 ```php 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 ```php 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 ```php 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, '
');
                
                // 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
```php
// 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
```php
// 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
```php
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
```php
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
```php
// 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
```php
// 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
```php
// 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 = '';
    
    $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);
});
```