Add ip allow list for traefik in network configuration

This commit is contained in:
= 2025-12-18 14:38:18 +01:00
parent a528f4c3d1
commit 8cf956b4d4
10 changed files with 203 additions and 8 deletions

View file

@ -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;

View file

@ -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'],
]
)]

View file

@ -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 = [];

View file

@ -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,

View file

@ -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(

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->text('test_ipallowlist')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('test_ipallowlist');
});
}
};

View file

@ -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"

View file

@ -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'

View file

@ -456,6 +456,11 @@
wire:model="customNetworkAliases" x-bind:disabled="!canUpdate" />
@endif
</div>
<div class="flex xl:flex-row">
<x-forms.input id="testIpallowlist" label="Network allow list (Traefik only)"
helper="A comma separated list of allowed ips you would like to be able to access the Docker service.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>192.168.1.100, 192.168.1.200"
wire:model="testIpallowlist" x-bind:disabled="!canUpdate" />
</div>
<h3 class="pt-8">HTTP Basic Authentication</h3>
<div>

93
t_ipallowlist Normal file
View file

@ -0,0 +1,93 @@
= App\Models\Application {#6725
id: 1,
repository_project_id: 603035348,
uuid: "d1k1vmq6p5v0wc9022146b1z",
name: "Docker Compose Example",
fqdn: null,
config_hash: null,
git_repository: "coollabsio/coolify-examples",
git_branch: "v4.x",
git_commit_sha: "HEAD",
git_full_url: null,
docker_registry_image_name: null,
docker_registry_image_tag: null,
build_pack: "dockercompose",
static_image: "nginx:alpine",
install_command: null,
build_command: null,
start_command: null,
ports_exposes: "80",
ports_mappings: null,
base_directory: "/docker-compose",
publish_directory: null,
health_check_path: "/",
health_check_port: null,
health_check_host: "localhost",
health_check_method: "GET",
health_check_return_code: 200,
health_check_scheme: "http",
health_check_response_text: null,
health_check_interval: 5,
health_check_timeout: 5,
health_check_retries: 10,
health_check_start_period: 5,
limits_memory: "0",
limits_memory_swap: "0",
limits_memory_swappiness: 60,
limits_memory_reservation: "0",
limits_cpus: "0",
limits_cpuset: null,
limits_cpu_shares: 1024,
status: "exited",
preview_url_template: "{{pr_id}}.{{domain}}",
destination_type: "App\Models\StandaloneDocker",
destination_id: 0,
source_type: "App\Models\GithubApp",
source_id: 1,
private_key_id: null,
environment_id: 1,
created_at: "2025-12-15 15:42:03",
updated_at: "2025-12-15 15:42:03",
description: null,
dockerfile: null,
health_check_enabled: false,
dockerfile_location: null,
custom_labels: null,
dockerfile_target_build: null,
manual_webhook_secret_github: null,
manual_webhook_secret_gitlab: null,
docker_compose_location: "/docker-compose-test.yaml",
docker_compose: null,
docker_compose_raw: null,
docker_compose_domains: null,
deleted_at: null,
docker_compose_custom_start_command: null,
docker_compose_custom_build_command: null,
swarm_replicas: 1,
swarm_placement_constraints: null,
manual_webhook_secret_bitbucket: null,
custom_docker_run_options: null,
post_deployment_command: null,
post_deployment_command_container: null,
pre_deployment_command: null,
pre_deployment_command_container: null,
watch_paths: null,
custom_healthcheck_found: false,
manual_webhook_secret_gitea: null,
redirect: "both",
compose_parsing_version: "5",
last_online_at: "2025-12-15 15:36:57",
custom_nginx_configuration: null,
custom_network_aliases: null,
is_http_basic_auth_enabled: false,
http_basic_auth_username: null,
http_basic_auth_password: null,
restart_count: 0,
last_restart_at: null,
last_restart_type: null,
test_ipallowlist: null,
additional_servers_count: 0,
additional_networks_count: 0,
+server_status: true,
}