diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index dffe1ec67..cbe150e09 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 40e41c2a7..fbf817d43 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 a0f810480..55e9baeaa 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;
@@ -405,7 +406,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');
@@ -416,6 +417,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]);
}
@@ -424,6 +426,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[] = trim($_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) {
@@ -522,6 +539,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);
});
@@ -567,7 +587,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
@@ -600,6 +624,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);
});
@@ -612,6 +639,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
+ if (isListStringFilled($test_ipallowlist)) {
+ $middlewares->push('test-ipallowlist');
+ }
if (str($image)->contains('ghost')) {
$middlewares->push("redir-ghost-{$uuid}");
}
@@ -673,6 +703,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:
@@ -703,6 +734,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,
@@ -739,6 +771,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:
@@ -767,6 +800,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,
@@ -981,6 +1015,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 43ba58e59..d6bc4fcb8 100644
--- a/bootstrap/helpers/parsers.php
+++ b/bootstrap/helpers/parsers.php
@@ -1238,7 +1238,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:
@@ -1265,7 +1266,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,
@@ -2334,7 +2336,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:
@@ -2361,7 +2364,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 9fc1e6f1c..661f05fbe 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -1908,7 +1908,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:
@@ -1934,7 +1935,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,
@@ -2684,6 +2686,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;
@@ -2713,6 +2716,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 fe8ca863e..f60ef9f7a 100644
--- a/openapi.json
+++ b/openapi.json
@@ -10392,6 +10392,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 a7faa8c72..b1a7c2f0c 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -6589,6 +6589,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 cf9621462..fe2ec21ab 100644
--- a/resources/views/livewire/project/application/general.blade.php
+++ b/resources/views/livewire/project/application/general.blade.php
@@ -509,6 +509,12 @@
helper="A comma separated list of custom network aliases you would like to add for container in Docker network.
Example:
api.internal,api.local"
wire:model="customNetworkAliases" x-bind:disabled="!canUpdate" />
@endif
+ @if(!application->destination->server->isSwarm())
+