Skip to content

Commit 801f0e5

Browse files
[Feat] Server Security (#1143)
* wip * linting * tsx * code rabbit feedback * warning banner * Update resources/js/pages/security/index.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent a47047c commit 801f0e5

41 files changed

Lines changed: 2165 additions & 11 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace App\Actions\Server\Security;
4+
5+
use App\Jobs\Server\Security\DetectSecurityJob;
6+
use App\Models\Server;
7+
8+
class CheckSecurityState
9+
{
10+
public function check(Server $server): void
11+
{
12+
DetectSecurityJob::dispatch($server);
13+
}
14+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Actions\Server\Security;
4+
5+
use App\Jobs\Server\Security\ConfigureFail2banJob;
6+
use App\Models\Server;
7+
use App\Models\Service;
8+
use App\Services\Fail2ban\Fail2ban;
9+
use Illuminate\Support\Facades\Validator;
10+
11+
class ConfigureFail2ban
12+
{
13+
/**
14+
* @param array<string, mixed> $input
15+
*/
16+
public function configure(Server $server, array $input): void
17+
{
18+
$service = $server->fail2ban();
19+
20+
if (! $service instanceof Service) {
21+
return;
22+
}
23+
24+
Validator::make($input, Fail2ban::jailValidationRules())->validate();
25+
26+
$service->type_data = [
27+
'maxretry' => (int) ($input['maxretry'] ?? 5),
28+
'findtime' => $input['findtime'] ?? '10m',
29+
'bantime' => $input['bantime'] ?? '10m',
30+
'ignoreip' => $input['ignoreip'] ?? '',
31+
];
32+
$service->save();
33+
34+
dispatch(new ConfigureFail2banJob($service))->onQueue('ssh');
35+
}
36+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Actions\Server\Security;
4+
5+
use App\Models\Server;
6+
use Closure;
7+
use Cron\CronExpression;
8+
use Illuminate\Support\Facades\Validator;
9+
10+
class ManageAutoUpdate
11+
{
12+
/**
13+
* @param array<string, mixed> $input
14+
*/
15+
public function update(Server $server, array $input): void
16+
{
17+
$this->validate($input);
18+
19+
$enabled = (bool) ($input['enabled'] ?? false);
20+
21+
$server->auto_update = $enabled;
22+
$server->auto_update_schedule = $enabled ? $input['schedule'] : null;
23+
$server->save();
24+
}
25+
26+
/**
27+
* @param array<string, mixed> $input
28+
*/
29+
private function validate(array $input): void
30+
{
31+
Validator::make($input, [
32+
'enabled' => ['required', 'boolean'],
33+
'schedule' => [
34+
'nullable',
35+
'required_if:enabled,true',
36+
'string',
37+
function (string $attribute, mixed $value, Closure $fail): void {
38+
if ($value && ! CronExpression::isValidExpression((string) $value)) {
39+
$fail('The schedule must be a valid cron expression.');
40+
}
41+
},
42+
],
43+
])->validate();
44+
}
45+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Actions\Server\Security;
4+
5+
use App\Enums\SecurityControlStatus;
6+
use App\Jobs\Server\Security\ApplyPasswordAuthJob;
7+
use App\Models\Server;
8+
use Illuminate\Support\Facades\Validator;
9+
10+
class ManagePasswordAuth
11+
{
12+
/**
13+
* @param array<string, mixed> $input
14+
*/
15+
public function update(Server $server, array $input): void
16+
{
17+
Validator::make($input, [
18+
'enabled' => ['required', 'boolean'],
19+
])->validate();
20+
21+
$enabled = (bool) $input['enabled'];
22+
23+
$server->refresh();
24+
$security = $server->feature_data['security'] ?? [];
25+
$security['password_authentication'] = array_merge($security['password_authentication'] ?? [], [
26+
'enabled' => $enabled,
27+
'status' => SecurityControlStatus::UPDATING->value,
28+
]);
29+
$server->jsonUpdate('feature_data', 'security', $security);
30+
31+
dispatch(new ApplyPasswordAuthJob($server, $enabled))->onQueue('ssh');
32+
}
33+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Actions\Server\Security;
4+
5+
use App\Enums\SecurityControlStatus;
6+
use App\Jobs\Server\Security\ApplyRootLoginJob;
7+
use App\Models\Server;
8+
use Illuminate\Support\Facades\Validator;
9+
use Illuminate\Validation\ValidationException;
10+
11+
class ManageRootLogin
12+
{
13+
/**
14+
* @param array<string, mixed> $input
15+
*
16+
* @throws ValidationException
17+
*/
18+
public function update(Server $server, array $input): void
19+
{
20+
Validator::make($input, [
21+
'enabled' => ['required', 'boolean'],
22+
])->validate();
23+
24+
$enabled = (bool) $input['enabled'];
25+
26+
if (! $enabled && $server->getSshUser() === 'root') {
27+
throw ValidationException::withMessages([
28+
'enabled' => 'Vito connects to this server as root. Switch to a non-root SSH user before disabling root login.',
29+
]);
30+
}
31+
32+
$server->refresh();
33+
$security = $server->feature_data['security'] ?? [];
34+
$security['root_login'] = array_merge($security['root_login'] ?? [], [
35+
'enabled' => $enabled,
36+
'status' => SecurityControlStatus::UPDATING->value,
37+
]);
38+
$server->jsonUpdate('feature_data', 'security', $security);
39+
40+
dispatch(new ApplyRootLoginJob($server, $enabled))->onQueue('ssh');
41+
}
42+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Actions\Server\Update;
6+
use App\Enums\ServerStatus;
7+
use App\Models\Server;
8+
use Cron\CronExpression;
9+
use Illuminate\Console\Command;
10+
11+
class RunServerAutoUpdatesCommand extends Command
12+
{
13+
protected $signature = 'servers:auto-update';
14+
15+
protected $description = 'Dispatch package updates to servers whose auto-update schedule is due';
16+
17+
public function handle(): void
18+
{
19+
Server::query()
20+
->where('status', ServerStatus::READY)
21+
->where('auto_update', true)
22+
->whereNotNull('auto_update_schedule')
23+
->chunk(50, function ($servers): void {
24+
/** @var Server $server */
25+
foreach ($servers as $server) {
26+
if (! CronExpression::isValidExpression((string) $server->auto_update_schedule)) {
27+
continue;
28+
}
29+
30+
if ((new CronExpression((string) $server->auto_update_schedule))->isDue(now(), config('app.timezone'))) {
31+
app(Update::class)->update($server);
32+
}
33+
}
34+
});
35+
}
36+
}

app/Console/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ protected function schedule(Schedule $schedule): void
2121
$schedule->command('metrics:get')->everyMinute();
2222
$schedule->command('servers:check')->everyFiveMinutes();
2323
$schedule->command('servers:check-updates')->dailyAt('02:00');
24+
$schedule->command('servers:auto-update')->everyMinute()->withoutOverlapping();
2425
$schedule->command('domains:check-pending')->everyFiveMinutes();
2526
$schedule->command('ssl:renew-wildcards')->daily();
2627
$schedule->command('ssl:check-expiry')->daily();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Enums;
4+
5+
use App\Contracts\VitoEnum;
6+
7+
enum SecurityControlStatus: string implements VitoEnum
8+
{
9+
case DISABLED = 'disabled';
10+
case UPDATING = 'updating';
11+
case READY = 'ready';
12+
case FAILED = 'failed';
13+
14+
public function getColor(): string
15+
{
16+
return match ($this) {
17+
self::DISABLED => 'gray',
18+
self::UPDATING => 'warning',
19+
self::READY => 'success',
20+
self::FAILED => 'danger',
21+
};
22+
}
23+
24+
public function getText(): string
25+
{
26+
return $this === self::READY ? 'active' : $this->value;
27+
}
28+
}

app/Http/Controllers/ConsoleController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function token(Server $server, Request $request): JsonResponse
3737
$this->validate($request, [
3838
'user' => [
3939
'required',
40-
Rule::in($server->getSshUsers()),
40+
Rule::in($server->sshLoginUsers()),
4141
],
4242
]);
4343

0 commit comments

Comments
 (0)