From 8cf956b4d482ebc63fd2348908f0999967e57917 Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 18 Dec 2025 14:38:18 +0100 Subject: [PATCH] Add ip allow list for traefik in network configuration --- app/Livewire/Project/Application/General.php | 5 + app/Models/Application.php | 1 + bootstrap/helpers/docker.php | 50 +++++++++- bootstrap/helpers/parsers.php | 12 ++- bootstrap/helpers/shared.php | 8 +- ...test_ipallowlist_to_applications_table.php | 28 ++++++ openapi.json | 5 + openapi.yaml | 4 + .../project/application/general.blade.php | 5 + t_ipallowlist | 93 +++++++++++++++++++ 10 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 database/migrations/2025_12_18_095331_add_test_ipallowlist_to_applications_table.php create mode 100644 t_ipallowlist diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 71ca9720e..d2263c137 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -145,6 +145,9 @@ class General extends Component #[Validate(['string', 'nullable'])] public ?string $httpBasicAuthPassword = null; + #[Validate(['string', 'nullable'])] + public ?string $testIpallowlist = null; + #[Validate(['nullable'])] public ?string $watchPaths = null; @@ -413,6 +416,7 @@ class General extends Component $this->application->is_http_basic_auth_enabled = $this->isHttpBasicAuthEnabled; $this->application->http_basic_auth_username = $this->httpBasicAuthUsername; $this->application->http_basic_auth_password = $this->httpBasicAuthPassword; + $this->application->test_ipallowlist = $this->testIpallowlist; $this->application->watch_paths = $this->watchPaths; $this->application->redirect = $this->redirect; @@ -463,6 +467,7 @@ class General extends Component $this->isHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled; $this->httpBasicAuthUsername = $this->application->http_basic_auth_username; $this->httpBasicAuthPassword = $this->application->http_basic_auth_password; + $this->testIpallowlist = $this->application->test_ipallowlist; $this->watchPaths = $this->application->watch_paths; $this->redirect = $this->application->redirect; diff --git a/app/Models/Application.php b/app/Models/Application.php index 6e920f8e6..d9bc234ba 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -106,6 +106,7 @@ use Visus\Cuid2\Cuid2; 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'], + 'test_ipallowlist' => ['type' => 'string', 'nullable' => true, 'description' => 'List of allowed ips'], ] )] diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 4a0faaec1..d3fe50ed1 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -5,6 +5,7 @@ use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\Server; use App\Models\ServiceApplication; +use Aws\Middleware; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Url\Url; @@ -375,7 +376,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, return $labels->sort(); } -function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null) +function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?string $test_ipallowlist = null) { $labels = collect([]); $labels->push('traefik.enable=true'); @@ -386,6 +387,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null; $http_basic_auth_label = "http-basic-auth-{$uuid}"; + $list_string = ''; if ($is_http_basic_auth_enabled) { $hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]); } @@ -394,6 +396,21 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push("traefik.http.middlewares.{$http_basic_auth_label}.basicauth.users={$http_basic_auth_username}:{$hashedPassword}"); } + if ($test_ipallowlist) { + $ip_list = explode(',', $test_ipallowlist); + $sane_ip_list = []; + foreach ($ip_list as $_ip) { + if (filter_var($_ip, FILTER_VALIDATE_IP)) { + $sane_ip_list[] = $_ip; + } + } + + if (count($sane_ip_list) > 0) { + $list_string = implode(', ', $sane_ip_list); + $labels->push("traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange={$list_string}"); + } + } + $middlewares_from_labels = collect([]); if ($serviceLabels) { @@ -492,6 +509,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_http_basic_auth_enabled) { $middlewares->push($http_basic_auth_label); } + if (isListStringFilled($test_ipallowlist)) { + $middlewares->push('test-ipallowlist'); + } $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { $middlewares->push($middleware_name); }); @@ -537,7 +557,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push("traefik.http.routers.{$http_label}.service={$http_label}"); } if ($is_force_https_enabled) { - $labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https"); + $middleware_append_string = 'redirect-to-https'; + if (isListStringFilled($test_ipallowlist)) { + $middleware_append_string .= ', test-ipallowlist'; + } + $labels->push("traefik.http.routers.{$http_label}.middlewares={$middleware_append_string}"); } } else { // Set labels for http @@ -570,6 +594,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_http_basic_auth_enabled) { $middlewares->push($http_basic_auth_label); } + if (isListStringFilled($test_ipallowlist)) { + $middlewares->push('test-ipallowlist'); + } $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { $middlewares->push($middleware_name); }); @@ -643,6 +670,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, + test_ipallowlist: $application->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -673,6 +701,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, + test_ipallowlist: $application->test_ipallowlist, )); $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, @@ -709,6 +738,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, + test_ipallowlist: $application->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -737,6 +767,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled, http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, + test_ipallowlist: $application->test_ipallowlist, )); $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, @@ -935,6 +966,21 @@ function isDatabaseImageWithContext(string $imageName, array $serviceConfig): bo return true; } +function isListStringFilled(string $list) +{ + $items = explode(',', $list); + if (! empty($items)) { + $sane_list = []; + foreach ($items as $item) { + if (strlen($item) > 0) { + return true; + } + } + } + + return false; +} + function convertDockerRunToCompose(?string $custom_docker_run_options = null) { $options = []; diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index e7d875777..86f1c8ce1 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -1236,7 +1236,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), service_name: $serviceName, - image: $image + image: $image, + test_ipallowlist: $originalResource->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -1263,7 +1264,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), service_name: $serviceName, - image: $image + image: $image, + test_ipallowlist: $originalResource->test_ipallowlist, )); $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( network: $network, @@ -2318,7 +2320,8 @@ function serviceParser(Service $resource): Collection is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), service_name: $serviceName, - image: $image + image: $image, + test_ipallowlist: $originalResource->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -2345,7 +2348,8 @@ function serviceParser(Service $resource): Collection is_gzip_enabled: $originalResource->isGzipEnabled(), is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), service_name: $serviceName, - image: $image + image: $image, + test_ipallowlist: $originalResource->test_ipallowlist, )); $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( network: $network, diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1066f1a63..b42b85a12 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1883,7 +1883,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_gzip_enabled: $savedService->isGzipEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(), service_name: $serviceName, - image: data_get($service, 'image') + image: data_get($service, 'image'), + test_ipallowlist: $savedService->test_ipallowlist, )); break; case ProxyTypes::CADDY->value: @@ -1909,7 +1910,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_gzip_enabled: $savedService->isGzipEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(), service_name: $serviceName, - image: data_get($service, 'image') + image: data_get($service, 'image'), + test_ipallowlist: $savedService->test_ipallowlist, )); $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( network: $resource->destination->network, @@ -2659,6 +2661,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_force_https_enabled: $resource->isForceHttpsEnabled(), is_gzip_enabled: $resource->isGzipEnabled(), is_stripprefix_enabled: $resource->isStripprefixEnabled(), + test_ipallowlist: $resource->test_ipallowlist, ) ); break; @@ -2688,6 +2691,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_force_https_enabled: $resource->isForceHttpsEnabled(), is_gzip_enabled: $resource->isGzipEnabled(), is_stripprefix_enabled: $resource->isStripprefixEnabled(), + test_ipallowlist: $resource->test_ipallowlist, ) ); $serviceLabels = $serviceLabels->merge( diff --git a/database/migrations/2025_12_18_095331_add_test_ipallowlist_to_applications_table.php b/database/migrations/2025_12_18_095331_add_test_ipallowlist_to_applications_table.php new file mode 100644 index 000000000..42fd47652 --- /dev/null +++ b/database/migrations/2025_12_18_095331_add_test_ipallowlist_to_applications_table.php @@ -0,0 +1,28 @@ +text('test_ipallowlist')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('test_ipallowlist'); + }); + } +}; diff --git a/openapi.json b/openapi.json index dd3c6783a..99d620581 100644 --- a/openapi.json +++ b/openapi.json @@ -9506,6 +9506,11 @@ "type": "string", "nullable": true, "description": "Password for HTTP Basic Authentication" + }, + "test_ipallowlist": { + "type": "string", + "nullable": true, + "description": "List of allowed ips" } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index 754b7ec6f..9e3a4c419 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -6099,6 +6099,10 @@ components: type: string nullable: true description: 'Password for HTTP Basic Authentication' + test_ipallowlist: + type: string + nullable: true + description: 'List of allowed ips' type: object ApplicationDeploymentQueue: description: 'Project model' diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index d1a331d1a..1d06a81ae 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -456,6 +456,11 @@ wire:model="customNetworkAliases" x-bind:disabled="!canUpdate" /> @endif +