Refactor SSH Keys
This commit is contained in:
@@ -3,22 +3,14 @@
|
||||
namespace App\Livewire\Security\PrivateKey;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Livewire\Component;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
class Create extends Component
|
||||
{
|
||||
use WithRateLimiting;
|
||||
|
||||
public string $name;
|
||||
|
||||
public string $value;
|
||||
|
||||
public string $name = '';
|
||||
public string $value = '';
|
||||
public ?string $from = null;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public ?string $publicKey = null;
|
||||
|
||||
protected $rules = [
|
||||
@@ -26,84 +18,69 @@ class Create extends Component
|
||||
'value' => 'required|string',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'name' => 'name',
|
||||
'value' => 'private Key',
|
||||
];
|
||||
|
||||
public function generateNewRSAKey()
|
||||
{
|
||||
try {
|
||||
$this->rateLimit(10);
|
||||
$this->name = generate_random_name();
|
||||
$this->description = 'Created by Coolify';
|
||||
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->generateNewKey('rsa');
|
||||
}
|
||||
|
||||
public function generateNewEDKey()
|
||||
{
|
||||
try {
|
||||
$this->rateLimit(10);
|
||||
$this->name = generate_random_name();
|
||||
$this->description = 'Created by Coolify';
|
||||
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->generateNewKey('ed25519');
|
||||
}
|
||||
|
||||
public function updated($updateProperty)
|
||||
private function generateNewKey($type)
|
||||
{
|
||||
if ($updateProperty === 'value') {
|
||||
try {
|
||||
$key = PublicKeyLoader::load($this->$updateProperty);
|
||||
$this->publicKey = $key->getPublicKey()->toString('OpenSSH', ['comment' => '']);
|
||||
} catch (\Throwable $e) {
|
||||
$this->publicKey = '';
|
||||
$this->addError('value', 'Invalid private key');
|
||||
}
|
||||
$keyData = PrivateKey::generateNewKeyPair($type);
|
||||
$this->setKeyData($keyData);
|
||||
}
|
||||
|
||||
public function updated($property)
|
||||
{
|
||||
if ($property === 'value') {
|
||||
$this->validatePrivateKey();
|
||||
}
|
||||
$this->validateOnly($updateProperty);
|
||||
}
|
||||
|
||||
public function createPrivateKey()
|
||||
{
|
||||
$this->validate([
|
||||
'name' => 'required|string',
|
||||
'value' => [
|
||||
'required',
|
||||
'string',
|
||||
function ($attribute, $value, $fail) {
|
||||
try {
|
||||
PublicKeyLoader::load($value);
|
||||
} catch (\Throwable $e) {
|
||||
$fail('The private key is invalid.');
|
||||
}
|
||||
},
|
||||
],
|
||||
]);
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
$this->value = trim($this->value);
|
||||
if (! str_ends_with($this->value, "\n")) {
|
||||
$this->value .= "\n";
|
||||
}
|
||||
$private_key = PrivateKey::create([
|
||||
$privateKey = PrivateKey::createAndStore([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'private_key' => $this->value,
|
||||
'private_key' => trim($this->value) . "\n",
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
if ($this->from === 'server') {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
|
||||
return $this->redirectAfterCreation($privateKey);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function setKeyData(array $keyData)
|
||||
{
|
||||
$this->name = $keyData['name'];
|
||||
$this->description = $keyData['description'];
|
||||
$this->value = $keyData['private_key'];
|
||||
$this->publicKey = $keyData['public_key'];
|
||||
}
|
||||
|
||||
private function validatePrivateKey()
|
||||
{
|
||||
$validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
|
||||
$this->publicKey = $validationResult['publicKey'];
|
||||
|
||||
if (!$validationResult['isValid']) {
|
||||
$this->addError('value', 'Invalid private key');
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectAfterCreation(PrivateKey $privateKey)
|
||||
{
|
||||
return $this->from === 'server'
|
||||
? redirect()->route('dashboard')
|
||||
: redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
|
||||
}
|
||||
}
|
||||
|
@@ -3,8 +3,10 @@
|
||||
namespace App\Models;
|
||||
|
||||
use OpenApi\Attributes as OA;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Private Key model',
|
||||
@@ -23,6 +25,8 @@ use Illuminate\Validation\ValidationException;
|
||||
)]
|
||||
class PrivateKey extends BaseModel
|
||||
{
|
||||
use WithRateLimiting;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
@@ -39,45 +43,127 @@ class PrivateKey extends BaseModel
|
||||
protected static function booted()
|
||||
{
|
||||
static::saving(function ($key) {
|
||||
$privateKey = data_get($key, 'private_key');
|
||||
if (substr($privateKey, -1) !== "\n") {
|
||||
$key->private_key = $privateKey . "\n";
|
||||
}
|
||||
$key->private_key = rtrim($key->private_key) . "\n";
|
||||
|
||||
try {
|
||||
$publicKey = PublicKeyLoader::load($key->private_key)->getPublicKey();
|
||||
$key->fingerprint = $publicKey->getFingerprint('sha256');
|
||||
} catch (\Throwable $e) {
|
||||
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 PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
|
||||
return self::whereTeamId(currentTeam()->id)->select($selectArray->all());
|
||||
}
|
||||
|
||||
public function publicKey()
|
||||
public static function validatePrivateKey($privateKey)
|
||||
{
|
||||
try {
|
||||
return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
|
||||
PublicKeyLoader::load($privateKey);
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return 'Error loading private key';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
public static function generateFingerprint($privateKey)
|
||||
{
|
||||
if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {
|
||||
return true;
|
||||
}
|
||||
$key = PublicKeyLoader::load($privateKey);
|
||||
return $key->getPublicKey()->getFingerprint('sha256');
|
||||
}
|
||||
|
||||
return false;
|
||||
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()
|
||||
@@ -100,9 +186,11 @@ class PrivateKey extends BaseModel
|
||||
return $this->hasMany(GitlabApp::class);
|
||||
}
|
||||
|
||||
public function generateFingerprint()
|
||||
public function isEmpty()
|
||||
{
|
||||
$key = PublicKeyLoader::load($this->private_key);
|
||||
return $key->getPublicKey()->getFingerprint('sha256');
|
||||
return $this->servers()->count() === 0
|
||||
&& $this->applications()->count() === 0
|
||||
&& $this->githubApps()->count() === 0
|
||||
&& $this->gitlabApps()->count() === 0;
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,8 @@
|
||||
<div class="font-bold">You should not use passphrase protected keys.</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<x-forms.button wire:click="generateNewRSAKey">Generate new RSA SSH Key</x-forms.button>
|
||||
<x-forms.button wire:click="generateNewEDKey">Generate new ED25519 SSH Key</x-forms.button>
|
||||
<x-forms.button wire:click="generateNewRSAKey">Generate new RSA SSH Key</x-forms.button>
|
||||
<x-forms.button wire:click="generateNewEDKey">Generate new ED25519 SSH Key</x-forms.button>
|
||||
</div>
|
||||
<form class="flex flex-col gap-2" wire:submit='createPrivateKey'>
|
||||
<div class="flex gap-2">
|
||||
|
Reference in New Issue
Block a user