Skip to content

Git credential request handling

The Git credential endpoints share a single request handler. This page describes the order in which a request is evaluated, and the conditions that produce credentials, a successful empty response, or an error.

It applies to:

Chinmina Bridge implements the Git credential helper protocol natively, keeping most of the logic out of any credential helper that exercises it. The protocol, host, and path properties in a request body describe the request’s target: the URL Git is asking about.

The supplied target repository is not a security boundary. It exists so Chinmina Bridge works within Git’s credential chain, where a 200 carrying no properties reads as a decline and Git moves on to the next configured helper for the repository. Git ignores a helper’s exit status, so an error in place of that response does not stop the chain; it only adds the failure to the build log.

Authorization is determined by three controls:

  • The Authorization header carries the Buildkite OIDC token which establishes the calling pipeline’s identity. This is validated before the body is read.
  • The profile’s match rules decide whether that pipeline may use the profile it named.
  • The profile’s permissions and the reach of the GitHub App installation bound what the issued token can do.

A target repository allows the request to be filtered according to the Git credential helper protocol, so credential helpers function without spurious error messages and the service telemetry stays clean. Caller-scoped organization profiles are the only case where the target selects a repository, and the selected repository must be one that the GitHub application has access to.

The Git-specific logic of these specialized handlers keeps more complicated processing and quality testing central, rather than distributed to all credential helper implementations.

The /token and /organization/token/{profile} endpoints return the same tokens in a generic JSON format and accept no Git credential context, so none of the rules on this page apply to them.

Each stage either answers the request or passes it to the next.

Git credential request classification
StageOutcome
Routing404 when the method and path do not match a route
Authentication401 when the OIDC token is missing or invalid
Property parsing413 when the body exceeds 20 KB, 500 on any other read failure
Protocol and host required200 with no credentials, or 400
GitHub repositories only200 with no credentials
Profile resolution400, including an unresolved caller-scoped repository; 404 or 500 for other resolution failures
Profile match rules403 when the caller may not use the profile
Repository matchingContinues on a match; 200 with no credentials on mismatch; 500 if the Buildkite repository lookup fails
Token issuance200 with credentials; 403 when GitHub refuses; 500 on other upstream failures

Authentication runs before the body is touched. The 20 KB body limit is installed ahead of authentication, but only takes effect when the handler reads the body, so an oversized request without a valid token returns 401 rather than 413.

Both checks on the requested target are settled before any profile lookup, cache access, Buildkite repository lookup, or GitHub token mint. Their outcome does not vary with the profile named, the cache state, or either upstream service.

The request body follows Git’s credential helper input format. Parsing is tolerant:

  • Each line is split at the first =. The remainder of the line is the value.
  • The first empty line terminates input. Anything after it is ignored.
  • Lines with no =, or with an empty key, are discarded.
  • A repeated key keeps the last value supplied.
  • Properties Chinmina Bridge does not use are ignored.

A malformed line does not fail the request. A failure to read the body returns 413 when the body exceeded 20 KB, and 500 otherwise. Both carry the Chinmina-Denied header.

Requests generated by Git include protocol and host. A client calling the endpoint directly may omit all three target properties: protocol, host, and path.

A request that supplies no target at all is well formed but unfulfillable, and returns 200 with no credentials. A request that supplies part of a target must supply all of the required parts.

path is optional: Git omits it when credential.useHttpPath is false. An omitted property and an explicitly empty one (host=) are equivalent everywhere.

protocol / hostHost omittedHost emptygithub.comOther host
Protocol omittedEmpty-target ruleEmpty-target rule400400
Protocol emptyEmpty-target ruleEmpty-target rule400400
https400400Continue200 empty
Other protocol400400200 empty200 empty

Empty-target rule: 200 with no credentials when path is also omitted or empty, and 400 otherwise.

Consequences:

  • An empty body returns 200 with no credentials, as does a body containing only properties that supply no target, such as username and password.
  • A path on its own returns 400. path=owner/repository and path=/ both supply target information, and neither substitutes for a missing protocol or host.
  • A request with an unsupported protocol and no host returns 400. Completeness is checked before destination support.
  • No whitespace is trimmed. A property valued with a space counts as supplied.

Chinmina Bridge issues credentials for exactly one destination: protocol=https with host=github.com. The supplied values are compared literally. Any other non-empty pair returns 200 with no credentials, which allows another credential helper to answer for that destination.

No normalisation is applied, so none of the following are accepted spellings of the supported pair:

Property valueReason
protocol=HTTPSCase is significant
host=GitHub.comCase is significant
host=github.com:443Ports are not removed
host=github.com.Trailing dots are not removed
host= plus spacesWhitespace is not trimmed

An unsupported destination is classified before the requested profile is resolved, so the profile is never consulted. An unknown profile name, or one the caller may not use, still returns 200 with no credentials.

Once the destination is supported, the outcome depends on the path property and the kind of profile named by the request.

Requested pathPipeline profileStatic organization profileCaller-scoped organization profileWildcard organization profile
Omitted or empty200 empty200 empty400Credentials
Root (/)200 empty200 empty400Credentials
Owner only, with or without slashes200 empty200 empty400Credentials
A repository the profile coversCredentialsCredentialsCredentialsCredentials
A repository the profile does not cover200 empty200 emptyCredentialsCredentials

A pipeline profile covers the repository Buildkite reports for the authenticated pipeline, and the reconstructed request URL must equal that repository’s URL. An SSH-form repository is translated to HTTPS before the comparison, which is otherwise exact. A failed Buildkite lookup returns 500 rather than an empty response.

A static organization profile covers the repositories named in its repositories list, and the repository name from the request must appear there. Only the name is compared, with a .git suffix removed and the owner ignored. The GitHub App installation’s reach determines which repositories the token can actually be used for.

A caller-scoped profile covers every repository derivable from path, and scopes its token to the name derived. A path that derives no name returns 400 with repository scope is required for this profile. Omitted, empty, root, and owner-only paths derive nothing, as do paths whose repository component contains a further /, whitespace, or control characters.

Every repository the installation can reach is covered, including a request that supplies no path at all. A host-only request returns credentials scoped to the whole installation.

The repository-scope query parameter is read on POST /organization/token/{profile} only. It is ignored by the Git credential endpoints, which derive scope from path.

Credential properties in Git’s output format. The individual endpoint pages document the properties returned.

Content-Type: text/plain, Content-Length: 0, an empty body, and no Chinmina-Denied header. Git treats this as “this helper has nothing for that URL” and moves on to the next configured helper. Empty target, unsupported destination, and repository mismatch all produce the same response.

Content-Type: text/plain; charset=utf-8 and a body of Bad Request, with no Chinmina-Denied header. This shape is unique to an incomplete target.

An empty body and a caller-facing reason in the Chinmina-Denied header, with the full cause recorded in the audit log. Authentication failures are the exception: the middleware answers with a JSON body and a WWW-Authenticate header. The endpoint pages list the statuses and their conditions.

Every request is audited, including those answered before profile resolution.

  • A response carrying no credentials records skipped(success): no credentials for requested context as its error value. The value records a success, and applies equally to an empty target, an unsupported destination, and a repository mismatch.
  • The requested repository is recorded as the URL reconstructed from the supplied properties, including for an unsupported destination. An empty target has no requested repository, and an incomplete target records none.
  • A request answered before profile resolution records the profile name it asked for, and no resolved profile, App, or token metadata.