Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions app/Models/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -1087,12 +1087,14 @@ public function dirOnServer()
return application_configuration_dir()."/{$this->uuid}";
}

public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null)
public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null, ?string $gitSshCommand = null)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
$escapedBaseDir = escapeshellarg($baseDir);
$isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false;

$sshCommand = $gitSshCommand ?? 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';

// Use the explicitly passed commit (e.g. from rollback), falling back to the application's git_commit_sha.
// Invalid refs will cause the git checkout/fetch command to fail on the remote server.
$commitToUse = $commit ?? $this->git_commit_sha;
Expand All @@ -1102,9 +1104,9 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_
// If shallow clone is enabled and we need a specific commit,
// we need to fetch that specific commit with depth=1
if ($isShallowCloneEnabled) {
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
} else {
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
}
}
if ($this->settings->is_git_submodules_enabled) {
Expand All @@ -1115,10 +1117,10 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_
}
// Add shallow submodules flag if shallow clone is enabled
$submoduleFlags = $isShallowCloneEnabled ? '--depth=1' : '';
$git_clone_command = "{$git_clone_command} git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git submodule update --init --recursive {$submoduleFlags}; fi";
$git_clone_command = "{$git_clone_command} git submodule sync && GIT_SSH_COMMAND=\"{$sshCommand}\" git submodule update --init --recursive {$submoduleFlags}; fi";
}
if ($this->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git lfs pull";
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git lfs pull";
}

