787 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			787 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
---
 | 
						|
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
 | 
						|
  - Authentik
 | 
						|
  - 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, '<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
 | 
						|
```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 = '<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);
 | 
						|
});
 | 
						|
```
 |