['type' => 'integer'], 'uuid' => ['type' => 'string'], 'name' => ['type' => 'string'], 'description' => ['type' => 'string'], 'private_key' => ['type' => 'string', 'format' => 'private-key'], 'is_git_related' => ['type' => 'boolean'], 'team_id' => ['type' => 'integer'], 'created_at' => ['type' => 'string'], 'updated_at' => ['type' => 'string'], ], )] class PrivateKey extends BaseModel { use WithRateLimiting; protected $fillable = [ 'name', 'description', 'private_key', 'fingerprint', 'is_git_related', 'team_id', ]; protected $casts = [ 'private_key' => 'encrypted', ]; protected static function booted() { static::saving(function ($key) { $key->private_key = rtrim($key->private_key) . "\n"; if (!self::validatePrivateKey($key->private_key)) { throw ValidationException::withMessages([ 'private_key' => ['The private key is invalid.'], ]); } $key->fingerprint = self::generateFingerprint($key->private_key); }); static::deleted(function ($key) { self::deleteFromStorage($key); }); } public function getPublicKey() { return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key'; } // For backwards compatibility public function publicKey() { return $this->getPublicKey(); } public static function ownedByCurrentTeam(array $select = ['*']) { $selectArray = collect($select)->concat(['id']); return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); } public static function validatePrivateKey($privateKey) { try { PublicKeyLoader::load($privateKey); return true; } catch (\Throwable $e) { return false; } } public static function generateFingerprint($privateKey) { $key = PublicKeyLoader::load($privateKey); return $key->getPublicKey()->getFingerprint('sha256'); } public static function createAndStore(array $data) { $privateKey = new self($data); $privateKey->save(); $privateKey->storeInFileSystem(); return $privateKey; } public static function generateNewKeyPair($type = 'rsa') { try { $instance = new self(); $instance->rateLimit(10); $name = generate_random_name(); $description = 'Created by Coolify'; ['private' => $privateKey, 'public' => $publicKey] = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa'); return [ 'name' => $name, 'description' => $description, 'private_key' => $privateKey, 'public_key' => $publicKey, ]; } catch (\Throwable $e) { throw new \Exception("Failed to generate new {$type} key: " . $e->getMessage()); } } public static function extractPublicKeyFromPrivate($privateKey) { try { $key = PublicKeyLoader::load($privateKey); return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']); } catch (\Throwable $e) { return null; } } public static function validateAndExtractPublicKey($privateKey) { $isValid = self::validatePrivateKey($privateKey); $publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : ''; return [ 'isValid' => $isValid, 'publicKey' => $publicKey, ]; } public function storeInFileSystem() { $filename = "id_rsa@{$this->uuid}"; Storage::disk('ssh-keys')->put($filename, $this->private_key); return "/var/www/html/storage/app/ssh/keys/{$filename}"; } public static function deleteFromStorage(self $privateKey) { $filename = "id_rsa@{$privateKey->uuid}"; Storage::disk('ssh-keys')->delete($filename); } public function getKeyLocation() { return "/var/www/html/storage/app/ssh/keys/id_rsa@{$this->uuid}"; } public function updatePrivateKey(array $data) { $this->update($data); $this->storeInFileSystem(); return $this; } public function servers() { return $this->hasMany(Server::class); } public function applications() { return $this->hasMany(Application::class); } public function githubApps() { return $this->hasMany(GithubApp::class); } public function gitlabApps() { return $this->hasMany(GitlabApp::class); } public function isEmpty() { return $this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0; } }