return $git_clone_command;
Expand Down Expand Up @@ -1301,12 +1303,18 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
}
} else {
$github_access_token = generateGithubInstallationToken($this->source);
// Configure git to rewrite URLs with the token so submodules on the same host authenticate correctly
$escapedTokenUrl = escapeshellarg("{$source_html_url_scheme}://x-access-token:{$github_access_token}@{$source_html_url_host}/");
$escapedHostUrl = escapeshellarg("{$source_html_url_scheme}://{$source_html_url_host}/");
$gitConfigCommand = "git config --global url.{$escapedTokenUrl}.insteadOf {$escapedHostUrl}";
Comment on lines +1307 to +1309
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the code around lines 1307-1309 with broader context
head -n 1350 app/Models/Application.php | tail -n 100 | cat -n

Repository: coollabsio/coolify

Length of output: 6435


🏁 Script executed:

# Search for SSH-related handling in the Application.php file
rg -n "ssh|git@|insteadOf" app/Models/Application.php -A 2 -B 2

Repository: coollabsio/coolify

Length of output: 7173


🏁 Script executed:

# Check if there's any .gitmodules handling or submodule configuration
rg -n "gitmodules|submodule" app/Models/Application.php -A 3 -B 3

Repository: coollabsio/coolify

Length of output: 3290


🏁 Script executed:

# Look for any environment variable or config related to SSH keys or token
rg -n "github.*token|access.token" app/Models/Application.php -i -A 2 -B 2

Repository: coollabsio/coolify

Length of output: 2600


SSH submodules will skip the GitHub App token authentication.

The git config url.{token}.insteadOf rewrite only matches https://{host}/... URLs. Any submodules declared as git@{host}:owner/repo.git or ssh://git@{host}/owner/repo.git in .gitmodules will use SSH during git submodule update and bypass this token-based authentication entirely.

The codebase already handles this for public repos (line 1116 converts git@... to https:// in .gitmodules), but private GitHub App repos lack the equivalent SSH URL normalization. Either add the same .gitmodules sed rewrite for private repos, or normalize SSH URLs before submodule init.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Application.php` around lines 1307 - 1309, The current git
credential rewrite (building $escapedTokenUrl / $escapedHostUrl and
$gitConfigCommand) only covers https submodule URLs so SSH submodules (git@...
or ssh://git@...) will bypass the GitHub App token; fix by normalizing SSH
submodule URLs to HTTPS before submodule init/update: apply the same .gitmodules
SSH-to-https sed/replace logic used for public repos (the existing git@... ->
https conversion) when handling private GitHub App repos, or perform an explicit
rewrite of git@ and ssh://git@ patterns in .gitmodules prior to running git
submodule init/update so the token-based git config rewrite will match. Ensure
this normalization runs in the same code path that constructs
$escapedTokenUrl/$gitConfigCommand so token auth applies to submodules.

if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $gitConfigCommand));
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
$escapedRepoUrl = escapeshellarg($repoUrl);
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
$fullRepoUrl = $repoUrl;
} else {
$commands->push($gitConfigCommand);
Comment on lines 1305 to +1317
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the file and examine the specific lines and surrounding context
sed -n '1300,1320p' app/Models/Application.php

Repository: coollabsio/coolify

Length of output: 1790


🏁 Script executed:

# Check if there's any cleanup of global git config
rg 'git.*config.*--global.*--unset' app/Models/Application.php

Repository: coollabsio/coolify

Length of output: 44


🏁 Script executed:

# Look at the loadComposeFile function and how it calls generateGitImportCommands
sed -n '1540,1555p' app/Models/Application.php

Repository: coollabsio/coolify

Length of output: 1105


🏁 Script executed:

# Check the full generateGitImportCommands function signature and context
sed -n '1200,1250p' app/Models/Application.php

Repository: coollabsio/coolify

Length of output: 2483


🏁 Script executed:

# Search for any cleanup mechanisms after git config is set
rg -A 10 'git config --global url' app/Models/Application.php

Repository: coollabsio/coolify

Length of output: 980


Remove --global flag; use git -c override instead to avoid token residue on the host.

Line 1309 writes the GitHub installation token into global git config, and this branch executes on the actual host machine (called from loadComposeFile() at line 1547 with exec_in_docker: false). After deployment finishes, that credential stays in the host's global config—basically a taco left in your server's gluten-free kitchen. No cleanup happens afterward. Short-lived tokens don't need permanent storage; use git -c url.<tokenUrl>.insteadOf=<hostUrl> to scope the rewrite to just that command instead.

🧰 Tools
🪛 PHPMD (2.15.0)

[error] 1305-1305: The variable $github_access_token is not named in camelCase. (undefined)

(CamelCaseVariableName)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Application.php` around lines 1305 - 1317, The code currently
writes a token rewrite into global Git config via $gitConfigCommand (used when
exec_in_docker is false), which persists credentials; instead, remove the use of
--global and do not push a global config entry—use a per-command override with
git -c url.<tokenUrl>.insteadOf=<hostUrl> applied to the clone command: build
the config fragment from the same escapedTokenUrl/escapedHostUrl values and
prepend or inject it into $git_clone_command (or the string sent to
$commands->push) so the token is only in the environment of that single git
invocation; keep the existing executeInDocker path unchanged and ensure
$fullRepoUrl/$escapedRepoUrl/$escapedBaseDir behavior remains correct when
modifying $git_clone_command.

$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
$escapedRepoUrl = escapeshellarg($repoUrl);
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
Expand Down Expand Up @@ -1348,11 +1356,12 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
}
$private_key = base64_encode($private_key);
$escapedCustomRepository = escapeshellarg($customRepository);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
$deployKeySshCommand = "ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa";
$git_clone_command_base = "GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
if ($only_checkout) {
$git_clone_command = $git_clone_command_base;
} else {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit);
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, gitSshCommand: $deployKeySshCommand);
}
if ($exec_in_docker) {
$commands = collect([
Expand All @@ -1375,22 +1384,22 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" ".$this->buildGitCheckoutCommand($commit, $deployKeySshCommand);
}
}

Expand All @@ -1411,6 +1420,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$escapedCustomRepository = escapeshellarg($customRepository);
$git_clone_command = "{$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit);
$otherSshCommand = "ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa";

if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') {
Expand All @@ -1420,22 +1430,22 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" ".$this->buildGitCheckoutCommand($commit, $otherSshCommand);
}
}

Expand Down Expand Up @@ -1684,13 +1694,14 @@ public function fqdns(): Attribute
);
}

protected function buildGitCheckoutCommand($target): string
protected function buildGitCheckoutCommand($target, ?string $gitSshCommand = null): string
{
$escapedTarget = escapeshellarg($target);
$command = "git checkout {$escapedTarget}";

if ($this->settings->is_git_submodules_enabled) {
$command .= ' && git submodule update --init --recursive';
$sshCommand = $gitSshCommand ?? 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
$command .= " && GIT_SSH_COMMAND=\"{$sshCommand}\" git submodule update --init --recursive";
}

return $command;
Expand Down
27 changes: 27 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3339,6 +3339,15 @@
"schema": {
"type": "string"
}
},
{
"name": "docker_cleanup",
"in": "query",
"description": "Perform docker cleanup (prune networks, volumes, etc.).",
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
Expand Down Expand Up @@ -5864,6 +5873,15 @@
"schema": {
"type": "string"
}
},
{
"name": "docker_cleanup",
"in": "query",
"description": "Perform docker cleanup (prune networks, volumes, etc.).",
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
Expand Down Expand Up @@ -10561,6 +10579,15 @@
"schema": {
"type": "string"
}
},
{
"name": "docker_cleanup",
"in": "query",
"description": "Perform docker cleanup (prune networks, volumes, etc.).",
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
Expand Down
21 changes: 21 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,13 @@ paths:
required: true
schema:
type: string
-
name: docker_cleanup
in: query
description: 'Perform docker cleanup (prune networks, volumes, etc.).'
schema:
type: boolean
default: true
responses:
'200':
description: 'Stop application.'
Expand Down Expand Up @@ -3806,6 +3813,13 @@ paths:
required: true
schema:
type: string
-
name: docker_cleanup
in: query
description: 'Perform docker cleanup (prune networks, volumes, etc.).'
schema:
type: boolean
default: true
responses:
'200':
description: 'Stop database.'
Expand Down Expand Up @@ -6645,6 +6659,13 @@ paths:
required: true
schema:
type: string
-
name: docker_cleanup
in: query
description: 'Perform docker cleanup (prune networks, volumes, etc.).'
schema:
type: boolean
default: true
responses:
'200':
description: 'Stop service.'
Expand Down
Loading
Loading