Skip to content

Git Credential Managers: Secure Authentication

Lesson 10 of 11Intermediate10 min readModern Git Workflows · Modern Git ProductivityVerified: Git 2.43.0 on Ubuntu 24.04; behaviour checked against the current gitcredentials documentation

Git does not store credentials itself. It delegates to credential helpers — external programs that supply, save and erase credentials on request.

Understanding that separation explains everything else: why some setups prompt on every push, why others never do, and why one particular helper writes your token to a plain-text file.

When Git needs credentials for an HTTPS remote, it works through a sequence:

  1. A configured credential helper, if one can supply them.
  2. GIT_ASKPASS, if set.
  3. core.askPass, if configured.
  4. SSH_ASKPASS, if set.
  5. A terminal prompt, as the fallback.

A helper is just a program. credential.helper = manager makes Git run git credential-manager, passing it one of three operations on standard input:

OperationMeaning
getSupply credentials for this host, if you have them
storeRemember these credentials
eraseForget credentials for this host

That is the entire contract. Any program implementing it can be a helper, which is why platform keychains, enterprise single sign-on systems and password managers can all plug into Git.

You can drive it directly, which is a useful way to see the mechanism:

Terminal window
printf 'protocol=https\nhost=github.com\n\n' | git credential fill

Git asks each configured helper in turn, then prompts if none can help.

HelperPlatformStorageSecurity
managerCross-platform, bundled with Git for WindowsOS credential storeGood
osxkeychainmacOSKeychainGood
libsecretLinux with a keyringKeyring via D-BusGood
wincredWindowsWindows Credential ManagerGood
cacheAnyMemory only, time-limitedGood
storeAnyPlain-text filePoor
Terminal window
# In-memory, expires after an hour — no disk storage at all
git config --global credential.helper 'cache --timeout=3600'

For persistent storage, the libsecret helper integrates with your desktop keyring. Distributions ship its source rather than a binary, so it is built once:

Terminal window
sudo apt install build-essential pkg-config libsecret-1-dev
sudo make --directory=/usr/share/doc/git/contrib/credential/libsecret
git config --global credential.helper \
/usr/share/doc/git/contrib/credential/libsecret/git-credential-libsecret

Check /usr/share/doc/git/contrib/credential/ on your own system — the path is set by your git package and differs between distributions.

On a headless server with no keyring, cache is the right choice. Do not fall back to store.

Terminal window
git config --global credential.helper store

This writes credentials to ~/.git-credentials in plain text:

https://username:ghp_examplenotarealtoken@github.com

Git’s own documentation describes this helper as inherently insecure. The specific problems:

  • Anything that can read your home directory can read your tokens. Other processes, backup software, synchronisation tools, a misconfigured container mount.
  • The file is easy to include by accident in a backup, a support bundle, or a shared home directory.
  • Nothing expires. A token written in 2023 is still there.
  • There is no audit trail. Nothing records that the file was read.

If you have used store previously, the file persists after you change helpers. Remove it deliberately:

Terminal window
rm ~/.git-credentials

Then rotate anything that was in it, on the assumption that it may have been exposed.

Different remotes often need different authentication. Credential settings can be scoped by URL:

[credential "https://github.com"]
helper = manager
username = your-username
[credential "https://git.internal.example.com"]
helper = cache --timeout=7200

By default Git treats all repositories on a host as sharing credentials — https://example.com/team-a/repo and https://example.com/team-b/repo use the same entry. To distinguish them:

Terminal window
git config --global credential.useHttpPath true

This matters when you have different tokens for different projects on one host, and is worth knowing about because the default behaviour is occasionally surprising.

SSH does not use the credential subsystem. Nothing on this page applies to it.

With SSH you generate a key pair, upload the public half to your hosting provider, and authenticate with the private half:

Terminal window
ssh-keygen -t ed25519 -C "you@example.com"

That writes ~/.ssh/id_ed25519 (private — never share, never commit) and ~/.ssh/id_ed25519.pub (public — safe to upload).

HTTPS + credential helperSSH keys
Secret storedToken, in a credential storePrivate key file
Managed byGit’s credential subsystemssh-agent and SSH config
Works behind restrictive firewallsUsuallySometimes blocked
Per-repository credentialsWith useHttpPathPer key, via SSH config
RotationRegenerate the tokenGenerate a new key pair
Passphrase protectionN/AYes — and you should use one

Neither is universally better. HTTPS with a good helper is simpler to set up and works everywhere. SSH avoids token expiry and integrates with agent forwarding.

Three layers get conflated here, and separating them makes troubleshooting much easier:

Git provides the credential subsystem: a protocol for asking helpers for credentials. It does not know what a token is, or which provider you are using.

The hosting platform decides what counts as a valid credential — a personal access token with particular scopes, an OAuth flow, an app installation token — and how long it lasts. Those rules are the platform’s, not Git’s.

SSH is a transport with its own authentication, entirely outside both.

So “Git authentication failed” is rarely a Git problem. It is usually an expired or insufficiently-scoped token, which is a platform matter.

Automation authenticates differently from people, and the differences matter.

Use a dedicated credential. A CI job should not use a developer’s personal token. If that person leaves or rotates their token, the pipeline breaks; worse, the job inherits all of their access.

Prefer short-lived, scoped tokens. Most platforms can issue a token scoped to one repository for the duration of a single job. That is far better than a long-lived secret stored in configuration.

Never write credentials to a file in the workspace. A .git-credentials file inside a checked-out repository is one careless git add away from being committed.

Pass tokens through the environment, and let the platform’s secret store hold them:

Terminal window
git clone "https://x-access-token:${CI_TOKEN}@github.com/org/repo.git"

Rotate on any suspicion, and prefer platforms that let you scope tokens to a single repository and a single permission.

Prompted on every operation. No helper is configured, or it cannot store anything:

Terminal window
git config --get credential.helper

Empty output means no helper.

Authentication fails after rotating a token. A stale credential is cached. Erase it:

Terminal window
printf 'protocol=https\nhost=github.com\n\n' | git credential reject

Or remove the entry through your platform’s credential store UI.

Which credential is Git using?

Terminal window
printf 'protocol=https\nhost=github.com\n\n' | git credential fill

This prints what the helpers supply — including the password field, so do not run it where the output could be captured or shared.

Trace the exchange:

Terminal window
GIT_TRACE=1 GIT_CURL_VERBOSE=1 git fetch 2>&1 | head -40

Verbose and useful, and it may include sensitive headers — review before pasting anywhere.

Permission denied over SSH:

Terminal window
ssh -T git@github.com

Most providers respond with a message identifying you if the key is recognised.

Wrong account used. With several accounts on one host, either use credential.useHttpPath with HTTPS, or distinct SSH hosts in ~/.ssh/config:

Host github-work
HostName github.com
IdentityFile ~/.ssh/id_ed25519_work

Then clone with git@github-work:org/repo.git.

Both are legitimate. The choice usually comes down to constraints rather than preference.

Choose HTTPS with a credential helper when:

  • Your network blocks outbound SSH, which is common on corporate and guest networks.
  • You want the platform’s authentication flow, including multi-factor and single sign-on.
  • You work on many machines and prefer not to distribute private keys.
  • Your organisation issues short-lived tokens centrally.

Choose SSH when:

  • You dislike token expiry interrupting work.
  • You already use SSH keys for servers and want one mechanism.
  • You need agent forwarding.
  • Your provider’s token lifetimes are shorter than your patience.

Use both where it makes sense: SSH for repositories you work in daily, HTTPS for CI and for one-off clones. Nothing prevents different remotes using different protocols, and insteadOf rules can rewrite one to the other per host:

[url "git@github.com:"]
insteadOf = https://github.com/

Least privilege. Give a token only the scopes it needs. A token that can only read one repository is a far smaller problem if it leaks.

Short lifetimes. Prefer tokens that expire. Rotation stops being an event you have to remember.

Never commit credentials. If one is committed, rotate it immediately — removing the file or rewriting history does not un-leak it. See Editing Commit History.

Separate credentials for automation. CI should use its own credential with its own scopes, not a developer’s personal token.

Protect private keys with a passphrase.

Revoke on any suspicion. Revocation is cheap; investigating a compromise is not.

Using store because it is the first result. Plain text on disk. Use cache or a keychain.

Leaving ~/.git-credentials behind after switching helpers. Delete it and rotate.

Committing a token. Rotate first, then clean up.

Assuming SSH uses credential helpers. Different mechanism entirely.

Unprotected SSH private keys.

Using one long-lived token everywhere. One leak compromises everything.

Disabling http.sslVerify to fix a certificate error. Configure the CA for that host instead.

Pasting git credential fill output into a bug report. It contains the password.

Git is a doorman who does not keep keys.

When a door needs unlocking, it asks the keyholder — your credential helper — and passes along whatever it is given. It never inspects, validates or stores anything itself.

Which keyholder you appoint determines the security. A keychain keeps keys in a safe. The store helper keeps them on a sticky note by the door.

  • Git delegates authentication to credential helpers implementing get, store and erase.
  • manager, osxkeychain, libsecret and wincred use encrypted OS credential stores.
  • cache keeps credentials in memory with a timeout — the right choice with no keychain available.
  • store writes plain text to ~/.git-credentials and should be avoided.
  • Helpers can be configured per host; credential.useHttpPath distinguishes repositories on one host.
  • SSH authentication is a separate mechanism that does not use this subsystem.
  • git credential fill and reject inspect and clear stored credentials.
  • Scope and lifetime limit damage; storage security alone does not.

No real credentials required — this uses a local repository so nothing sensitive is involved.

  1. Check what is configured:

    Terminal window
    git config --get credential.helper
    git config --show-origin --get credential.helper
  2. See the subsystem run. In a repository with an HTTPS remote:

    Terminal window
    printf 'protocol=https\nhost=example.com\n\n' | git credential fill

    With no helper and no stored credential, Git prompts. Press Ctrl+C to cancel.

  3. Configure the memory cache:

    Terminal window
    git config --global credential.helper 'cache --timeout=60'
  4. Confirm no file is created. After using it, check that ~/.git-credentials does not exist:

    Terminal window
    ls -la ~/.git-credentials 2>&1
  5. Inspect a per-host configuration:

    Terminal window
    git config --global credential."https://example.com".username demo-user
    git config --get-regexp '^credential\.'
  6. Practise clearing a credential:

    Terminal window
    printf 'protocol=https\nhost=example.com\n\n' | git credential reject
  7. Clean up:

    Terminal window
    git config --global --unset credential.helper
    git config --global --remove-section 'credential.https://example.com'

Step 4 is the point of the exercise: cache genuinely never touches disk, which is what makes it the safe fallback when no keychain exists.

Credentials establish that you may push. Signing establishes who wrote a commit — a different question, with a carefully bounded answer.