Skip to content

Support external Git credential providers for non-GitHub repositories #404

Description

@bcho

Background

We want to reuse Cachew for private Git repositories hosted outside GitHub. Authentication for these repositories is not based on a GitHub App, so the existing GitHub-specific integration cannot provide the required credentials.

Cachew should support repository-scoped external credential providers without embedding provider-specific authentication logic in the main process. Providers should return a complete HTTP authorization value so repositories can use schemes such as Basic or Bearer authentication.

Proposed configuration

Add a repeatable git-credential-command block:

git-credential-command "private-git" {
  command = [
    "/usr/local/bin/private-git-credential",
    "--provider-option", "value",
  ]

  remotes = [
    "https://git.example.com/project/repository",
  ]

  timeout        = "5s"
  refresh-before = "5m"
}

Configured repository URLs are canonicalized and matched exactly. A provider's credential must not be applied to a repository that was not assigned to it.

Proposed internal interface

type Credential struct {
    Authorization string
    URLScope       string
}

type Provider interface {
    Credential(
        ctx context.Context,
        repositoryURL string,
    ) (credential Credential, matched bool, err error)
}

Authorization is the complete HTTP authorization value, for example Basic ... or Bearer .... URLScope is the canonical repository URL to which it may be applied.

Providers can be composed in order, stopping at the first match. Existing in-process authentication integrations can be adapted to this interface without invoking a plugin binary.

A matched provider failure should fail closed rather than falling back to unauthenticated access. Unmatched repositories retain the existing authentication behavior.

Plugin interface

A plugin implements credential acquisition for one canonical repository URL:

type CommandResult struct {
    Authorization string
    ExpiresAt     time.Time
}

type CommandHandler interface {
    Credential(ctx context.Context, remoteURL string) (CommandResult, error)
}

A helper library can expose ServeCommand, request/response encoding functions, and an optional CLI entry point for Go implementations. Plugins may be written in any language as long as they implement the protocol.

Main process and plugin protocol

Cachew executes the configured command directly without a shell and applies the configured timeout. One request and one response are exchanged per invocation.

Cachew writes one newline-terminated JSON object to stdin:

{"version":1,"remote_url":"https://git.example.com/project/repository"}

The plugin writes one newline-terminated JSON object to stdout:

{"version":1,"authorization":"Bearer example-token","expires_at":"2026-08-10T12:00:00Z"}

Proposed schemas:

const ProtocolVersion = 1

type Request struct {
    Version   int    `json:"version"`
    RemoteURL string `json:"remote_url"`
}

type Response struct {
    Version       int       `json:"version"`
    Authorization string    `json:"authorization"`
    ExpiresAt     time.Time `json:"expires_at"`
}

Protocol requirements:

  • remote_url is the canonical upstream repository URL.
  • authorization contains the complete HTTP Authorization header value.
  • expires_at is a future RFC 3339 timestamp.
  • Requests and responses are size-limited and strictly decoded.
  • Unknown fields, extra JSON values, unsupported versions, invalid authorization values, and expired credentials are rejected.
  • Plugin failures, timeouts, unsuccessful exits, and invalid responses fail the matched operation.

Credential lifecycle

Successful credentials are cached only in memory:

  1. Canonicalize the requested repository URL and select its exact provider match.
  2. Return an unexpired cached credential when available.
  3. Refresh credentials within the configured refresh-before window.
  4. Coalesce concurrent refreshes for the same provider and repository.
  5. Scope the returned authorization to the canonical repository URL.
  6. Discard credentials after expiration; never persist them.

Applying credentials

The authorization should be applied to Git as repository-scoped HTTP configuration through GIT_CONFIG_* environment entries:

GIT_CONFIG_KEY_<n>=http.<canonical-repository-url>.extraHeader
GIT_CONFIG_VALUE_<n>=Authorization: <provider-value>

This supports Basic and Bearer credentials without a shell-form credential helper or credentials in Git subprocess arguments. This part is related to #402.

The same provider should authenticate:

  • Background clone, fetch, and mirror operations.
  • Direct upstream Git HTTP requests.
  • Cache-miss and stale-reference fallbacks.
  • Push requests.
  • Git LFS requests.

Security properties

  • Exact canonical repository matching and URL scoping.
  • No shell execution for provider commands.
  • No credentials in subprocess command-line arguments.
  • In-memory caching only.
  • Credentials are never logged or written to stderr.
  • Strict protocol and authorization-value validation.
  • Fail-closed behavior for matched provider failures.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions