feat(v5): add server reconciliation and canvas APIs

Split V5 dashboard behavior into domain controllers and policies,
add agent token rotation/revocation, status reconciliation jobs,
ingress firewall syncing, and canvas connection APIs.

Add migrations for V5 status tracking, server capabilities, resource
connection aliases, and revoked agent tokens.
This commit is contained in:
Andras Bacsai committed 2026-07-06 17:40:37 +02:00
1 parent df854feee8
commit 6ae45684f9
147 files changed
+22444 -10407

No files matched your search

+17
View File
@@ -5,4 +5,21 @@ return [
'coold_version' => env('COOLIFY_COOLD_VERSION', 'nightly'),
'corrosion_version' => env('COOLIFY_CORROSION_VERSION', 'v1.0.0'),
'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
),
];
+100
View File
@@ -7,5 +7,105 @@ return [
'health_timeout_seconds' => (float) env('COOLIFY_FLUX_HEALTH_TIMEOUT_SECONDS', 1.0),
'connection_timeout_seconds' => (float) env('COOLIFY_FLUX_CONNECTION_TIMEOUT_SECONDS', 1.0),
'dispatch_timeout_seconds' => (float) env('COOLIFY_FLUX_DISPATCH_TIMEOUT_SECONDS', 35.0),
'bootstrap_host_connection_timeout_seconds' => (int) env('COOLIFY_FLUX_BOOTSTRAP_HOST_CONNECTION_TIMEOUT_SECONDS', 30),
/*
|--------------------------------------------------------------------------
| Host agent (coold) token capabilities
|--------------------------------------------------------------------------
|
| The EXACT set of primitive capability strings coold advertises to flux on
| connect (coold/coold/src/grpc/client.rs, `primitive_capabilities`). Minting
| these explicit strings — instead of the `host-agent:default` wildcard
| profile — means the token no longer relies on flux's
| `capability_profile_authorizes_all` bypass. flux INTERSECTS the jwt `caps`
| with coold's advertised set, so as long as this list matches coold's
| advertised primitives the host retains exactly the same effective power.
|
| SAFETY: keep this list byte-for-byte in sync with coold's
| `primitive_capabilities()`. A string here that coold does not advertise is
| silently dropped by flux's intersection; a verb coold needs that is missing
| here means the host loses that ability.
|
| `host.jwt.set` authorizes RPC-delivered host-JWT rotation (the token can
| authorize its own replacement over the live stream; Laravel is the root of
| trust that holds the signing key).
*/
'host_capabilities' => [
'images.pull',
'images.list',
'images.delete',
'containers.create',
'containers.start',
'containers.stop',
'containers.restart',
'containers.delete',
'containers.inspect',
'containers.list',
'containers.logs',
'containers.exec',
'containers.healthcheck.run',
'ingress.apply',
'ingress.stop',
'firewall.allow',
'firewall.revoke',
'firewall.list',
'firewall.reconcile',
'coold.logs',
'corrosion.tables',
'host.jwt.set',
],
/*
| Emergency escape hatch: when set (e.g. to `host-agent:default`), minted
| host tokens carry ONLY this single capability profile instead of the
| explicit list above. This re-enables flux's wildcard bypass and is meant
| purely for rollback without a code change if the explicit list ever drifts
| from coold's advertised set and breaks the data plane. Leave NULL in
| production so tokens are explicitly scoped.
*/
'host_capability_profile' => env('COOLIFY_FLUX_HOST_CAPABILITY_PROFILE'),
/*
| Host JWT lifetime and rotation.
|
| `host_token_ttl` is the token `exp` window (default 1h) — the maximum time
| a leaked/undetected token stays valid if BOTH rotation and revocation fail.
| Keep this at or below flux's `COOLIFY_FLUX_MAX_TOKEN_LIFETIME_SECS`
| default (3600), otherwise flux rejects coold streams at connect.
| `host_token_refresh_threshold` is the remaining-lifetime below which the
| rotation job re-mints and re-delivers a fresh token (default 30m).
| Keep the threshold below the TTL.
*/
'host_token_ttl' => (int) env('COOLIFY_FLUX_HOST_TOKEN_TTL', 3600),
'host_token_refresh_threshold' => (int) env('COOLIFY_FLUX_HOST_TOKEN_REFRESH_THRESHOLD', 1800),
/*
| JWT header `kid` minted into host tokens. flux selects the verification key
| by this id (single default key today; a per-cluster keys directory can map
| `kid = cluster-<id>` to `<kid>.pub` for per-tenant signing keys later).
*/
'jwt_kid' => env('COOLIFY_FLUX_JWT_KID', 'flux-default'),
/*
|--------------------------------------------------------------------------
| Inbound flux -> Laravel API token(s)
|--------------------------------------------------------------------------
|
| flux authenticates to Laravel's internal status-ingest endpoint with a
| bearer token. `laravel_api_tokens` accepts SEVERAL tokens at once so an
| operator can rotate with zero downtime:
| 1. add the new token alongside the old:
| COOLIFY_FLUX_LARAVEL_API_TOKENS=<old>,<new>
| then `php artisan config:clear`
| 2. cut every flux instance over to <new>
| 3. drop <old> from the list and `config:clear` again
| Generate tokens with `openssl rand -hex 32`. The single `laravel_api_token`
| remains as a fallback so existing single-token installs keep working.
*/
'laravel_api_token' => env('COOLIFY_FLUX_LARAVEL_API_TOKEN'),
'laravel_api_tokens' => array_values(array_filter(array_map(
'trim',
explode(',', (string) env('COOLIFY_FLUX_LARAVEL_API_TOKENS', ''))
))),
];
+28 -1
View File
@@ -192,6 +192,24 @@ return [
'sleep' => 3,
'timeout' => env('HORIZON_TIMEOUT', 36000),
],
// Dedicated low-priority pool for the v5 reconcile + host-token rotation
// jobs (queue `v5-reconcile`, set via onQueue()). Isolated from the
// user-facing high/default deploy pool so a starved rotation cannot let
// host tokens drift to expiry, and so the 5-minute fleet fan-out never
// blocks deploys.
'v5reconcile' => [
'connection' => 'redis',
'balance' => env('HORIZON_V5_RECONCILE_BALANCE', 'false'),
'queue' => 'v5-reconcile',
'maxTime' => env('HORIZON_V5_RECONCILE_MAX_TIME', 0),
'maxJobs' => 200,
'memory' => 128,
'tries' => 1,
'nice' => 10,
'sleep' => 3,
'timeout' => env('HORIZON_V5_RECONCILE_TIMEOUT', 300),
],
],
'environments' => [
@@ -203,7 +221,11 @@ return [
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
'v5reconcile' => [
'autoScalingStrategy' => 'size',
'minProcesses' => env('HORIZON_V5_RECONCILE_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_V5_RECONCILE_MAX_PROCESSES', 2),
],
],
'local' => [
's6' => [
@@ -213,6 +235,11 @@ return [
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
'v5reconcile' => [
'autoScalingStrategy' => 'size',
'minProcesses' => env('HORIZON_V5_RECONCILE_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_V5_RECONCILE_MAX_PROCESSES', 1),
],
],
],
];