feat(proxy): add hashing options for HTTP basic authentication

This commit is contained in:
peaklabs-dev
2026-10-04 19:57:48 +02:00
parent 16a8c7999d
commit 174e0131f7
18 changed files with 778 additions and 11 deletions
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum HttpBasicAuthHashAlgorithm: string
{
case BCRYPT = 'bcrypt';
case ARGON2ID = 'argon2id';
}
@@ -6,6 +6,7 @@ use App\Actions\Application\CleanupPreviewDeployment;
use App\Actions\Application\LoadComposeFile;
use App\Actions\Application\StopApplication;
use App\Enums\BuildPackTypes;
use App\Enums\HttpBasicAuthHashAlgorithm;
use App\Http\Controllers\Controller;
use App\Jobs\DeleteResourceJob;
use App\Models\Application;
@@ -425,6 +426,10 @@ class ApplicationsController extends Controller
'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'],
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'default' => 'bcrypt', 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'default' => 10, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'default' => 65536, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
@@ -620,6 +625,10 @@ class ApplicationsController extends Controller
'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'],
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'default' => 'bcrypt', 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'default' => 10, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'default' => 65536, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
@@ -815,6 +824,10 @@ class ApplicationsController extends Controller
'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'],
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'default' => 'bcrypt', 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'default' => 10, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'default' => 65536, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
@@ -981,6 +994,10 @@ class ApplicationsController extends Controller
'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'],
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'default' => 'bcrypt', 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'default' => 10, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'default' => 65536, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
@@ -1143,6 +1160,10 @@ class ApplicationsController extends Controller
'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'],
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'default' => 'bcrypt', 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'default' => 10, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'default' => 65536, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
@@ -1228,7 +1249,7 @@ class ApplicationsController extends Controller
if ($return instanceof JsonResponse) {
return $return;
}
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'noindex_domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'use_build_secrets', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'noindex_domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'use_build_secrets', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'http_basic_auth_hash_algorithm', 'http_basic_auth_bcrypt_cost', 'http_basic_auth_argon2id_memory_cost', 'http_basic_auth_argon2id_time_cost', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@@ -1240,7 +1261,9 @@ class ApplicationsController extends Controller
'destination_uuid' => 'string',
'is_http_basic_auth_enabled' => 'boolean',
'http_basic_auth_username' => 'string|nullable',
'http_basic_auth_password' => 'string|nullable',
// bcrypt ignores everything after 72 bytes. Labels for Traefik always use bcrypt and are also generated on
// Caddy servers when "Labels for all supported proxies" is selected, so that the proxy can be switched easily.
'http_basic_auth_password' => 'string|nullable|max:72',
'autogenerate_domain' => 'boolean',
'tags' => 'array|nullable',
'tags.*' => 'string|min:2',
@@ -3106,6 +3129,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'use_build_secrets' => ['type' => 'boolean', 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
@@ -3226,7 +3253,7 @@ class ApplicationsController extends Controller
$dnsCleanup = app(ManagedDnsRecordCleanup::class);
$previousDnsHostnames = $dnsCleanup->hostnamesOf($application);
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'noindex_domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'use_build_secrets', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'noindex_domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'use_build_secrets', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'http_basic_auth_hash_algorithm', 'http_basic_auth_bcrypt_cost', 'http_basic_auth_argon2id_memory_cost', 'http_basic_auth_argon2id_time_cost', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
$validationRules = [
'name' => 'string|max:255',
@@ -3241,7 +3268,9 @@ class ApplicationsController extends Controller
'is_http_basic_auth_enabled' => 'boolean|nullable',
'is_preview_deployments_enabled' => 'boolean|nullable',
'http_basic_auth_username' => 'string',
'http_basic_auth_password' => 'string',
// bcrypt ignores everything after 72 bytes. Labels for Traefik always use bcrypt and are also generated on
// Caddy servers when "Labels for all supported proxies" is selected, so that the proxy can be switched easily.
'http_basic_auth_password' => 'string|max:72',
'include_source_commit_in_build' => 'boolean',
'ports_exposes' => 'nullable|string|regex:/^(\d+)(,\d+)*$/',
];
@@ -3322,7 +3351,8 @@ class ApplicationsController extends Controller
$requestHasHttpBasicAuth = $request->has('is_http_basic_auth_enabled')
|| $request->has('http_basic_auth_username')
|| $request->has('http_basic_auth_password');
|| $request->has('http_basic_auth_password')
|| $request->hasAny(['http_basic_auth_hash_algorithm', 'http_basic_auth_bcrypt_cost', 'http_basic_auth_argon2id_memory_cost', 'http_basic_auth_argon2id_time_cost']);
if ($request->has('is_http_basic_auth_enabled') && $request->is_http_basic_auth_enabled === true) {
if (blank($application->http_basic_auth_username) || blank($application->http_basic_auth_password)) {
@@ -4867,6 +4897,15 @@ class ApplicationsController extends Controller
{
$teamId = getTeamIdFromToken();
if ($request->input('http_basic_auth_hash_algorithm') === HttpBasicAuthHashAlgorithm::ARGON2ID->value && ! $server->caddySupportsArgon2idBasicAuth()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'http_basic_auth_hash_algorithm' => 'Argon2id needs a server that runs the Caddy proxy, version 2.11 or newer.',
],
], 422);
}
// Validate ports_mappings
if ($request->has('ports_mappings')) {
$ports = [];
+28 -2
View File
@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Application;
use App\Enums\HttpBasicAuthHashAlgorithm;
use App\Enums\StaticImageTypes;
use App\Jobs\ApplicationDeploymentJob;
use App\Livewire\Project\Service\Storage;
@@ -118,6 +119,14 @@ class General extends Component
public ?string $httpBasicAuthPassword = null;
public string $httpBasicAuthHashAlgorithm = HttpBasicAuthHashAlgorithm::BCRYPT->value;
public int $httpBasicAuthBcryptCost = 10;
public int $httpBasicAuthArgon2idMemoryCost = 65536;
public int $httpBasicAuthArgon2idTimeCost = 4;
public ?string $watchPaths = null;
public string $redirect;
@@ -193,7 +202,13 @@ class General extends Component
'isPreserveRepositoryEnabled' => 'boolean|required',
'isHttpBasicAuthEnabled' => 'boolean|required',
'httpBasicAuthUsername' => 'string|nullable',
'httpBasicAuthPassword' => 'string|nullable',
// bcrypt ignores everything after 72 bytes. Labels for Traefik always use bcrypt and are also generated on
// Caddy servers when "Labels for all supported proxies" is selected, so that the proxy can be switched easily.
'httpBasicAuthPassword' => 'string|nullable|max:72',
'httpBasicAuthHashAlgorithm' => ['required', Rule::enum(HttpBasicAuthHashAlgorithm::class)],
'httpBasicAuthBcryptCost' => 'required|integer|min:4|max:14',
'httpBasicAuthArgon2idMemoryCost' => 'required|integer|min:8192|max:262144',
'httpBasicAuthArgon2idTimeCost' => 'required|integer|min:1|max:12',
'watchPaths' => 'nullable',
'redirect' => 'string|required',
];
@@ -367,6 +382,9 @@ class General extends Component
if ($toModel) {
$this->validate();
$this->validateChangedDockerComposeRaw();
if ($this->httpBasicAuthHashAlgorithm === HttpBasicAuthHashAlgorithm::ARGON2ID->value && ! $this->application->destination->server->caddySupportsArgon2idBasicAuth()) {
throw new Exception('Argon2id needs a server that runs the Caddy proxy, version 2.11 or newer.');
}
// Application properties
$this->application->name = $this->name;
@@ -407,6 +425,10 @@ 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->http_basic_auth_hash_algorithm = $this->httpBasicAuthHashAlgorithm;
$this->application->http_basic_auth_bcrypt_cost = $this->httpBasicAuthBcryptCost;
$this->application->http_basic_auth_argon2id_memory_cost = $this->httpBasicAuthArgon2idMemoryCost;
$this->application->http_basic_auth_argon2id_time_cost = $this->httpBasicAuthArgon2idTimeCost;
$this->application->watch_paths = $this->watchPaths;
$this->application->redirect = $this->redirect;
@@ -460,6 +482,10 @@ class General extends Component
$this->httpBasicAuthPassword = auth()->user()->can('update', $this->application)
? $this->application->http_basic_auth_password
: null;
$this->httpBasicAuthHashAlgorithm = ($this->application->usesArgon2idBasicAuth() ? HttpBasicAuthHashAlgorithm::ARGON2ID : HttpBasicAuthHashAlgorithm::BCRYPT)->value;
$this->httpBasicAuthBcryptCost = $this->application->http_basic_auth_bcrypt_cost;
$this->httpBasicAuthArgon2idMemoryCost = $this->application->http_basic_auth_argon2id_memory_cost;
$this->httpBasicAuthArgon2idTimeCost = $this->application->http_basic_auth_argon2id_time_cost;
$this->watchPaths = $this->application->watch_paths;
$this->redirect = $this->application->redirect;
@@ -497,7 +523,7 @@ class General extends Component
if ($oldIsSpa !== $this->isSpa) {
$this->generateNginxConfiguration($this->isSpa ? 'spa' : 'static');
}
if ($oldIsHttpBasicAuthEnabled !== $this->isHttpBasicAuthEnabled) {
if ($oldIsHttpBasicAuthEnabled !== $this->isHttpBasicAuthEnabled || $this->application->isDirty(['http_basic_auth_hash_algorithm', 'http_basic_auth_bcrypt_cost', 'http_basic_auth_argon2id_memory_cost', 'http_basic_auth_argon2id_time_cost'])) {
$this->application->save();
}
+16
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\BuildPackTypes;
use App\Enums\HttpBasicAuthHashAlgorithm;
use App\Enums\ProxyTypes;
use App\Exceptions\DeploymentException;
use App\Services\ConfigurationGenerator;
@@ -125,6 +126,10 @@ use Symfony\Component\Yaml\Yaml;
'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'],
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
new OA\Property(property: 'settings', ref: '#/components/schemas/ApplicationSetting'),
]
)]
@@ -220,6 +225,10 @@ class Application extends BaseModel
'is_http_basic_auth_enabled',
'http_basic_auth_username',
'http_basic_auth_password',
'http_basic_auth_hash_algorithm',
'http_basic_auth_bcrypt_cost',
'http_basic_auth_argon2id_memory_cost',
'http_basic_auth_argon2id_time_cost',
'connect_to_docker_network',
'force_domain_override',
'is_container_label_escape_enabled',
@@ -268,6 +277,7 @@ class Application extends BaseModel
{
return [
'http_basic_auth_password' => 'encrypted',
'http_basic_auth_hash_algorithm' => HttpBasicAuthHashAlgorithm::class,
'manual_webhook_secret_github' => 'encrypted',
'manual_webhook_secret_gitlab' => 'encrypted',
'manual_webhook_secret_bitbucket' => 'encrypted',
@@ -657,6 +667,12 @@ class Application extends BaseModel
return $this->source instanceof GithubApp;
}
public function usesArgon2idBasicAuth(): bool
{
return $this->http_basic_auth_hash_algorithm === HttpBasicAuthHashAlgorithm::ARGON2ID
&& $this->destination->server->caddySupportsArgon2idBasicAuth();
}
public function isForceHttpsEnabled()
{
return data_get($this, 'settings.is_force_https_enabled', false);
+6
View File
@@ -1244,6 +1244,12 @@ $siteAddress {
return $this->caddyRunsCurrentVersion();
}
public function caddySupportsArgon2idBasicAuth(): bool
{
return $this->caddyRunsCurrentVersion()
&& self::caddyDockerProxyImageVersion($this->configuredCaddyProxyImage()) >= [2, 11];
}
public function isServerApiEnabled(): bool
{
return $this->settings->is_sentinel_enabled;
@@ -208,6 +208,10 @@ class ApplicationConfigurationSnapshot
$this->item('is_http_basic_auth_enabled', 'HTTP basic auth', $this->application->is_http_basic_auth_enabled, 'redeploy'),
$this->item('http_basic_auth_username', 'HTTP basic auth username', $this->application->http_basic_auth_username, 'redeploy'),
$this->item('http_basic_auth_password', 'HTTP basic auth password', $this->application->http_basic_auth_password, 'redeploy', sensitive: true),
$this->item('http_basic_auth_hash_algorithm', 'HTTP basic auth hash algorithm', $this->application->http_basic_auth_hash_algorithm?->value, 'redeploy'),
$this->item('http_basic_auth_bcrypt_cost', 'HTTP basic auth bcrypt cost factor', $this->application->http_basic_auth_bcrypt_cost, 'redeploy'),
$this->item('http_basic_auth_argon2id_memory_cost', 'HTTP basic auth Argon2id memory (KiB)', $this->application->http_basic_auth_argon2id_memory_cost, 'redeploy'),
$this->item('http_basic_auth_argon2id_time_cost', 'HTTP basic auth Argon2id iterations', $this->application->http_basic_auth_argon2id_time_cost, 'redeploy'),
];
}
+5
View File
@@ -2,6 +2,7 @@
use App\Actions\Shared\MigrateResourceToDestination;
use App\Enums\BuildPackTypes;
use App\Enums\HttpBasicAuthHashAlgorithm;
use App\Enums\RedirectTypes;
use App\Enums\StaticImageTypes;
use App\Models\Application;
@@ -276,6 +277,10 @@ function sharedDataApplications(?Application $application = null): array
'docker_compose_custom_build_command' => ValidationPatterns::shellSafeCommandRules(),
'is_container_label_escape_enabled' => 'boolean',
'is_preserve_repository_enabled' => 'boolean',
'http_basic_auth_hash_algorithm' => Rule::enum(HttpBasicAuthHashAlgorithm::class),
'http_basic_auth_bcrypt_cost' => 'integer|min:4|max:14',
'http_basic_auth_argon2id_memory_cost' => 'integer|min:8192|max:262144',
'http_basic_auth_argon2id_time_cost' => 'integer|min:1|max:12',
];
}
+25 -4
View File
@@ -766,7 +766,7 @@ function isNoindexDomain(string $domain, ?Collection $noindex_domains): bool
->contains(ValidationPatterns::normalizeApplicationDomainUrl($domain));
}
function fqdnLabelsForCaddy(string $network, 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, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $is_traffic_analytics_enabled = false, array $domainPortOverrides = [], bool $supports_log_append = false, bool $supports_basic_auth_directive = false)
function fqdnLabelsForCaddy(string $network, 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, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $is_traffic_analytics_enabled = false, array $domainPortOverrides = [], bool $supports_log_append = false, bool $supports_basic_auth_directive = false, ?int $http_basic_auth_bcrypt_cost = null, array $http_basic_auth_argon2id_options = [])
{
$labels = collect([]);
if ($serviceLabels) {
@@ -777,7 +777,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null;
if ($is_http_basic_auth_enabled) {
$hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]);
$hashedPassword = $http_basic_auth_argon2id_options
? password_hash($http_basic_auth_password, PASSWORD_ARGON2ID, $http_basic_auth_argon2id_options)
: password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => $http_basic_auth_bcrypt_cost ?? 10]);
}
$trafficAppKey = caddyTrafficAppKey($uuid, $service_name);
@@ -834,6 +836,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
if ($is_http_basic_auth_enabled) {
// Caddy 2.8 renamed basicauth to basic_auth; see Server::caddySupportsBasicAuthDirective().
$basicAuthDirective = $supports_basic_auth_directive ? 'basic_auth' : 'basicauth';
if ($http_basic_auth_argon2id_options) {
$labels->push("caddy_{$loop}.{$basicAuthDirective}=argon2id");
}
$labels->push("caddy_{$loop}.{$basicAuthDirective}.{$http_basic_auth_username}=\"{$hashedPassword}\"");
}
if ($is_traffic_analytics_enabled) {
@@ -907,7 +912,7 @@ function dockerComposeServicePorts(?string $compose, ?string $serviceName): arra
->unique()->values()->all();
}
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, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [])
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, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [], ?int $http_basic_auth_bcrypt_cost = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -919,7 +924,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}";
if ($is_http_basic_auth_enabled) {
$hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]);
$hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => $http_basic_auth_bcrypt_cost ?? 10]);
}
if ($is_http_basic_auth_enabled) {
@@ -1186,6 +1191,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$appUuid = $appUuid.'-pr-'.$pull_request_id;
}
$labels = collect([]);
$httpBasicAuthArgon2idOptions = $application->usesArgon2idBasicAuth() ? [
'memory_cost' => $application->http_basic_auth_argon2id_memory_cost,
'time_cost' => $application->http_basic_auth_argon2id_time_cost,
] : [];
if ($pull_request_id === 0) {
if ($application->fqdn) {
$domains = str(data_get($application, 'fqdn'))->explode(',');
@@ -1205,6 +1214,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,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
noindex_domains: $noindexDomains,
domainPortOverrides: $application->domain_port_overrides ?? [],
));
@@ -1222,6 +1232,8 @@ 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,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
http_basic_auth_argon2id_options: $httpBasicAuthArgon2idOptions,
noindex_domains: $noindexDomains,
is_traffic_analytics_enabled: $application->destination->server->isTrafficAnalyticsEnabled(),
supports_log_append: $application->destination->server->caddySupportsLogAppend(),
@@ -1242,6 +1254,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,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $application->domain_port_overrides ?? [],
@@ -1258,6 +1271,8 @@ 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,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
http_basic_auth_argon2id_options: $httpBasicAuthArgon2idOptions,
noindex_domains: $noindexDomains,
is_traffic_analytics_enabled: $application->destination->server->isTrafficAnalyticsEnabled(),
supports_log_append: $application->destination->server->caddySupportsLogAppend(),
@@ -1288,6 +1303,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,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $preview->domain_port_overrides ?? [],
@@ -1305,6 +1321,8 @@ 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,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
http_basic_auth_argon2id_options: $httpBasicAuthArgon2idOptions,
noindex_domains: $noindexDomains,
is_traffic_analytics_enabled: $application->destination->server->isTrafficAnalyticsEnabled(),
supports_log_append: $application->destination->server->caddySupportsLogAppend(),
@@ -1324,6 +1342,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,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $preview->domain_port_overrides ?? [],
@@ -1339,6 +1358,8 @@ 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,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
http_basic_auth_argon2id_options: $httpBasicAuthArgon2idOptions,
noindex_domains: $noindexDomains,
is_traffic_analytics_enabled: $application->destination->server->isTrafficAnalyticsEnabled(),
supports_log_append: $application->destination->server->caddySupportsLogAppend(),
@@ -21,6 +21,9 @@ class ApplicationFactory extends Factory
'ports_exposes' => '3000',
'environment_id' => 1,
'destination_id' => 1,
'http_basic_auth_bcrypt_cost' => 10,
'http_basic_auth_argon2id_memory_cost' => 65536,
'http_basic_auth_argon2id_time_cost' => 4,
];
}
}
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('http_basic_auth_hash_algorithm')->default('bcrypt');
$table->unsignedTinyInteger('http_basic_auth_bcrypt_cost')->default(10);
$table->unsignedInteger('http_basic_auth_argon2id_memory_cost')->default(65536);
$table->unsignedTinyInteger('http_basic_auth_argon2id_time_cost')->default(4);
});
}
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn([
'http_basic_auth_hash_algorithm',
'http_basic_auth_bcrypt_cost',
'http_basic_auth_argon2id_memory_cost',
'http_basic_auth_argon2id_time_cost',
]);
});
}
};
+202
View File
@@ -979,6 +979,36 @@
"nullable": true,
"description": "Password for HTTP Basic Authentication"
},
"http_basic_auth_hash_algorithm": {
"type": "string",
"enum": [
"bcrypt",
"argon2id"
],
"default": "bcrypt",
"description": "Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer."
},
"http_basic_auth_bcrypt_cost": {
"type": "integer",
"minimum": 4,
"maximum": 14,
"default": 10,
"description": "Bcrypt cost for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_memory_cost": {
"type": "integer",
"minimum": 8192,
"maximum": 262144,
"default": 65536,
"description": "Argon2id memory cost in KiB for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_time_cost": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"default": 4,
"description": "Argon2id time cost (iterations) for the HTTP Basic Authentication password."
},
"connect_to_docker_network": {
"type": "boolean",
"description": "The flag to connect the service to the predefined Docker network."
@@ -1570,6 +1600,36 @@
"nullable": true,
"description": "Password for HTTP Basic Authentication"
},
"http_basic_auth_hash_algorithm": {
"type": "string",
"enum": [
"bcrypt",
"argon2id"
],
"default": "bcrypt",
"description": "Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer."
},
"http_basic_auth_bcrypt_cost": {
"type": "integer",
"minimum": 4,
"maximum": 14,
"default": 10,
"description": "Bcrypt cost for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_memory_cost": {
"type": "integer",
"minimum": 8192,
"maximum": 262144,
"default": 65536,
"description": "Argon2id memory cost in KiB for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_time_cost": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"default": 4,
"description": "Argon2id time cost (iterations) for the HTTP Basic Authentication password."
},
"connect_to_docker_network": {
"type": "boolean",
"description": "The flag to connect the service to the predefined Docker network."
@@ -2161,6 +2221,36 @@
"nullable": true,
"description": "Password for HTTP Basic Authentication"
},
"http_basic_auth_hash_algorithm": {
"type": "string",
"enum": [
"bcrypt",
"argon2id"
],
"default": "bcrypt",
"description": "Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer."
},
"http_basic_auth_bcrypt_cost": {
"type": "integer",
"minimum": 4,
"maximum": 14,
"default": 10,
"description": "Bcrypt cost for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_memory_cost": {
"type": "integer",
"minimum": 8192,
"maximum": 262144,
"default": 65536,
"description": "Argon2id memory cost in KiB for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_time_cost": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"default": 4,
"description": "Argon2id time cost (iterations) for the HTTP Basic Authentication password."
},
"connect_to_docker_network": {
"type": "boolean",
"description": "The flag to connect the service to the predefined Docker network."
@@ -2647,6 +2737,36 @@
"nullable": true,
"description": "Password for HTTP Basic Authentication"
},
"http_basic_auth_hash_algorithm": {
"type": "string",
"enum": [
"bcrypt",
"argon2id"
],
"default": "bcrypt",
"description": "Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer."
},
"http_basic_auth_bcrypt_cost": {
"type": "integer",
"minimum": 4,
"maximum": 14,
"default": 10,
"description": "Bcrypt cost for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_memory_cost": {
"type": "integer",
"minimum": 8192,
"maximum": 262144,
"default": 65536,
"description": "Argon2id memory cost in KiB for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_time_cost": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"default": 4,
"description": "Argon2id time cost (iterations) for the HTTP Basic Authentication password."
},
"connect_to_docker_network": {
"type": "boolean",
"description": "The flag to connect the service to the predefined Docker network."
@@ -3113,6 +3233,36 @@
"nullable": true,
"description": "Password for HTTP Basic Authentication"
},
"http_basic_auth_hash_algorithm": {
"type": "string",
"enum": [
"bcrypt",
"argon2id"
],
"default": "bcrypt",
"description": "Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer."
},
"http_basic_auth_bcrypt_cost": {
"type": "integer",
"minimum": 4,
"maximum": 14,
"default": 10,
"description": "Bcrypt cost for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_memory_cost": {
"type": "integer",
"minimum": 8192,
"maximum": 262144,
"default": 65536,
"description": "Argon2id memory cost in KiB for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_time_cost": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"default": 4,
"description": "Argon2id time cost (iterations) for the HTTP Basic Authentication password."
},
"connect_to_docker_network": {
"type": "boolean",
"description": "The flag to connect the service to the predefined Docker network."
@@ -3702,6 +3852,32 @@
"type": "boolean",
"description": "Use Docker Build Secrets for build-time environment variables."
},
"http_basic_auth_hash_algorithm": {
"type": "string",
"enum": [
"bcrypt",
"argon2id"
],
"description": "Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer."
},
"http_basic_auth_bcrypt_cost": {
"type": "integer",
"minimum": 4,
"maximum": 14,
"description": "Bcrypt cost for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_memory_cost": {
"type": "integer",
"minimum": 8192,
"maximum": 262144,
"description": "Argon2id memory cost in KiB for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_time_cost": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"description": "Argon2id time cost (iterations) for the HTTP Basic Authentication password."
},
"is_git_submodules_enabled": {
"type": "boolean",
"description": "Clone Git submodules."
@@ -23504,6 +23680,32 @@
"nullable": true,
"description": "Password for HTTP Basic Authentication"
},
"http_basic_auth_hash_algorithm": {
"type": "string",
"enum": [
"bcrypt",
"argon2id"
],
"description": "Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer."
},
"http_basic_auth_bcrypt_cost": {
"type": "integer",
"minimum": 4,
"maximum": 14,
"description": "Bcrypt cost for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_memory_cost": {
"type": "integer",
"minimum": 8192,
"maximum": 262144,
"description": "Argon2id memory cost in KiB for the HTTP Basic Authentication password."
},
"http_basic_auth_argon2id_time_cost": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"description": "Argon2id time cost (iterations) for the HTTP Basic Authentication password."
},
"settings": {
"$ref": "#\/components\/schemas\/ApplicationSetting"
}
+155
View File
@@ -645,6 +645,29 @@ paths:
type: string
nullable: true
description: 'Password for HTTP Basic Authentication'
http_basic_auth_hash_algorithm:
type: string
enum: [bcrypt, argon2id]
default: bcrypt
description: 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'
http_basic_auth_bcrypt_cost:
type: integer
minimum: 4
maximum: 14
default: 10
description: 'Bcrypt cost for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_memory_cost:
type: integer
minimum: 8192
maximum: 262144
default: 65536
description: 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_time_cost:
type: integer
minimum: 1
maximum: 12
default: 4
description: 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'
connect_to_docker_network:
type: boolean
description: 'The flag to connect the service to the predefined Docker network.'
@@ -1031,6 +1054,29 @@ paths:
type: string
nullable: true
description: 'Password for HTTP Basic Authentication'
http_basic_auth_hash_algorithm:
type: string
enum: [bcrypt, argon2id]
default: bcrypt
description: 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'
http_basic_auth_bcrypt_cost:
type: integer
minimum: 4
maximum: 14
default: 10
description: 'Bcrypt cost for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_memory_cost:
type: integer
minimum: 8192
maximum: 262144
default: 65536
description: 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_time_cost:
type: integer
minimum: 1
maximum: 12
default: 4
description: 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'
connect_to_docker_network:
type: boolean
description: 'The flag to connect the service to the predefined Docker network.'
@@ -1417,6 +1463,29 @@ paths:
type: string
nullable: true
description: 'Password for HTTP Basic Authentication'
http_basic_auth_hash_algorithm:
type: string
enum: [bcrypt, argon2id]
default: bcrypt
description: 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'
http_basic_auth_bcrypt_cost:
type: integer
minimum: 4
maximum: 14
default: 10
description: 'Bcrypt cost for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_memory_cost:
type: integer
minimum: 8192
maximum: 262144
default: 65536
description: 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_time_cost:
type: integer
minimum: 1
maximum: 12
default: 4
description: 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'
connect_to_docker_network:
type: boolean
description: 'The flag to connect the service to the predefined Docker network.'
@@ -1744,6 +1813,29 @@ paths:
type: string
nullable: true
description: 'Password for HTTP Basic Authentication'
http_basic_auth_hash_algorithm:
type: string
enum: [bcrypt, argon2id]
default: bcrypt
description: 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'
http_basic_auth_bcrypt_cost:
type: integer
minimum: 4
maximum: 14
default: 10
description: 'Bcrypt cost for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_memory_cost:
type: integer
minimum: 8192
maximum: 262144
default: 65536
description: 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_time_cost:
type: integer
minimum: 1
maximum: 12
default: 4
description: 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'
connect_to_docker_network:
type: boolean
description: 'The flag to connect the service to the predefined Docker network.'
@@ -2057,6 +2149,29 @@ paths:
type: string
nullable: true
description: 'Password for HTTP Basic Authentication'
http_basic_auth_hash_algorithm:
type: string
enum: [bcrypt, argon2id]
default: bcrypt
description: 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'
http_basic_auth_bcrypt_cost:
type: integer
minimum: 4
maximum: 14
default: 10
description: 'Bcrypt cost for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_memory_cost:
type: integer
minimum: 8192
maximum: 262144
default: 65536
description: 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_time_cost:
type: integer
minimum: 1
maximum: 12
default: 4
description: 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'
connect_to_docker_network:
type: boolean
description: 'The flag to connect the service to the predefined Docker network.'
@@ -2429,6 +2544,25 @@ paths:
use_build_secrets:
type: boolean
description: 'Use Docker Build Secrets for build-time environment variables.'
http_basic_auth_hash_algorithm:
type: string
enum: [bcrypt, argon2id]
description: 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'
http_basic_auth_bcrypt_cost:
type: integer
minimum: 4
maximum: 14
description: 'Bcrypt cost for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_memory_cost:
type: integer
minimum: 8192
maximum: 262144
description: 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_time_cost:
type: integer
minimum: 1
maximum: 12
description: 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'
is_git_submodules_enabled:
type: boolean
description: 'Clone Git submodules.'
@@ -15009,6 +15143,27 @@ components:
type: string
nullable: true
description: 'Password for HTTP Basic Authentication'
http_basic_auth_hash_algorithm:
type: string
enum:
- bcrypt
- argon2id
description: 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'
http_basic_auth_bcrypt_cost:
type: integer
minimum: 4
maximum: 14
description: 'Bcrypt cost for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_memory_cost:
type: integer
minimum: 8192
maximum: 262144
description: 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'
http_basic_auth_argon2id_time_cost:
type: integer
minimum: 1
maximum: 12
description: 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'
settings:
$ref: '#/components/schemas/ApplicationSetting'
type: object
@@ -1,3 +1,4 @@
@php use App\Enums\HttpBasicAuthHashAlgorithm; use App\Enums\ProxyTypes; @endphp
<div x-data="{
initLoadingCompose: $wire.entangle('initLoadingCompose'),
canUpdate: @js(auth()->user()->can('update', $application)),
@@ -572,6 +573,44 @@
<x-forms.input id="httpBasicAuthPassword" type="password" label="Password" required
x-bind:disabled="!canUpdate" />
</div>
@php
$server = $application->destination->server;
$supportsArgon2id = $server->caddySupportsArgon2idBasicAuth();
$usesArgon2id = $httpBasicAuthHashAlgorithm === HttpBasicAuthHashAlgorithm::ARGON2ID->value;
@endphp
<div class="mt-5 border-t border-neutral-200 pt-5 dark:border-white/[0.07]">
<h3 class="mb-3 text-sm font-semibold text-black dark:text-fg">Password hashing</h3>
<div class="grid w-full gap-4 {{ $usesArgon2id ? 'sm:grid-cols-3' : 'sm:grid-cols-2' }}">
<x-forms.listbox id="httpBasicAuthHashAlgorithm" label="Algorithm" onChange="instantSave"
helper="Algorithm that hashes the password for the proxy labels.<br><br>Traefik supports only bcrypt. Argon2id needs the Caddy proxy, version 2.11 or newer. With Argon2id, labels generated for Traefik use bcrypt."
:options="[
['value' => HttpBasicAuthHashAlgorithm::BCRYPT->value, 'label' => 'bcrypt', 'description' => 'Works with every proxy'],
['value' => HttpBasicAuthHashAlgorithm::ARGON2ID->value, 'label' => 'Argon2id', 'disabled' => ! $supportsArgon2id, 'description' => match (true) {
$supportsArgon2id => 'Memory-hard, more resistant to cracking',
$server->proxyType() === ProxyTypes::CADDY->value => 'Needs Caddy 2.11 or newer on this server',
default => 'Only supported by the Caddy proxy',
}],
]" x-bind:disabled="!canUpdate" />
@if ($usesArgon2id)
<div wire:key="http-basic-auth-argon2id-memory-cost" class="min-w-0">
<x-forms.listbox id="httpBasicAuthArgon2idMemoryCost" label="Memory" onChange="instantSave"
helper="Memory that the proxy needs to check a password. More memory makes the hash harder to crack, but every login attempt that the proxy has not seen before uses this much memory on the server."
:options="collect([8, 16, 32, 64, 128, 256])->map(fn (int $mebibytes) => ['value' => $mebibytes * 1024, 'label' => $mebibytes.' MiB'])->all()" x-bind:disabled="!canUpdate" />
</div>
<div wire:key="http-basic-auth-argon2id-time-cost" class="min-w-0">
<x-forms.listbox id="httpBasicAuthArgon2idTimeCost" label="Iterations" onChange="instantSave"
helper="Number of passes over the memory. More iterations make the hash harder to crack and a password check slower."
:options="collect(range(1, 12))->map(fn (int $iterations) => ['value' => $iterations, 'label' => (string) $iterations])->all()" x-bind:disabled="!canUpdate" />
</div>
@else
<div wire:key="http-basic-auth-bcrypt-cost" class="min-w-0">
<x-forms.listbox id="httpBasicAuthBcryptCost" label="Cost factor" onChange="instantSave"
helper="Each step doubles the work to check or crack the password. Traefik checks the password on every request, so a higher cost makes every request slower."
:options="collect(range(4, 14))->map(fn (int $cost) => ['value' => $cost, 'label' => (string) $cost])->all()" x-bind:disabled="!canUpdate" />
</div>
@endif
</div>
</div>
@endif
@endif
</x-application.settings-section>
@@ -267,6 +267,35 @@ test('http basic auth updates preserve user-managed labels', function () {
expect(base64_decode($this->application->fresh()->custom_labels))->toBe('sentinel-label=true');
});
test('http basic auth hash settings are saved and applied to managed labels', function () {
$this->application->update([
'fqdn' => 'https://app.example.com',
'is_http_basic_auth_enabled' => true,
'http_basic_auth_username' => 'api-user',
'http_basic_auth_password' => 'api-password',
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", ['http_basic_auth_bcrypt_cost' => 4])
->assertOk();
expect($this->application->fresh()->parseContainerLabels())->toContain('.basicauth.users=api-user:$2y$04$');
});
test('rejects http basic auth settings that are not supported', function (string $field, mixed $value) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [$field => $value])
->assertUnprocessable()
->assertJsonValidationErrors($field);
})->with([
'Argon2id on a proxy without it' => ['http_basic_auth_hash_algorithm', 'argon2id'],
'unknown algorithm' => ['http_basic_auth_hash_algorithm', 'md5'],
'bcrypt cost above 14' => ['http_basic_auth_bcrypt_cost', 15],
'Argon2id memory above 256 MiB' => ['http_basic_auth_argon2id_memory_cost', 262145],
'Argon2id iterations above 12' => ['http_basic_auth_argon2id_time_cost', 13],
'password longer than 72 characters' => ['http_basic_auth_password', str_repeat('a', 73)],
]);
test('rejects invalid boolean application settings', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
@@ -1,5 +1,6 @@
<?php
use App\Enums\ProxyTypes;
use App\Livewire\Project\Application\General;
use App\Models\Application;
use App\Models\Environment;
@@ -16,6 +17,7 @@ use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
Server::flushIdentityMap();
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
@@ -47,9 +49,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
?? StandaloneDocker::factory()->create(['server_id' => $this->server->id, 'network' => 'coolify-test']);
});
afterEach(fn () => Server::flushIdentityMap());
function createApplicationForHttpBasicAuth(array $overrides = []): Application
{
return Application::factory()->create(array_merge([
'name' => 'Basic Auth App',
'environment_id' => test()->environment->id,
'destination_id' => test()->destination->id,
'destination_type' => StandaloneDocker::class,
@@ -112,3 +117,68 @@ test('disabling http basic auth is saved instantly', function () {
expect((bool) $application->is_http_basic_auth_enabled)->toBeFalse();
});
function useCaddyProxyForHttpBasicAuth(string $image): void
{
test()->server->forceFill(['proxy' => [
'type' => ProxyTypes::CADDY->value,
'status' => 'running',
'last_saved_settings' => 'applied',
'last_applied_settings' => 'applied',
'last_saved_proxy_configuration' => "services:\n caddy:\n image: '{$image}'\n",
]])->save();
}
test('changing the bcrypt cost factor is saved instantly and applied to the proxy labels', function () {
$application = createApplicationForHttpBasicAuth(['fqdn' => 'https://app.example.com']);
Livewire::test(General::class, ['application' => $application])
->set('httpBasicAuthBcryptCost', 4)
->call('instantSave')
->assertDispatched('success');
$application->refresh();
expect($application->http_basic_auth_bcrypt_cost)->toBe(4)
->and(base64_decode($application->custom_labels))->toContain('.basicauth.users=admin:$2y$04$');
});
test('selecting Argon2id is saved instantly and applied to the Caddy labels', function () {
useCaddyProxyForHttpBasicAuth('lucaslorentz/caddy-docker-proxy:2.13-alpine');
$application = createApplicationForHttpBasicAuth(['fqdn' => 'https://app.example.com']);
Livewire::test(General::class, ['application' => $application])
->set('httpBasicAuthHashAlgorithm', 'argon2id')
->set('httpBasicAuthArgon2idMemoryCost', 8192)
->call('instantSave')
->assertDispatched('success')
->assertSee('Iterations')
->assertDontSee('Cost factor');
expect(base64_decode($application->refresh()->custom_labels))
->toContain('caddy_0.basic_auth=argon2id', 'caddy_0.basic_auth.admin="$argon2id$v=19$m=8192,t=4,p=1$');
});
test('http basic auth form explains why Argon2id cannot be selected', function (?string $image, string $reason) {
if ($image !== null) {
useCaddyProxyForHttpBasicAuth($image);
}
Livewire::test(General::class, ['application' => createApplicationForHttpBasicAuth()])
->assertSee($reason);
})->with([
'Traefik' => [null, 'Only supported by the Caddy proxy'],
'Caddy 2.10' => ['lucaslorentz/caddy-docker-proxy:2.10-alpine', 'Needs Caddy 2.11 or newer on this server'],
]);
test('an unsupported http basic auth setting is rejected', function (string $property, mixed $value) {
Livewire::test(General::class, ['application' => createApplicationForHttpBasicAuth()])
->set($property, $value)
->call('instantSave')
->assertDispatched('error')
->assertNotDispatched('success');
})->with([
'Argon2id on a proxy without it' => ['httpBasicAuthHashAlgorithm', 'argon2id'],
'bcrypt cost above 14' => ['httpBasicAuthBcryptCost', 15],
'password longer than 72 characters' => ['httpBasicAuthPassword', str_repeat('a', 73)],
]);
@@ -106,3 +106,55 @@ it('emits the matching basic auth directive in application labels', function (st
'caddy-docker-proxy 2.13, preview, all proxies' => ['lucaslorentz/caddy-docker-proxy:2.13-alpine', false, true, 'basic_auth'],
'caddy-docker-proxy 2.13, preview, exact labels' => ['lucaslorentz/caddy-docker-proxy:2.13-alpine', true, true, 'basic_auth'],
]);
it('hashes the password in application labels with the selected settings', function (array $proxy, bool $exactLabels, bool $preview, array $hashSettings, array $expectedLabels) {
$team = Team::factory()->create();
$environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $team->id])->id]);
$server = Server::factory()->create(['team_id' => $team->id, 'proxy' => $proxy]);
$server->settings->update(['generate_exact_labels' => $exactLabels]);
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
$application = Application::factory()->createOne([
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
'fqdn' => 'https://example.com',
'is_http_basic_auth_enabled' => true,
'http_basic_auth_username' => 'admin',
'http_basic_auth_password' => 'secret',
...$hashSettings,
]);
Server::flushIdentityMap();
$applicationPreview = $preview
? (new ApplicationPreview)->forceFill(['pull_request_id' => 7, 'fqdn' => 'https://pr-7.example.com'])
: null;
$labels = collect(generateLabelsApplication($application->fresh(), $applicationPreview))->implode("\n");
expect($labels)->toContain(...$expectedLabels);
})->with(function () {
$caddy = basicAuthCaddyProxy('lucaslorentz/caddy-docker-proxy:2.13-alpine');
$traefik = basicAuthCaddyProxy('traefik:v3.7', ['type' => ProxyTypes::TRAEFIK->value]);
$bcrypt = ['http_basic_auth_bcrypt_cost' => 4];
$argon2id = ['http_basic_auth_hash_algorithm' => 'argon2id', 'http_basic_auth_argon2id_memory_cost' => 8192, 'http_basic_auth_argon2id_time_cost' => 2];
$caddyBcrypt = 'caddy_0.basic_auth.admin="$2y$04$';
$traefikBcrypt = '.basicauth.users=admin:$2y$04$';
return [
'bcrypt, all proxies' => [$caddy, false, false, $bcrypt, [$caddyBcrypt, $traefikBcrypt]],
'bcrypt, all proxies, preview' => [$caddy, false, true, $bcrypt, [$caddyBcrypt, $traefikBcrypt]],
'bcrypt, exact Caddy labels' => [$caddy, true, false, $bcrypt, [$caddyBcrypt]],
'bcrypt, exact Caddy labels, preview' => [$caddy, true, true, $bcrypt, [$caddyBcrypt]],
'bcrypt, exact Traefik labels' => [$traefik, true, false, $bcrypt, [$traefikBcrypt]],
'bcrypt, exact Traefik labels, preview' => [$traefik, true, true, $bcrypt, [$traefikBcrypt]],
'Argon2id for Caddy, bcrypt for Traefik' => [$caddy, false, false, $argon2id, [
'caddy_0.basic_auth=argon2id',
'caddy_0.basic_auth.admin="$argon2id$v=19$m=8192,t=2,p=1$',
'.basicauth.users=admin:$2y$10$',
]],
'bcrypt when Caddy is too old for the stored Argon2id' => [basicAuthCaddyProxy('lucaslorentz/caddy-docker-proxy:2.10-alpine'), false, false, $argon2id, [
'caddy_0.basic_auth.admin="$2y$10$',
'.basicauth.users=admin:$2y$10$',
]],
];
});
@@ -148,6 +148,22 @@ it('detects deployment hook changes as redeploy-only changes', function (string
'post-deployment container' => ['post_deployment_command_container', 'Post-deployment command container', 'worker'],
]);
it('detects HTTP basic auth hash setting changes as redeploy-only changes', function (string $field, string $label, string|int $newValue) {
$application = snapshotTestApplication();
markSnapshotTestApplicationDeployed($application);
$application->update([$field => $newValue]);
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
expect($diff->requiresBuild())->toBeFalse()
->and(collect($diff->changes())->pluck('label'))->toContain($label);
})->with([
'hash algorithm' => ['http_basic_auth_hash_algorithm', 'HTTP basic auth hash algorithm', 'argon2id'],
'bcrypt cost factor' => ['http_basic_auth_bcrypt_cost', 'HTTP basic auth bcrypt cost factor', 4],
'Argon2id memory' => ['http_basic_auth_argon2id_memory_cost', 'HTTP basic auth Argon2id memory (KiB)', 8192],
'Argon2id iterations' => ['http_basic_auth_argon2id_time_cost', 'HTTP basic auth Argon2id iterations', 2],
]);
it('detects source integration changes as build changes', function (string $field, string $label, mixed $newValue) {
$application = snapshotTestApplication([
'source_id' => 1,
@@ -0,0 +1,45 @@
<?php
use App\Enums\HttpBasicAuthHashAlgorithm;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(fn () => Server::flushIdentityMap());
afterEach(fn () => Server::flushIdentityMap());
it('replaces the cost factor with memory and iterations when Argon2id is selected', function () {
$stack = seedBrowserResourceStack();
$stack['server']->forceFill(['proxy' => [
'type' => ProxyTypes::CADDY->value,
'status' => 'running',
'last_saved_settings' => 'applied',
'last_applied_settings' => 'applied',
'last_saved_proxy_configuration' => "services:\n caddy:\n image: 'lucaslorentz/caddy-docker-proxy:2.13-alpine'\n",
]])->save();
$application = createBrowserApplication($stack, [
'name' => 'Basic Auth App',
'fqdn' => 'https://basic-auth.example.com',
'is_http_basic_auth_enabled' => true,
'http_basic_auth_username' => 'admin',
'http_basic_auth_password' => 'secret',
]);
loginAndSkipBoarding();
$page = visit(applicationConfigurationUrl($stack['project'], $stack['environment'], $application));
$page->assertSee('Cost factor')
->script("document.querySelector('#security-section').scrollIntoView()");
selectListboxOption($page, 'httpBasicAuthHashAlgorithm', 'Argon2id Memory-hard, more resistant to cracking');
$page->assertSeeIn('#httpBasicAuthArgon2idMemoryCost-trigger', '64 MiB')
->assertSeeIn('#httpBasicAuthArgon2idTimeCost-trigger', '4')
->assertDontSee('Cost factor')
->assertNoJavaScriptErrors()
->screenshot(filename: 'application-basic-auth-argon2id');
expect($application->fresh()->http_basic_auth_hash_algorithm)->toBe(HttpBasicAuthHashAlgorithm::ARGON2ID);
});