--- 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);
});
```