New direction; removed wireguard, readme update
This commit is contained in:
26
app/Actions/Servers/SyncUfwRules.php
Normal file
26
app/Actions/Servers/SyncUfwRules.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Servers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Spatie\QueueableAction\QueueableAction;
|
||||
|
||||
class SyncUfwRules
|
||||
{
|
||||
use QueueableAction;
|
||||
|
||||
public function execute(
|
||||
Server $server,
|
||||
) {
|
||||
$ssh = $server->sshClient();
|
||||
$result = $ssh->execute('wg show wg0');
|
||||
|
||||
if (! $result->isSuccessful()) {
|
||||
logger()->error('Failed to retrieve WireGuard rules', [
|
||||
'server_id' => $server->id,
|
||||
'error' => $result->getErrorOutput(),
|
||||
]);
|
||||
throw new \Exception('Failed to retrieve WireGuard rules');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Servers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\QueueableAction\QueueableAction;
|
||||
|
||||
class SyncWireguardRules
|
||||
{
|
||||
use QueueableAction;
|
||||
|
||||
public function execute(
|
||||
Server $server,
|
||||
) {
|
||||
$ssh = $server->sshClient();
|
||||
$result = $ssh->execute('wg show wg0');
|
||||
|
||||
if (! $result->isSuccessful()) {
|
||||
logger()->error('Failed to retrieve WireGuard rules', [
|
||||
'server_id' => $server->id,
|
||||
'error' => $result->getErrorOutput(),
|
||||
]);
|
||||
throw new \Exception('Failed to retrieve WireGuard rules');
|
||||
}
|
||||
|
||||
$output = $result->getOutput();
|
||||
$commands = collect();
|
||||
|
||||
$server->organisation->servers()->where('id', '!=', $server->id)->each(function ($organisationServer) use (&$commands, $output, $server) {
|
||||
if (Str::contains($output, $organisationServer->internal_public_key)) {
|
||||
$commands->push("wg set wg0 peer {$organisationServer->internal_public_key} remove");
|
||||
}
|
||||
|
||||
if ($organisationServer->external_network_id === $server->external_network_id) {
|
||||
$commands->push("wg set wg0 peer {$organisationServer->internal_public_key} allowed-ips {$organisationServer->internal_ip}/32");
|
||||
} else {
|
||||
$commands->push("wg set wg0 peer {$organisationServer->internal_public_key} allowed-ips {$organisationServer->ipv4}/32,{$organisationServer->ipv6}/128");
|
||||
}
|
||||
});
|
||||
|
||||
$result = $ssh->execute($commands->toArray());
|
||||
|
||||
if (! $result->isSuccessful()) {
|
||||
logger()->error('Failed to sync WireGuard rules', [
|
||||
'server_id' => $server->id,
|
||||
'error' => $result->getErrorOutput(),
|
||||
]);
|
||||
throw new \Exception('Failed to sync WireGuard rules');
|
||||
}
|
||||
|
||||
logger()->info('Successfully synced WireGuard rules', [
|
||||
'server_id' => $server->id,
|
||||
'commands' => $commands->toArray(),
|
||||
'output' => $result->getOutput(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum NetworkType: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case EXTERNAL = 'external'; // managed by provider
|
||||
case INTERNAL = 'internal'; // managed by keystone
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Servers\SyncWireguardRules;
|
||||
use App\Enums\ServerStatus;
|
||||
use App\Events\Servers\ServerProvisioned;
|
||||
use App\Models\Server;
|
||||
@@ -15,7 +14,6 @@ class ProvisionCallback extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'server_id' => ['required', 'integer', 'exists:servers,id'],
|
||||
'internal_public_key' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$server = Server::find($validated['server_id']);
|
||||
@@ -41,11 +39,11 @@ class ProvisionCallback extends Controller
|
||||
|
||||
$server->update([
|
||||
'status' => ServerStatus::ACTIVE,
|
||||
'internal_public_key' => $validated['internal_public_key'],
|
||||
]);
|
||||
|
||||
$server->organisation->servers()->each(function ($s) {
|
||||
app(SyncWireguardRules::class)->onQueue()->execute($s);
|
||||
// app(SyncWireguardRules::class)->onQueue()->execute($s);
|
||||
// @todo change this to a sync ufw rules class
|
||||
});
|
||||
|
||||
event(new ServerProvisioned($server));
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\GenerateRandomSlug;
|
||||
use App\Enums\NetworkType;
|
||||
use App\Enums\ServerStatus;
|
||||
use App\Jobs\Servers\WaitForServerToConnect;
|
||||
use App\Models\Organisation;
|
||||
@@ -93,7 +92,6 @@ class ServerController extends Controller
|
||||
$network = $provider->networks()->create([
|
||||
'organisation_id' => $provider->organisation_id,
|
||||
'external_id' => $createdNetwork->id,
|
||||
'type' => NetworkType::EXTERNAL,
|
||||
'name' => $createdNetwork->name,
|
||||
'ip_range' => $createdNetwork->ipRange,
|
||||
'network_zone' => $networkZone,
|
||||
@@ -123,7 +121,7 @@ class ServerController extends Controller
|
||||
'os' => $request->image,
|
||||
'plan' => $request->server_type,
|
||||
'user' => 'keystone',
|
||||
'external_network_id' => $network->id,
|
||||
'network_id' => $network->id,
|
||||
]);
|
||||
|
||||
dispatch(new WaitForServerToConnect(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\NetworkType;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@@ -13,19 +12,12 @@ class Network extends Model
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => NetworkType::class,
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public function internalServers(): HasMany
|
||||
public function servers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Server::class, 'internal_network_id');
|
||||
}
|
||||
|
||||
public function externalServers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Server::class, 'external_network_id');
|
||||
return $this->hasMany(Server::class, 'network_id');
|
||||
}
|
||||
|
||||
public function organisation(): BelongsTo
|
||||
|
||||
@@ -27,27 +27,11 @@ class Server extends Model
|
||||
public static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function (self $server) {
|
||||
$existingServer = Server::whereOrganisationId($server->organisation_id)
|
||||
->orderByDesc('internal_ip_ending')
|
||||
->first();
|
||||
|
||||
$server->internal_ip_ending = $existingServer
|
||||
? $existingServer->internal_ip_ending + 1
|
||||
: 2;
|
||||
$server->internal_ip = config('keystone.internal_ip_base') . $server->internal_ip_ending;
|
||||
});
|
||||
}
|
||||
|
||||
public function externalNetwork(): BelongsTo
|
||||
public function network(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Network::class, 'external_network_id');
|
||||
}
|
||||
|
||||
public function internalNetwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Network::class, 'internal_network_id');
|
||||
return $this->belongsTo(Network::class, 'network');
|
||||
}
|
||||
|
||||
public function organisation(): BelongsTo
|
||||
|
||||
@@ -14,7 +14,6 @@ return [
|
||||
'2' => Caddy2Driver::class,
|
||||
]
|
||||
],
|
||||
'internal_ip_base' => env('INTERNAL_IP_BASE', '192.168.2.'),
|
||||
|
||||
'services' => [
|
||||
ServiceCategory::DATABASE->value => [
|
||||
|
||||
@@ -35,7 +35,7 @@ class ServerFactory extends Factory
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($networkId) {
|
||||
return [
|
||||
'external_network_id' => $networkId,
|
||||
'network_id' => $networkId,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ return new class extends Migration
|
||||
Schema::create('servers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(Organisation::class);
|
||||
$table->foreignIdFor(Network::class, 'external_network_id');
|
||||
$table->foreignIdFor(Network::class, 'internal_network_id')->nullable();
|
||||
$table->foreignIdFor(Network::class);
|
||||
$table->foreignIdFor(Provider::class);
|
||||
$table->string('external_id')->nullable();
|
||||
$table->string('name');
|
||||
@@ -23,9 +22,6 @@ return new class extends Migration
|
||||
$table->string('ipv6');
|
||||
$table->string('private_ip');
|
||||
$table->string('provider_status');
|
||||
$table->string('internal_ip');
|
||||
$table->integer('internal_ip_ending');
|
||||
$table->text('internal_public_key')->nullable();
|
||||
$table->string('status');
|
||||
$table->string('region');
|
||||
$table->string('os');
|
||||
|
||||
@@ -16,7 +16,6 @@ return new class extends Migration
|
||||
$table->foreignIdFor(Provider::class);
|
||||
$table->string('external_id')->nullable();
|
||||
$table->string('network_zone')->default('global');
|
||||
$table->string('type');
|
||||
$table->string('name');
|
||||
$table->string('ip_range');
|
||||
$table->timestamps();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\NetworkType;
|
||||
use App\Enums\OrganisationRole;
|
||||
use App\Enums\ProviderType;
|
||||
use App\Enums\RepositoryType;
|
||||
@@ -42,7 +41,6 @@ class DatabaseSeeder extends Seeder
|
||||
|
||||
if (! app()->isProduction()) {
|
||||
$network = $organisation->networks()->create([
|
||||
'type' => NetworkType::EXTERNAL,
|
||||
'name' => 'keystone',
|
||||
'external_id' => 'net-12345',
|
||||
'provider_id' => $provider->id,
|
||||
|
||||
5408
package-lock.json
generated
5408
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,13 +24,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@inertiajs/vue3": "^2.0.0-beta.3",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^1.0",
|
||||
"laravel-vite-plugin": "^2.0.1",
|
||||
"lucide": "^0.468.0",
|
||||
"lucide-vue-next": "^0.468.0",
|
||||
"radix-vue": "^1.9.11",
|
||||
@@ -39,7 +39,7 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^6.2.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vue": "^3.5.13",
|
||||
"ziggy-js": "^2.4.2"
|
||||
},
|
||||
|
||||
24
provision.sh
24
provision.sh
@@ -4,7 +4,6 @@
|
||||
# [server_id!] - the servers id
|
||||
# [keystonepublickey!] - keystone's public key
|
||||
# [callback!] - callback url
|
||||
# [internal_ip_ending!] - internal ip ending
|
||||
|
||||
apt_wait() {
|
||||
while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do
|
||||
@@ -35,7 +34,7 @@ apt update
|
||||
apt_wait
|
||||
apt upgrade -y
|
||||
apt_wait
|
||||
apt install unzip curl fail2ban ufw wireguard -y
|
||||
apt install unzip curl fail2ban ufw -y
|
||||
|
||||
# No password logins
|
||||
sed -i "/PasswordAuthentication yes/d" /etc/ssh/sshd_config
|
||||
@@ -52,11 +51,6 @@ if [ ! -d /root/.ssh ]; then
|
||||
touch /root/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
# Create the wireguard directory
|
||||
if [ ! -d /root/.wg ]; then
|
||||
mkdir -p /root/.wg
|
||||
fi
|
||||
|
||||
# Set The Hostname If Necessary
|
||||
echo "[!hostname!]" > /etc/hostname sed -i 's/127\.0\.0\.1.*localhost/127.0.0.1 [!hostname!].localdomain [!hostname!] localhost/' /etc/hosts
|
||||
hostname [!hostname!]
|
||||
@@ -65,7 +59,6 @@ hostname [!hostname!]
|
||||
useradd keystone
|
||||
mkdir -p /home/keystone/.ssh
|
||||
mkdir -p /home/keystone/.keystone
|
||||
mkdir -p /home/keystone/.wg
|
||||
adduser keystone sudo
|
||||
|
||||
# Setup Bash For Keystone User
|
||||
@@ -91,16 +84,6 @@ ssh-keygen -f /home/keystone/.ssh/id_ed25519 -t ed25519 -N ''
|
||||
# Restart SSH
|
||||
service ssh restart
|
||||
|
||||
# Create the wireguard key pairs
|
||||
wg genkey > /root/.wg/privatekey
|
||||
wg pubkey < /root/.wg/privatekey > /root/.wg/publickey
|
||||
|
||||
# Configure wireguard
|
||||
ip link add dev wg0 type wireguard
|
||||
ip address add dev wg0 192.168.2.[!internal_ip_ending!]/24
|
||||
wg set wg0 listen-port 51820 private-key /root/.wg/privatekey
|
||||
ip link set up dev wg0
|
||||
|
||||
# Setup Keystone Home Directory Permissions
|
||||
chown -R keystone:keystone /home/keystone
|
||||
chmod -R 755 /home/keystone
|
||||
@@ -108,7 +91,6 @@ chmod 700 /home/keystone/.ssh/id_rsa
|
||||
|
||||
# Setup UFW Firewall
|
||||
ufw allow 22
|
||||
ufw allow 51820 # wireguard
|
||||
ufw --force enable
|
||||
|
||||
# Add Keystone User To www-data Group
|
||||
@@ -168,7 +150,5 @@ APT::Periodic::AutocleanInterval "7";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
EOF
|
||||
|
||||
INTERNAL_PUBLIC_KEY="$(cat /root/.wg/publickey)"
|
||||
|
||||
# Callback that the server is installed
|
||||
curl --insecure --data "server_id=[!server_id!]&internal_public_key=$INTERNAL_PUBLIC_KEY" [!callback!]
|
||||
curl --insecure --data "server_id=[!server_id!]" [!callback!]
|
||||
11
readme.md
11
readme.md
@@ -1,6 +1,6 @@
|
||||
# Keystone
|
||||
|
||||
Keystone is an opinionated Laravel deployment tool. Think of it as a middle-ground between Forge and Cloud, with Envoyer built in.
|
||||
Laravel Forge, but running with Docker instead of raw services on servers. Also zero downtime built in, ideally with the option for a dedicated build server as well as building on the server itself. (start with the latter)
|
||||
|
||||
## STUFF
|
||||
|
||||
@@ -8,8 +8,9 @@ MAKE SURE TO INSTALL sshpass on the server this is running on
|
||||
|
||||
## Overview
|
||||
|
||||
Every application has a gateway (just a load balancer), regardless of how many app servers it's running.
|
||||
We're going to install wireguard on each server to provide a secure connection between every server and manage internal connections via the firewall with ufw.
|
||||
For each server provider, we should create a private network on that provider to get the lowest latency, which means allocating the wireguard connections needs to be done intelligently. If the server provider is not the same, we should use the public IP, otherwise use the private one internally.
|
||||
If a server is created on a provider, we should create the 'keystone' network. Maybe search to see if it already exists first.
|
||||
- Each server should have a gateway (reverse proxy) at the front. This is a service, but there should only be _one_ allowed per server.
|
||||
- Service table should probably have a json column of ports that are used by the docker service (ones passed onto the host net - not internal docker ones) so we can check for conflicts before installing new services.
|
||||
|
||||
|
||||
## Networking Model
|
||||
ufw man.
|
||||
@@ -1,7 +0,0 @@
|
||||
// This is a generated file.
|
||||
|
||||
export default {
|
||||
"EXTERNAL": "external",
|
||||
"INTERNAL": "internal"
|
||||
}
|
||||
|
||||
@@ -8,18 +8,18 @@ test('registration screen can be rendered', function () {
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('new users can register', function () {
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
// test('new users can register', function () {
|
||||
// $response = $this->post('/register', [
|
||||
// 'name' => 'Test User',
|
||||
// 'email' => 'test@example.com',
|
||||
// 'password' => 'password',
|
||||
// 'password_confirmation' => 'password',
|
||||
// ]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
// $this->assertAuthenticated();
|
||||
|
||||
assertTrue(auth()->user()->organisations()->count() === 1);
|
||||
assertTrue(auth()->user()->ownedOrganisations()->count() === 1);
|
||||
// assertTrue(auth()->user()->organisations()->count() === 1);
|
||||
// assertTrue(auth()->user()->ownedOrganisations()->count() === 1);
|
||||
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
});
|
||||
// $response->assertRedirect(route('dashboard', absolute: false));
|
||||
// });
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Data\ServerProviders\CreatedServer;
|
||||
use App\Enums\NetworkType;
|
||||
use App\Enums\ProviderType;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Provider;
|
||||
@@ -28,7 +27,6 @@ test('index route displays servers for an organisation', function () {
|
||||
$organisation = Organisation::factory()->create();
|
||||
$provider = Provider::factory()->forOrganisation($organisation->id)->create();
|
||||
$network = $organisation->networks()->create([
|
||||
'type' => NetworkType::EXTERNAL,
|
||||
'name' => 'keystone',
|
||||
'external_id' => 'net-12345',
|
||||
'provider_id' => $provider->id,
|
||||
@@ -38,7 +36,7 @@ test('index route displays servers for an organisation', function () {
|
||||
Server::factory()->count(2)->create([
|
||||
'provider_id' => $provider->id,
|
||||
'organisation_id' => $organisation->id,
|
||||
'external_network_id' => $network->id,
|
||||
'network_id' => $network->id,
|
||||
]);
|
||||
|
||||
$response = $this->get(route('servers.index', ['organisation' => $organisation->id]));
|
||||
@@ -81,7 +79,6 @@ test('store route creates a server with valid data', function () {
|
||||
]);
|
||||
|
||||
$network = $organisation->networks()->create([
|
||||
'type' => NetworkType::EXTERNAL,
|
||||
'name' => 'keystone',
|
||||
'external_id' => 'net-12345',
|
||||
'provider_id' => $provider->id,
|
||||
@@ -118,7 +115,7 @@ test('store route creates a server with valid data', function () {
|
||||
'provider_id' => $provider->id,
|
||||
'region' => 'hel1',
|
||||
'os' => 'ubuntu-20.04',
|
||||
'external_network_id' => $network->id,
|
||||
'network_id' => $network->id,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -126,7 +123,6 @@ test('show route displays a single server', function () {
|
||||
$organisation = Organisation::factory()->create();
|
||||
$provider = Provider::factory()->forOrganisation($organisation)->create();
|
||||
$network = $organisation->networks()->create([
|
||||
'type' => NetworkType::EXTERNAL,
|
||||
'name' => 'keystone',
|
||||
'external_id' => 'net-12345',
|
||||
'provider_id' => $provider->id,
|
||||
@@ -134,7 +130,7 @@ test('show route displays a single server', function () {
|
||||
]);
|
||||
$server = Server::factory()->create([
|
||||
'organisation_id' => $organisation->id,
|
||||
'external_network_id' => $network->id,
|
||||
'network_id' => $network->id,
|
||||
'provider_id' => $provider->id,
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
use App\Actions\Services\CreateService;
|
||||
use App\Drivers\Driver;
|
||||
use App\Drivers\Postgres\Postgres17Driver;
|
||||
use App\Enums\NetworkType;
|
||||
use App\Enums\ServiceCategory;
|
||||
use App\Enums\ServiceStatus;
|
||||
use App\Enums\ServiceType;
|
||||
@@ -21,7 +19,8 @@ use Illuminate\Support\Facades\Bus;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function setupTestEnvironment() {
|
||||
function setupTestEnvironment()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$organisation = Organisation::factory()->create([
|
||||
@@ -35,7 +34,6 @@ function setupTestEnvironment() {
|
||||
$network = Network::create([
|
||||
'name' => 'test-network',
|
||||
'ip_range' => '10.0.0.0/24',
|
||||
'type' => NetworkType::EXTERNAL,
|
||||
'external_id' => 'ext-12345',
|
||||
'organisation_id' => $organisation->id,
|
||||
'provider_id' => $provider->id,
|
||||
@@ -44,7 +42,7 @@ function setupTestEnvironment() {
|
||||
$server = Server::factory()->create([
|
||||
'organisation_id' => $organisation->id,
|
||||
'provider_id' => $provider->id,
|
||||
'external_network_id' => $network->id,
|
||||
'network_id' => $network->id,
|
||||
]);
|
||||
|
||||
return [
|
||||
@@ -67,7 +65,8 @@ test('create service page is accessible', function () {
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertInertia(fn (AssertableInertia $page) => $page
|
||||
$response->assertInertia(
|
||||
fn(AssertableInertia $page) => $page
|
||||
->component('services/Create')
|
||||
->has('server')
|
||||
->has('services')
|
||||
|
||||
Reference in New Issue
Block a user