diff --git a/.env.production b/.env.production index ac4c479b1..cda3214d2 100644 --- a/.env.production +++ b/.env.production @@ -1,7 +1,6 @@ APP_ID= APP_NAME=Coolify APP_KEY= -COOLIFY_FLUX_LARAVEL_API_TOKEN= DB_USERNAME=coolify DB_PASSWORD= diff --git a/app/Rules/ValidServerIp.php b/app/Rules/ValidServerIp.php index bb642e18a..270ff1c34 100644 --- a/app/Rules/ValidServerIp.php +++ b/app/Rules/ValidServerIp.php @@ -9,12 +9,6 @@ class ValidServerIp implements ValidationRule { /** * Accepts a valid IPv4 address, IPv6 address, or RFC 1123 hostname. - * - * IP literals in private/reserved ranges (loopback, link-local, RFC 1918, - * etc.) are rejected by default to stop a member from pointing a server at - * the Coolify host's internal network and abusing the synchronous SSH check - * to probe it. Self-hosters on private LANs can allow them via - * config('coold.allow_private_server_ips'). */ public function validate(string $attribute, mixed $value, Closure $fail): void { @@ -25,14 +19,10 @@ class ValidServerIp implements ValidationRule $trimmed = trim($value); if (filter_var($trimmed, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - $this->failIfDisallowedRange($trimmed, $fail); - return; } if (filter_var($trimmed, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - $this->failIfDisallowedRange($trimmed, $fail); - return; } @@ -47,25 +37,4 @@ class ValidServerIp implements ValidationRule $fail('The :attribute must be a valid IPv4 address, IPv6 address, or hostname.'); } } - - /** - * Reject IPs in private/reserved ranges unless the operator has explicitly - * opted in. The IP is already known to be a valid literal here. - */ - private function failIfDisallowedRange(string $ip, Closure $fail): void - { - if (config('coold.allow_private_server_ips')) { - return; - } - - $isPublic = filter_var( - $ip, - FILTER_VALIDATE_IP, - FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE - ); - - if ($isPublic === false) { - $fail('The :attribute must not be a private or reserved IP address.'); - } - } } diff --git a/config/coold.php b/config/coold.php index cdd5b082e..cbec06e66 100644 --- a/config/coold.php +++ b/config/coold.php @@ -7,19 +7,4 @@ return [ 'dev_ssh_user' => env('COOLIFY_CLI_SSH_USER', 'coolify'), 'flux_url' => env('COOLIFY_COOLD_FLUX_URL', env('COOLIFY_COOLD_VM_FLUX_URL')), 'flux_host_jwt_path' => env('COOLIFY_COOLD_HOST_JWT_PATH', '/etc/coolify/host-jwt'), - - /* - * When false (the default), v5 server hosts/node addresses may not point at - * private or reserved IP ranges (loopback, link-local, RFC 1918, CGNAT is - * still allowed as it is the WireGuard mesh space). This blocks a team - * member from adding a server that targets the Coolify host's internal - * network and abusing the synchronous SSH connectivity check to probe it. - * - * Self-hosters running Coolify on a private LAN can opt back in by setting - * COOLIFY_ALLOW_PRIVATE_SERVER_IPS=true. - */ - 'allow_private_server_ips' => filter_var( - env('COOLIFY_ALLOW_PRIVATE_SERVER_IPS', false), - FILTER_VALIDATE_BOOLEAN - ), ]; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 864dd475c..9618e18ec 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -18,7 +18,6 @@ services: - PHP_FPM_PM_START_SERVERS=${PHP_FPM_PM_START_SERVERS:-1} - PHP_FPM_PM_MIN_SPARE_SERVERS=${PHP_FPM_PM_MIN_SPARE_SERVERS:-1} - PHP_FPM_PM_MAX_SPARE_SERVERS=${PHP_FPM_PM_MAX_SPARE_SERVERS:-10} - - COOLIFY_CONTAINER_ROLE=${COOLIFY_CONTAINER_ROLE:-all} env_file: - /data/coolify/source/.env ports: diff --git a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run index 584bd0d9d..dbc472d06 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run @@ -2,14 +2,6 @@ cd /var/www/html -. /etc/s6-overlay/scripts/container-role -role="$(coolify_container_role_value)" - -if ! coolify_container_has_role worker && ! coolify_container_has_role horizon; then - echo " INFO Horizon is disabled for role '$role', sleeping." - exec sleep infinity -fi - if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then echo " INFO Horizon is disabled, sleeping." exec sleep infinity diff --git a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run index 1f8b37079..ee46dba7e 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run @@ -2,14 +2,6 @@ cd /var/www/html -. /etc/s6-overlay/scripts/container-role -role="$(coolify_container_role_value)" - -if ! coolify_container_has_role worker && ! coolify_container_has_role nightwatch && ! coolify_container_has_role nightwatch-agent; then - echo " INFO Nightwatch is disabled for role '$role', sleeping." - exec sleep infinity -fi - if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then echo " INFO Nightwatch is enabled, starting..." exec php artisan nightwatch:agent diff --git a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run index 2dcd493a1..bfa44c7e3 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run @@ -2,14 +2,6 @@ cd /var/www/html -. /etc/s6-overlay/scripts/container-role -role="$(coolify_container_role_value)" - -if ! coolify_container_has_role worker && ! coolify_container_has_role scheduler && ! coolify_container_has_role scheduler-worker; then - echo " INFO Scheduler worker is disabled for role '$role', sleeping." - exec sleep infinity -fi - if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then echo " INFO Scheduler is disabled, sleeping." exec sleep infinity diff --git a/docker/production/etc/s6-overlay/scripts/container-role b/docker/production/etc/s6-overlay/scripts/container-role deleted file mode 100755 index c7ee7dd3c..000000000 --- a/docker/production/etc/s6-overlay/scripts/container-role +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -coolify_container_role_value() { - role="${COOLIFY_CONTAINER_ROLE:-}" - - if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" - fi - - printf '%s\n' "${role:-all}" -} - -coolify_container_has_role() { - wanted_role="$1" - roles="$(coolify_container_role_value | tr '[:upper:]' '[:lower:]' | tr ',' ' ')" - - for role in $roles; do - case "$role" in - all|"$wanted_role") - return 0 - ;; - esac - done - - return 1 -} diff --git a/docs/v5/production-activation-checklist.md b/docs/v5/production-activation-checklist.md new file mode 100644 index 000000000..06f8a9690 --- /dev/null +++ b/docs/v5/production-activation-checklist.md @@ -0,0 +1,88 @@ +# V5 Production Activation Checklist + +V5 is intentionally limited to development environments so this branch can be merged without activating V5 or changing the V4 database schema in production. Complete this checklist before making V5 available outside development. + +## Feature Gate + +- Replace the development-environment-only gate in `app/Support/V5/V5Feature.php` and `config/v5.php` with an explicit rollout flag that can be enabled per installation. +- Keep V5 disabled by default during the rollout. +- Retain a disabled-mode test so V4 continues to work when the V5 schema is absent. + +The current gate controls: + +- V5 migrations and the V5 morph map in `app/Providers/AppServiceProvider.php`. +- `/v5` routes and the V5 rate limiter in `app/Providers/RouteServiceProvider.php`. +- The internal Flux status endpoint in `routes/api.php`. +- Reconciliation and agent-token rotation schedules in `app/Console/Kernel.php`. +- V5 applications in the V4 resource list in `app/Livewire/Project/Resource/Index.php`. +- V5-aware project and environment emptiness checks in `app/Models/Project.php` and `app/Models/Environment.php`. +- V5 host teardown during team deletion in `app/Models/Team.php`. + +## Database + +- Review and back up the production database before registering `database/migrations-v5/`. +- Run the V5 migrations in staging and verify both upgrade and rollback behavior. +- Keep V5 schema changes out of `database/migrations/` until the activation strategy explicitly changes. +- Verify existing V4 projects, environments, teams, and resources remain unchanged after the V5 migrations run. + +## Flux Production Runtime + +The production image deliberately does not ship Flux today. Before activation: + +- Add the Flux binary to `docker/production/Dockerfile` with a pinned production version. +- Add the production Flux s6 service and its `user/contents.d` entry, using the development service only as a reference. +- Provision the Flux Unix socket directory, JWT signing keys, Laravel API token, and required permissions. +- Configure the required `COOLIFY_FLUX_*` and `COOLIFY_COOLD_*` values from `config/flux.php` and `config/coold.php`. +- Decide whether Flux needs a published port, persistent mounts, or host binding in `docker-compose.prod.yml`. +- Define key and token rotation procedures before enrolling production hosts. + +Production environment templates and stable/nightly install and upgrade scripts deliberately do not provision Flux tokens or storage while V5 is inactive. At activation: + +- Add the token to `.env.production` and generate it during new stable and nightly installations. +- Update stable and nightly upgrades to generate a token only when one is missing, preserving existing tokens. +- Create the persistent Flux storage path with the ownership and permissions required by the production runtime. +- Verify the Laravel and Flux processes receive the same token without exposing it in logs. +- Document and test zero-downtime rotation with `COOLIFY_FLUX_LARAVEL_API_TOKENS` before production rollout. + +## Container Processes + +Container roles are development-only while V5 is inactive in production. + +- Keep role handling under `docker/development/` and `docker-compose.dev.yml`. +- Keep production Horizon, scheduler, and Nightwatch startup identical to `next` until V5 activation. +- At activation, decide whether production needs separate web, worker, scheduler, Nightwatch, or Flux roles. Introduce production role handling in a dedicated change if it is required. + +## Queues and Scheduling + +- Add a production `v5reconcile` Horizon supervisor for the `v5-reconcile` queue in `config/horizon.php`. +- Set production process counts, memory, retry, and timeout values from measured workloads. +- Enable and monitor `V5ReconcileServersJob` and `V5RotateAgentTokensJob` schedules. +- Verify reconciliation and token rotation are idempotent across multiple application instances. + +## Commands and Seeders + +- Keep `flux:dev` and `v5:sync-dev-lima-servers` development-only. +- Replace `v5:flux-generate-keys` with, or adapt it into, a production-safe key provisioning and rotation workflow. +- Keep `V5DevLimaSeeder` restricted to development environments. + +## V4 Compatibility + +- Keep shared server IP validation aligned with `next` unless a separately reviewed V4 change is intended. +- Verify V4 resource pages do not query V5 tables while V5 is disabled. +- Verify V4 API routes, deployment flows, queues, and background processes behave the same with the rollout flag disabled. +- Test project, environment, and team deletion with V5 disabled and enabled. + +## Tests to Update at Activation + +- Update `tests/Feature/V5DevelopmentIsolationTest.php`, which currently requires production and staging to omit V5 routes, migrations, Horizon workers, and the Flux runtime. +- Retain `tests/Feature/V5DisabledModelIsolationTest.php` for installations where V5 remains disabled. +- Extend `tests/Feature/ContainerRoleScriptTest.php` if production container roles are introduced. +- Run the V5 migration, authorization, lifecycle, reconciliation, token rotation, API, and browser test suites against a production-like staging environment. + +## Activation Exit Criteria + +- V5 is explicitly enabled rather than inferred only from `APP_ENV`. +- Production migrations, Flux, queues, schedules, secrets, and networking are provisioned and monitored. +- A rollback procedure has been tested. +- V4 regression tests pass with V5 both disabled and enabled. +- V5 browser and deployment smoke tests pass in staging. diff --git a/other/nightly/install.sh b/other/nightly/install.sh index f4458d440..365e4c330 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -840,7 +840,6 @@ update_env_var() { update_env_var "APP_ID" "$(openssl rand -hex 16)" update_env_var "APP_KEY" "base64:$(openssl rand -base64 32)" -update_env_var "COOLIFY_FLUX_LARAVEL_API_TOKEN" "$(openssl rand -hex 32)" # update_env_var "DB_USERNAME" "$(openssl rand -hex 16)" # Causes issues: database "random-user" does not exist update_env_var "DB_PASSWORD" "$(openssl rand -base64 32)" update_env_var "REDIS_PASSWORD" "$(openssl rand -base64 32)" diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index 635ddd77c..94fb77607 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -147,7 +147,6 @@ set_env_var() { } log "Checking environment variables..." -update_env_var "COOLIFY_FLUX_LARAVEL_API_TOKEN" "$(openssl rand -hex 32)" set_env_var "REGISTRY_URL" "$REGISTRY_URL" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" diff --git a/scripts/install.sh b/scripts/install.sh index 96449bb79..7569efcc8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -840,7 +840,6 @@ update_env_var() { update_env_var "APP_ID" "$(openssl rand -hex 16)" update_env_var "APP_KEY" "base64:$(openssl rand -base64 32)" -update_env_var "COOLIFY_FLUX_LARAVEL_API_TOKEN" "$(openssl rand -hex 32)" # update_env_var "DB_USERNAME" "$(openssl rand -hex 16)" # Causes issues: database "random-user" does not exist update_env_var "DB_PASSWORD" "$(openssl rand -base64 32)" update_env_var "REDIS_PASSWORD" "$(openssl rand -base64 32)" diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 206327f65..a909d2e49 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -147,7 +147,6 @@ set_env_var() { } log "Checking environment variables..." -update_env_var "COOLIFY_FLUX_LARAVEL_API_TOKEN" "$(openssl rand -hex 32)" set_env_var "REGISTRY_URL" "$REGISTRY_URL" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" diff --git a/tests/Feature/ContainerRoleScriptTest.php b/tests/Feature/ContainerRoleScriptTest.php index f53864d4b..969a7d8fa 100644 --- a/tests/Feature/ContainerRoleScriptTest.php +++ b/tests/Feature/ContainerRoleScriptTest.php @@ -33,9 +33,16 @@ it('falls back to the container role from the local env file', function () { expect($result->successful())->toBeTrue(); }); -it('keeps production and development role helpers in sync', function () { - expect(file_get_contents(base_path('docker/production/etc/s6-overlay/scripts/container-role'))) - ->toBe(file_get_contents(base_path('docker/development/etc/s6-overlay/scripts/container-role'))); +it('keeps container roles out of the production image', function () { + expect(base_path('docker/production/etc/s6-overlay/scripts/container-role'))->not->toBeFile(); + + foreach (['horizon', 'nightwatch-agent', 'scheduler-worker'] as $service) { + $runScript = file_get_contents(base_path("docker/production/etc/s6-overlay/s6-rc.d/{$service}/run")); + + expect($runScript) + ->not->toContain('container-role') + ->not->toContain('COOLIFY_CONTAINER_ROLE'); + } }); function runContainerRoleHelper(string $roles, string $serviceRole, ?string $workingDirectory = null): ProcessResult @@ -61,17 +68,15 @@ it('does not register a separate v5 flux status listener service', function () { } }); -it('configures flux to publish v5 resource statuses to laravel over local http by default', function () { - foreach (['development', 'production'] as $environment) { - $runScript = file_get_contents(base_path("docker/{$environment}/etc/s6-overlay/s6-rc.d/flux/run")); +it('configures development flux to publish v5 resource statuses to laravel over local http by default', function () { + $runScript = file_get_contents(base_path('docker/development/etc/s6-overlay/s6-rc.d/flux/run')); - expect($runScript) - ->toContain('COOLIFY_FLUX_LARAVEL_API_URL') - ->toContain('http://127.0.0.1:8080') - ->toContain('COOLIFY_FLUX_LARAVEL_API_TOKEN') - ->not->toContain('APP_KEY') - ->not->toContain("grep -E '^APP_KEY=' .env") - ->not->toContain('COOLIFY_FLUX_REDIS_URL') - ->not->toContain('COOLIFY_FLUX_RESOURCE_STATUS_CHANNEL'); - } + expect($runScript) + ->toContain('COOLIFY_FLUX_LARAVEL_API_URL') + ->toContain('http://127.0.0.1:8080') + ->toContain('COOLIFY_FLUX_LARAVEL_API_TOKEN') + ->not->toContain('APP_KEY') + ->not->toContain("grep -E '^APP_KEY=' .env") + ->not->toContain('COOLIFY_FLUX_REDIS_URL') + ->not->toContain('COOLIFY_FLUX_RESOURCE_STATUS_CHANNEL'); }); diff --git a/tests/Unit/UpgradePostgresScriptTest.php b/tests/Unit/UpgradePostgresScriptTest.php index 42ef53113..5fd427746 100644 --- a/tests/Unit/UpgradePostgresScriptTest.php +++ b/tests/Unit/UpgradePostgresScriptTest.php @@ -44,24 +44,22 @@ it('downloads postgres upgrade script during install and upgrade without auto-ru 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); -it('generates a dedicated flux laravel api token during install and upgrade', function (string $path) { - $script = file_get_contents(getcwd().'/'.$path); +it('does not provision a flux laravel api token before v5 production activation', function (string $path) { + $source = file_get_contents(getcwd().'/'.$path); - expect($script) - ->toContain('update_env_var "COOLIFY_FLUX_LARAVEL_API_TOKEN" "$(openssl rand -hex 32)"'); + expect($source)->not->toContain('COOLIFY_FLUX_LARAVEL_API_TOKEN'); })->with([ + 'production env' => '.env.production', 'stable install' => 'scripts/install.sh', 'nightly install' => 'other/nightly/install.sh', 'stable upgrade' => 'scripts/upgrade.sh', 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); -it('creates a writable flux storage directory during install and upgrade', function (string $path) { - $script = file_get_contents(getcwd().'/'.$path); +it('does not provision flux storage before v5 production activation', function (string $path) { + $source = file_get_contents(getcwd().'/'.$path); - expect($script) - ->toContain('/data/coolify/flux') - ->toContain('chown -R 9999:root /data/coolify/flux'); + expect($source)->not->toContain('/data/coolify/flux'); })->with([ 'stable install' => 'scripts/install.sh', 'nightly install' => 'other/nightly/install.sh', diff --git a/tests/Unit/ValidServerIpTest.php b/tests/Unit/ValidServerIpTest.php new file mode 100644 index 000000000..af44286e6 --- /dev/null +++ b/tests/Unit/ValidServerIpTest.php @@ -0,0 +1,23 @@ +validate('ip', $ip, function () use (&$failCalled): void { + $failCalled = true; + }); + + expect($failCalled)->toBeFalse(); +})->with([ + 'private IPv4' => '192.168.1.10', + 'loopback IPv4' => '127.0.0.1', + 'link-local IPv4' => '169.254.1.10', + 'loopback IPv6' => '::1', + 'unique local IPv6' => 'fd00::1', +]);