Skip to content

fix: keep git provider credentials out of cloned repositories - #27

Merged
agentHits merged 1 commit into
AgentHits-Devfrom
fix/provider-clone-credentials
Oct 7, 2026
Merged

agentHits merged 1 commit into
AgentHits-Devfrom
fix/provider-clone-credentials

Conversation

@agentHits

Copy link
Copy Markdown
Collaborator

What is this PR about?

Git provider tokens were embedded in the clone URL (oauth2:<token>@, x-bitbucket-api-token-auth:<token>@, user:app-password@), so git clone stored them in <code>/.git/config and in submodule configs. Build steps and bind mounts of the service directory could read them.

  • buildGitCloneCommand now clones from a URL without credentials and passes them only as git -c http.<provider origin>/.extraHeader=Authorization: Basic .... git -c values are not persisted, they reach submodule clones, and the origin scope keeps the header away from submodules hosted elsewhere.
  • GitHub, GitLab, Gitea and Bitbucket (API token and app password) use it. Custom Git HTTPS URLs with user:password@ are split the same way, so the stored remote has no credentials.
  • Exact compose deployments (expectedRevision) used a separate git fetch origin <sha> that relied on the stored credentials. The fetch and checkout --detach now run inside the clone command (in a subshell, so a failure still fails under set -e) with the same per-invocation credentials. Afterwards only rev-parse HEAD runs.
  • The custom Git SSH key no longer goes to a shared, fixed /tmp/id_rsa. It is written to a uniquely named file created with umask 077 and removed after the clone, including on failure.
  • Local and remote-server deployments are both covered because they run the same command string. When a command shows up in an ExecError, redactSensitiveText already masks Authorization: Basic ....

Checks

  • CI=true bunx vitest run --config __test__/vitest.config.ts on the new and changed tests (git-provider/clone-credentials-persistence, git-provider/clone-command-boundary, git-provider/github-clone-host, env/compose-env-upsert): 45/45 passed.
  • The same runner on the git-provider, env, compose, deployment, placement, volume-backups, security/patch-path and deploy/application.command tests: 79 files, 789 passed, 1 skipped.
  • With the provider and compose sources taken from origin/AgentHits-Dev, all 18 new and changed tests fail.
  • bun run typecheck: exit 0.
  • bunx biome check <changed files>: 0 errors.
  • Not run: a real clone against a Git server, including submodules. Propagation of git -c to submodules and http.<url>.extraHeader matching rely on documented Git behaviour.

Rollout notes

  • Checkouts made before this change keep the old token in .git/config until the next deployment recreates the code directory. Redeploy the services that use providers. Rotate long-lived Bitbucket API tokens or app passwords if those directories were exposed.
  • Authorization is now sent preemptively as Basic to the provider origin. Custom Git servers that accept only NTLM/Negotiate can no longer authenticate with user:password@ in the URL.
  • Same-host submodules with absolute URLs now also authenticate. Submodules on other hosts still get no credentials.
  • The token is still part of the deploy shell command (process arguments) while it runs, as before. This PR only stops it from being written to disk.

🤖 Generated with Claude Code

Что:
- GitHub, GitLab, Gitea и Bitbucket клонируют по URL без учетных данных; токен передается только через `git -c http.<origin>/.extraHeader=Authorization: Basic ...` и не записывается в `.git/config` и конфиги сабмодулей.
- Для custom Git HTTPS-URL с `user:password@` учетные данные убираются из сохраняемого remote тем же способом.
- Точный деплой compose (`expectedRevision`) делает fetch/checkout внутри той же команды клонирования с теми же временными учетными данными; после этого выполняется только `rev-parse`.
- SSH-ключ custom Git пишется во временный файл с уникальным именем и правами 0600 (`umask 077`) вместо общего `/tmp/id_rsa` и удаляется после клонирования, в том числе при ошибке.
Зачем:
- Токен провайдера оставался в `<code>/.git/config`, где его могли прочитать шаги сборки и bind mount каталога сервиса; использование провайдера не должно давать доступ к его токенам.
Риски:
- Авторизация теперь отправляется сразу (preemptive Basic) только на origin провайдера; сабмодули на других хостах токен не получают, а сабмодули на том же хосте с абсолютным URL теперь тоже авторизуются.
- Custom Git серверы, принимающие только NTLM/Negotiate, не смогут использовать `user:password@` в URL.
- Уже существующие checkout'ы сохраняют старый токен до следующего деплоя, который пересоздает каталог.
Проверки:
- Команды и результаты: `CI=true bunx vitest run --config __test__/vitest.config.ts` по новым и измененным тестам (clone-credentials-persistence, clone-command-boundary, github-clone-host, compose-env-upsert) - 45/45; по папкам git-provider, env, compose, deployment, placement, volume-backups, security/patch-path, deploy/application.command - 79 файлов, 789 passed, 1 skipped; на исходниках из origin/AgentHits-Dev все 18 новых и измененных тестов падают; `bun run typecheck` - exit 0; `bunx biome check <измененные файлы>` - 0 ошибок.
- Ограничения: реальное клонирование с git-сервером (включая сабмодули) не запускалось; передача `git -c` в сабмодули и `http.<url>.extraHeader` опираются на документированное поведение git.

What:
- GitHub, GitLab, Gitea and Bitbucket clone from a URL without credentials; the token is passed only through `git -c http.<origin>/.extraHeader=Authorization: Basic ...` and is not written to `.git/config` or submodule configs.
- Custom Git HTTPS URLs with `user:password@` get the same treatment, so the stored remote has no credentials.
- Exact compose deployments (`expectedRevision`) fetch and check out the revision inside the same clone command with the same per-invocation credentials; only `rev-parse` runs afterwards.
- The custom Git SSH key is written to a uniquely named temp file created with 0600 (`umask 077`) instead of the shared `/tmp/id_rsa`, and is removed after the clone, also on failure.
Why:
- The provider token stayed in `<code>/.git/config`, readable by build steps and bind mounts of the service directory; using a provider must not give access to its tokens.
Risks:
- Authorization is now sent preemptively (Basic) and only to the provider origin; submodules on other hosts do not receive the token, while same-host submodules with absolute URLs now get it too.
- Custom Git servers that accept only NTLM/Negotiate can no longer use `user:password@` in the URL.
- Existing checkouts keep the old token until the next deployment recreates the directory.
Checks:
- Commands and results: `CI=true bunx vitest run --config __test__/vitest.config.ts` on the new and changed tests (clone-credentials-persistence, clone-command-boundary, github-clone-host, compose-env-upsert) - 45/45; on the git-provider, env, compose, deployment, placement, volume-backups, security/patch-path and deploy/application.command folders - 79 files, 789 passed, 1 skipped; with the sources from origin/AgentHits-Dev all 18 new and changed tests fail; `bun run typecheck` - exit 0; `bunx biome check <changed files>` - 0 errors.
- Limitations: no real clone against a Git server (including submodules) was run; `git -c` propagation to submodules and `http.<url>.extraHeader` rely on documented Git behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@agentHits
agentHits merged commit ca8f5b5 into AgentHits-Dev Oct 7, 2026
11 of 12 checks passed
@agentHits
agentHits deleted the fix/provider-clone-credentials branch October 7, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant