Ana has her remote registered. She types git push origin main full of enthusiasm and gets this:

Username for 'https://git.example.com':

A prompt waiting for something she is not quite sure about. And if she types the password she uses to log into the web interface, she will very likely get a rejection along the lines of Support for password authentication was removed.

This wall stops a lot of people on their first contact with a remote repository, and it is a shame, because the problem has nothing to do with Git: it is a problem of identity. Registering a URL does not grant permission to write to it. Somebody has to prove to the server that they are who they claim to be.

This lesson takes that wall down once and for all. We will see why cloning a public repository asks for nothing while pushing does, the two authentication mechanisms in use today — tokens over HTTPS and SSH keys —, how to generate a key with ssh-keygen, and how to make the operating system remember your credentials so that you do not type them forty times a day. It is the lesson Carla needs before writing her first line, because she is coming in from Windows 11 and the credential manager works differently there.

Contents

  1. Why a public clone asks for nothing and a push does
  2. Authentication versus authorisation
  3. HTTPS with a personal access token
  4. SSH: the key pair
  5. Generating the key with ssh-keygen -t ed25519
  6. Uploading the public key, testing the connection and using the agent
  7. ~/.ssh/config: several identities on the same machine
  8. HTTPS versus SSH: a comparison table
  9. Credential managers by operating system
  10. Typical errors and how to diagnose them

  1. Why a public clone asks for nothing and a push does

The answer is simple and clears up a great deal of confusion: they are operations with different permission requirements.

Operation What it does Public repository Private repository
git clone, git fetch, git pull Read objects and references No credentials Credentials
git push Write: modify the server's references Credentials always Credentials

A public repository is, by definition, readable by anybody. Downloading the code of an open project requires no identification, in the same way that reading a web page requires no account.

Writing is another story. When you push, you are asking the server to modify its references: to make refs/heads/main point at a different commit. That changes what everyone will see when they clone from that moment on. No sensible server allows a stranger to do that.

And there is a subtlety that surprises a lot of people: your Git identity and your server identity are different things.

git config user.name
git config user.email

What you configured back in lesson 01-06 is commit metadata: text recorded inside the object and shown by git log. It authenticates nothing. Anyone can put any name and any address into their commits; Git does not verify it.

Authentication happens at another layer: at the transport layer, when your Git talks to the server. These are two independent identities, and mixing them up is a bad idea:

user.name / user.email Access credentials
Where it lives In Git's configuration In the credential manager or in ~/.ssh/
What it is for Signing the authorship of a commit Proving who you are to the server
Does anybody verify it No Yes, the server, on every connection
When it comes into play When you run git commit When you run fetch, pull or push

(There is a way to link the two for real, by signing commits cryptographically. That is a security topic and it is covered in lesson 08-05.)

  1. Authentication versus authorisation

Two words that get confused daily and that produce very different errors:

  • Authentication: who you are. Proving your identity to the server.
  • Authorisation: what you may do. The permissions that identity holds over that particular repository.

You can be perfectly authenticated and still be turned away:

remote: Permission to team/task-manager.git denied to carla.
fatal: unable to access 'https://git.example.com/team/task-manager.git/': The requested URL returned error: 403

Look at the message: the server knows you are Carla. Authentication worked. What fails is authorisation: Carla has not been given write access to that repository, or her token lacks the necessary scope.

Telling the two cases apart saves a lot of time:

Symptom Layer that fails What to check
Permission denied (publickey) Authentication (SSH) The key: whether it exists, whether it is loaded, whether it is uploaded to the server
Authentication failed (HTTPS) Authentication The token: whether it is valid, whether it has expired, whether it is stored correctly
403 Forbidden, Permission to … denied to <user> Authorisation Your account's permissions on the repository; the token's scopes
Repository not found Both, ambiguously Many platforms return this for private repositories you cannot access, so as not to reveal that they exist

That last case is especially treacherous: Repository not found does not necessarily mean you mistyped the URL. It can mean "it exists, but not for you".

  1. HTTPS with a personal access token

What a PAT is and why it replaced the password

A personal access token (PAT) is a long string generated by the platform that works as a password for one specific purpose. It looks something like this (fictitious, obviously):

glpat-7fK2mQx9RtVb3Ln8ZwYc

For years, HTTPS authentication used the account's username and password. GitHub withdrew that option in August 2021 and the other platforms followed suit. The reasons are solid:

  • Limited scope. Your password opens the whole account: repositories, settings, billing, deletion. A token can grant read-only access to one specific repository.
  • Individually revocable. If a token leaks, you delete it and only what used it breaks. Changing the password forces you to reconfigure everything.
  • Compatible with two-factor authentication. If your account has a second factor, there is no way to enter it at a Git prompt. The token resolves that dead end.
  • Expiry. A token can expire in 30, 60 or 90 days; a password lives until somebody remembers to change it.
  • Auditable. The platform records which token did what and when it was last used.

How to create one, and with which scopes

The exact procedure varies by platform, but the shape is always the same: account settings → access tokens → create → choose a name, an expiry and some scopes.

Scopes are the important part. Grant the bare minimum:

Need Typical scope What it allows
Cloning and fetching private repositories read_repository / repo:read Read only
Pushing write_repository / repo Reading and writing code
One specific repository only A project-scoped token Access limited to that repository
Continuous integration The minimum the job needs See module 7

What you almost never need: administration scopes, user-management scopes, repository-deletion scopes or access to the organisation's settings. A deployment token whose only job is to clone should not be able to delete anything.

One operational tip: create one token per machine, rather than one shared between your laptop, your home computer and the continuous integration server. If you lose the laptop, you revoke that token and everything else keeps working.

How to use it

The token goes where the password would go:

git push origin main
Username for 'https://git.example.com': ana.ferrer
Password for 'https://[email protected]':

The token goes at that second prompt, not the account password. Since it is not echoed as you type, pasting it from the clipboard is the convenient route.

What you must never do is embed it in the URL:

# WRONG: do not do this
git remote add origin https://ana:[email protected]/team/task-manager.git

That token ends up written in plain text in .git/config, appears in your shell history, sneaks into screenshots and shows up in any git remote -v you run in front of somebody. That is what the credential managers in section 9 are for.

  1. SSH: the key pair

The alternative to HTTPS is SSH, and it rests on a different concept: instead of a shared secret, it uses an asymmetric key pair.

  • The private key (~/.ssh/id_ed25519) stays on your machine and never leaves it. It is the equivalent of your physical key.
  • The public key (~/.ssh/id_ed25519.pub) is uploaded to the server. It is the equivalent of the lock: it can be handed around without risk.

The mathematical property that makes it work is that whatever the public key verifies can only have been produced by the private one, and the private one cannot be derived from the public one.

sequenceDiagram
    participant C as Carla's laptop
    participant S as git.example.com

    C->>S: I want to connect as user "git"
    S->>C: Challenge: sign this random data
    Note over C: Signs with the PRIVATE key<br/>(which never leaves the disk)
    C->>S: Here is the signature
    Note over S: Verifies with the PUBLIC key<br/>Carla uploaded earlier
    S->>C: Verified. Access granted

The advantage over the token is plain: the secret never travels over the network. Not even encrypted. All that is transmitted is a signature over a different piece of random data on every connection, useless to an attacker who captures it.

The key types

Type Status Comment
Ed25519 Recommended Modern, fast, short keys (68 characters), excellent security
RSA 4096 Acceptable Secure, but long keys and slower; useful on old systems that do not support Ed25519
RSA 2048 Discouraged No longer considered sufficient
ECDSA Discouraged Doubts about the standard curves; Ed25519 beats it on every count
DSA Forbidden Obsolete and removed from OpenSSH

Use Ed25519 unless you run into an old server that rejects it, in which case rsa with 4096 bits.

  1. Generating the key with ssh-keygen -t ed25519

Carla, on her Windows 11 machine, opens Git Bash (which she installed in lesson 01-02 and which ships with all the OpenSSH tools). The commands are identical on Linux, macOS and Git Bash:

ssh-keygen -t ed25519 -C "[email protected]"

A breakdown of the options:

  • -t ed25519: the key type. This is the argument that matters.
  • -C "…": a comment appended to the end of the public key. It helps you identify the key in the server's list once you have four keys from four machines. It can be your email address, but something like "carla-windows-laptop" is even better, because what you want to know is which machine that key belongs to.

The dialogue:

Generating public/private ed25519 key pair.
Enter file in which to save the key (/c/Users/carla/.ssh/id_ed25519):

Press Enter to accept the default path. Only change it if you are going to have several keys, a case we will look at in section 7.

Enter passphrase (empty for no passphrase):
Enter same passphrase again:

This is where you should stop and think. The passphrase encrypts the private key on disk. If somebody steals your laptop or copies the file, the key is useless to them without it.

With a passphrase Without a passphrase
Security if the file is stolen The key is useless without the phrase Immediate access to your repositories
Day-to-day convenience Typed once per session (with ssh-agent) Never typed
Use in unattended automation Awkward The usual choice, with a heavily restricted key
Recommendation Always set one on personal machines Only on read-only deployment keys

Set one. With ssh-agent, the real cost is typing it once when you boot the computer.

The result:

Your identification has been saved in /c/Users/carla/.ssh/id_ed25519
Your public key has been saved in /c/Users/carla/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:9xK2mQ7RtVb3Ln8ZwYcF4dJ6hP1sT5uA2eB9gN0kM3o [email protected]

Two files. Let us see what they hold:

ls -l ~/.ssh/
-rw-------  1 carla  carla   464 Aug  1 09:12 id_ed25519
-rw-r--r--  1 carla  carla   103 Aug  1 09:12 id_ed25519.pub

Look at the permissions, which are part of the security mechanism:

  • The private key is -rw------- (600): only you can read it. With more open permissions, OpenSSH would refuse to use it.
  • The public key is -rw-r--r-- (644): readable by anybody, and that is fine.
cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK7Rt2mQx9VbLn8ZwYcF4dJ6hP1sT5uA2eB9gN0kM3o [email protected]

Three parts: the type, the key itself and the comment. This line is what gets uploaded to the server, and it can be published without any risk at all.

And the private one, with the obvious warning:

head -2 ~/.ssh/id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDl...

This content is not shown, not copied and not sent to anybody. Ever. Not to a colleague, not to technical support, not pasted into a chat. If you are ever unsure which of the two files to upload, the rule never fails: the one ending in .pub.

  1. Uploading the public key, testing the connection and using the agent

Uploading the public key

You copy the contents of the .pub file and paste it into the platform (account settings → SSH keys → new key). To get it onto the clipboard:

# Linux (with xclip installed)
xclip -selection clipboard < ~/.ssh/id_ed25519.pub

# macOS
pbcopy < ~/.ssh/id_ed25519.pub

# Windows with Git Bash
clip < ~/.ssh/id_ed25519.pub

A common warning: no line break must survive in the middle when you paste. The key is one single long line; if the editor breaks it, it will not work.

Testing the connection with ssh -T

Before attempting any Git command, check that the channel works:

The -T option means "I do not want an interactive terminal": I am only checking authentication.

The first time, this appears:

The authenticity of host 'git.example.com (192.0.2.42)' can't be established.
ED25519 key fingerprint is SHA256:p2Q9xL4mR7tVc3Nb8ZwYeF6dJ1sK5uA0eB7gN2kM9o.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

Do not answer yes out of habit. That message is telling you your computer does not know that server yet and is asking you to confirm it is the genuine one. Platforms publish their fingerprints in their documentation; compare before accepting. It is your only defence against a man-in-the-middle attack on that first connection.

Once accepted, the fingerprint is stored in ~/.ssh/known_hosts and you will not be asked again.

If all goes well:

Hi carla! You've successfully authenticated, but I do not provide shell access.

That message is a complete success, even though it says shell access is not provided. That is exactly what should happen: the git user on a repository server is not there to open a session, only to speak Git.

ssh-agent: typing the passphrase just once

With a passphrase, every operation would ask for the phrase. The agent keeps it in memory for the session and answers on your behalf.

# Start the agent (if it is not already running)
eval "$(ssh-agent -s)"
Agent pid 5842
# Load the key: it will ask for the passphrase once
ssh-add ~/.ssh/id_ed25519
Enter passphrase for /home/carla/.ssh/id_ed25519:
Identity added: /home/carla/.ssh/id_ed25519 ([email protected])
# See which keys it has loaded
ssh-add -l
256 SHA256:9xK2mQ7RtVb3Ln8ZwYcF4dJ6hP1sT5uA2eB9gN0kM3o [email protected] (ED25519)

From here on, every push and pull works without typing anything.

Each system has its own way of automating this:

# Linux: add at the end of ~/.bashrc or ~/.zshrc
if [ -z "$SSH_AUTH_SOCK" ]; then
  eval "$(ssh-agent -s)" > /dev/null
  ssh-add ~/.ssh/id_ed25519 2>/dev/null
fi
# macOS: stores the passphrase in the Keychain and loads it by itself
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
# macOS: and so that it loads on every session, in ~/.ssh/config
Host *
  UseKeychain yes
  AddKeysToAgent yes
  IdentityFile ~/.ssh/id_ed25519

On Windows 11, the ssh-agent service ships with the system but starts out disabled. From PowerShell as administrator:

Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent

And then, from Git Bash:

ssh-add ~/.ssh/id_ed25519

One detail for Carla: if she uses Git Bash's ssh together with the system agent, the two may not see each other. The simple fix is to use the ~/.bashrc block above, which starts an agent of Git Bash's own.

  1. ~/.ssh/config: several identities on the same machine

A very real situation, and precisely Carla's: in lesson 01-05 we saw that she works in two contexts, the example.com one and another company's, with different email addresses. If each of them also requires a separate account and a separate key, SSH has to be told which one to use in each case.

That is solved in ~/.ssh/config:

# Work account
Host git.example.com
    HostName git.example.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes
    AddKeysToAgent yes

# The other company's account: an invented alias
Host git-othercompany
    HostName git.othercompany.example
    User git
    IdentityFile ~/.ssh/id_ed25519_othercompany
    IdentitiesOnly yes
    AddKeysToAgent yes

An explanation of each directive:

Directive What it does
Host The alias you type in the URL. It can be invented
HostName The real server to connect to
User The SSH user: on Git platforms it is almost always git
IdentityFile Which private key to use for this destination
IdentitiesOnly yes Use that key only, not every one you have loaded
AddKeysToAgent yes Load the key into the agent on first use

IdentitiesOnly yes deserves a comment, because it heads off a baffling failure. Without it, SSH offers all your keys one after another until one works; if you have five, the server may cut the connection for too many attempts and you get a Too many authentication failures that looks entirely unrelated to the real cause.

The invented alias trick is what makes this file powerful. Look at Host git-othercompany: that name exists in no DNS anywhere. But Carla can now write:

git clone git@git-othercompany:projects/billing.git

And SSH translates git-othercompany into git.othercompany.example with the right key. The same mechanism handles two accounts on the same platform, the classic case of a personal account and a work account on the same GitHub:

Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_personal
    IdentitiesOnly yes

Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_company
    IdentitiesOnly yes
# Personal repository
git clone git@github-personal:carla/my-blog.git

# Company repository
git clone git@github-work:example-ltd/task-manager.git

Same real server, two identities, with no ambiguity at all.

And to check which key will actually be used:

ssh -T -v [email protected] 2>&1 | grep -i "offering\|identity file"
debug1: identity file /home/carla/.ssh/id_ed25519_work type 3
debug1: Offering public key: /home/carla/.ssh/id_ed25519_work ED25519

The -v (verbose) option is the definitive diagnostic tool for any SSH problem.

File permissions, which are strict here too:

chmod 600 ~/.ssh/config
chmod 700 ~/.ssh

  1. HTTPS versus SSH: a comparison table

Both options are valid and both are used on a massive scale. The choice depends on the context:

Criterion HTTPS + token SSH
Initial ease High: paste a token and you are done Medium: generate a key, upload it, test it
Corporate firewalls Excellent: uses port 443, open everywhere Awkward: port 22 is usually closed
Day-to-day use Transparent with a credential manager Transparent with ssh-agent
The secret travels over the network Yes (inside the TLS encryption) No, never
Expiry Yes: the token has to be renewed every 30-90 days Does not expire
Permission granularity High: scopes per token Low: the key gives whatever your account gives
Several identities Awkward: the manager stores one per server Simple with ~/.ssh/config
Automation and CI Preferred: tokens with minimal scope and an expiry Possible with deployment keys
Revocation Immediate from the web Immediate by deleting the key
Read-only public repositories Ideal: no credentials Unnecessary
Behind a proxy Easily configured in Git Requires additional SSH configuration

Practical recommendation

  • Everyday work machine, always the same accountSSH. Configure it once and forget it: it does not expire, it does not need renewing and it does not interrupt.
  • Corporate network with port 22 closedHTTPS + token. (Some platforms offer SSH over port 443 as an alternative.)
  • Continuous integration, containers, servers → a token with the minimum scope and a short expiry, or a read-only deployment key.
  • Cloning a public project to read itHTTPS with no credentials.
  • Several accounts on the same machineSSH with ~/.ssh/config.

And one reassuring fact: switching from one to the other is a set-url, as we saw in the previous lesson:

# From HTTPS to SSH
git remote set-url origin [email protected]:team/task-manager.git

# From SSH to HTTPS
git remote set-url origin https://git.example.com/team/task-manager.git

You lose nothing: the commits are the same, the references are the same and only the route changes.

  1. Credential managers by operating system

With HTTPS one practical problem remains: nobody is going to type a 40-character token on every push. That is what credential.helper is for, which already appeared in lesson 01-06 and which we can now understand fully.

A credential helper is an external program that Git asks "do you have credentials for this server?" before showing the prompt, and tells "store these" when access works.

sequenceDiagram
    participant G as git push
    participant H as credential.helper
    participant A as System store
    participant S as Server

    G->>H: Credentials for git.example.com?
    H->>A: Query (encrypted)
    A-->>H: user + token
    H-->>G: Here they are
    G->>S: Authentication
    S-->>G: Correct
    Note over G,A: If they fail, Git asks the user<br/>and tells the helper to store the new ones

The one for each system

Linux (Ana, Ubuntu) — libsecret

It integrates with the desktop keyring (GNOME Keyring, KWallet), which stores secrets encrypted and unlocks with your session:

sudo apt install libsecret-1-0 libsecret-1-dev

# Compile the helper that ships with Git
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

On some distributions it comes precompiled and installing the relevant package is enough.

macOS (Bruno) — osxkeychain

It is included with Git on macOS; a single line:

git config --global credential.helper osxkeychain

Credentials are stored in the Keychain, encrypted and visible in the Keychain Access application, where they can be reviewed and deleted.

Windows 11 (Carla) — Git Credential Manager

This is the most interesting case, which is why we left it for last. GCM installs alongside Git for Windows and is configured by default:

git config --global credential.helper manager
# Check what is configured
git config --global --get credential.helper
manager

What GCM does goes beyond storing a string:

  • It stores credentials in the Windows Credential Manager, encrypted with your system user account.
  • It opens a browser window to authenticate against GitHub, GitLab or Azure DevOps, with support for two-factor authentication.
  • It can generate and renew tokens by itself, so Carla does not have to create any by hand.
  • It is cross-platform: it also exists for Linux and macOS, although on those systems the native ones are usually enough.

For Carla this means her first git clone of a private repository will open a browser window, she will log in with her account and her second factor, and from then on she will never see a prompt again. It is by far the smoothest experience of the three.

If she wants to check what is stored, she can open it from Git Bash:

# Opens the Windows Credential Manager
control /name Microsoft.CredentialManager

Git's entries appear in the "Generic Credentials" section with the git: prefix.

Summary, and store, the one you should not use

Helper System Where it stores Encrypted Recommendation
manager Windows (and cross-platform) Windows Credential Manager Yes The choice on Windows
osxkeychain macOS System Keychain Yes The choice on macOS
libsecret Linux Desktop keyring Yes The choice on Linux
cache Any RAM, temporarily Not applicable: it never touches the disk Acceptable: safe but forgetful
store Any ~/.git-credentials, plain text NO Avoid it

Why store writes in plain text, and when (not) to use it

It is tempting because it is so easy:

git config --global credential.helper store

And it works first time. The problem becomes obvious when you look at the file it creates:

cat ~/.git-credentials
https://ana.ferrer:[email protected]

Your token, legible, in a file in your home folder. Unencrypted, with no master password and no expiry. The permissions are 600, which protects it from other users of the same machine, but not from:

  • Any program running as your user, including a malicious dependency of your project.
  • A backup of your home folder that ends up on an external drive or in the cloud.
  • Somebody booting the computer from a USB stick or pulling the disk out.
  • A configuration file synchronised between machines by mistake.

When not to use it: on your laptop, on your desktop computer, on any machine with access to repositories that matter.

When it might make sense: on an ephemeral container or a disposable, isolated virtual machine, with a read-only token that expires in hours, where no keyring is available and the machine's lifetime is shorter than the token's. Even there, a better option almost always exists.

The reasonable alternative when there is no keyring is cache, which keeps credentials in memory for a limited time:

git config --global credential.helper 'cache --timeout=3600'

An hour without typing anything again, and nothing touching the disk. When you shut down, it is gone.

Everything to do with secret management, commit signing and handling credentials leaked into the history is developed in lesson 08-05. Here we have dealt with the operational side: being able to work comfortably without leaving secrets lying around.

  1. Typical errors and how to diagnose them

Permission denied (publickey)

[email protected]: Permission denied (publickey).
fatal: Could not read from remote repository.

This is the most common SSH error. It means: "The server did not accept any of the keys you offered it." The causes, in order of frequency:

# 1. Does the key exist?
ls -l ~/.ssh/
# 2. Is it loaded into the agent?
ssh-add -l

If it answers Could not open a connection to your authentication agent, the agent is not running:

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

If it answers The agent has no identities, it is running but empty: the ssh-add is missing.

# 3. Is the public key on the server the same one you have?
ssh-keygen -lf ~/.ssh/id_ed25519.pub
256 SHA256:9xK2mQ7RtVb3Ln8ZwYcF4dJ6hP1sT5uA2eB9gN0kM3o [email protected] (ED25519)

Compare that fingerprint with the one the platform shows in its list of keys. If they do not match, you uploaded a different one.

# 4. Are the permissions right?
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

OpenSSH silently ignores private keys with permissions that are too open, which produces this error with no clue whatsoever.

# 5. The definitive diagnosis
ssh -vT [email protected]

In the output you will see which keys it offers (Offering public key:) and how the server responds. It is the quickest way to discover you are offering the wrong key.

Host key verification failed

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

It means the server's key does not match the one you had stored in ~/.ssh/known_hosts. There are two possible explanations:

  1. Legitimate: the server has been reinstalled or migrated, or the platform has rotated its keys (which happens now and then and is always announced publicly).
  2. A man-in-the-middle attack: somebody is impersonating the server.

Do not delete the entry without checking. The correct procedure:

# 1. See which fingerprint the server is presenting now
ssh-keyscan git.example.com 2>/dev/null | ssh-keygen -lf -
256 SHA256:p2Q9xL4mR7tVc3Nb8ZwYeF6dJ1sK5uA0eB7gN2kM9o git.example.com (ED25519)
# 2. Compare with the official fingerprint published by the platform.
#    If and only if it matches, remove the old entry:
ssh-keygen -R git.example.com

# 3. Reconnect and accept the new one
ssh -T [email protected]

The mild variant of the same warning — The authenticity of host … can't be established — is the first connection: nothing is stored yet. There too it is worth comparing the fingerprint before accepting.

Other frequent errors

Message Usual cause Fix
Support for password authentication was removed You are using the account password Generate a token and use that instead
Authentication failed for 'https://…' Token expired, revoked or stored wrongly Delete the credential from the manager and authenticate again
403 Forbidden / Permission to … denied Authorisation, not authentication Review your permissions on the repository and the token's scopes
Repository not found Private repository with no access, or wrong URL Check the URL and your permissions
Connection timed out (port 22) A firewall blocking SSH Use HTTPS, or SSH over port 443 if the platform offers it
Too many authentication failures SSH is offering too many keys IdentitiesOnly yes in ~/.ssh/config
Load key … bad permissions Permissions too open on the private key chmod 600 ~/.ssh/id_ed25519

And one command that settles the "I do not know which credential Git is using" question:

GIT_TRACE=1 GIT_CURL_VERBOSE=1 git fetch origin 2>&1 | head -30

It shows the whole process, including which credential helper gets invoked. Use it only for debugging and do not share its output without reviewing it, because it can include sensitive headers.

Common Mistakes and Tips

Mistake 1: uploading the private key instead of the public one. It happens more than you would think. The rule is simple: you upload the file ending in .pub. If you have uploaded the private one by mistake, delete it from the server, remove the whole pair from your disk and generate a new one: that key is compromised for good.

Mistake 2: embedding the token in the remote's URL. It ends up in .git/config, in your shell history and in every screenshot. Use a credential manager.

Mistake 3: creating the key with no passphrase "to save time". A stolen laptop stops being a hardware problem and becomes a problem of access to all your repositories. With ssh-agent, the passphrase is typed once per session.

Mistake 4: confusing user.email with your access identity. Changing user.email fixes no authentication problem: it only changes the text shown in commits. They are different layers.

Mistake 5: accepting the server's fingerprint without looking at it. That prompt on the first connection is your only chance to spot an impersonation. Platforms publish their fingerprints; comparing them takes ten seconds.

Mistake 6: using the same token on five machines. When the time comes to revoke it, all five break at once and you will not know which one leaked. One token per machine and per purpose.

Mistake 7: reaching for credential.helper store by default. It leaves the token in plain text in your home folder. Use your system's native manager, or cache if none is available.

Tip 1: always test with ssh -T before blaming Git. If ssh -T git@server works, the problem is not in SSH. If it does not, ssh -vT tells you exactly which step fails.

Tip 2: put a useful comment on the key. -C "carla-windows-laptop" is infinitely more practical than an email address when, a year from now, you see four keys in the server's list and have to decide which one to revoke.

Tip 3: note down when your tokens expire. A token that expires on a Monday morning produces half an hour of bewilderment. Set yourself a reminder a few days beforehand.

Tip 4: IdentitiesOnly yes whenever you have more than one key. It avoids Too many authentication failures, one of the errors hardest to connect with its cause.

Tip 5: for automation, minimum scope and short expiry. A continuous integration token whose only job is to clone needs neither write nor administration permission.

Exercises

Exercise 1: generating and auditing a key pair

Without uploading anything to any server:

  1. Generate an Ed25519 pair at a specific path, ~/.ssh/course_practice, with a descriptive comment.
  2. Show the permissions of both files and explain why they differ.
  3. Show the public key's fingerprint.
  4. Load it into ssh-agent and verify that it is listed.
  5. Check that the fingerprint of the private key and that of the public one match. Explain why.
  6. Unload it from the agent and delete the files.

Exercise 2: configuring two identities

Carla needs to work with two accounts: the team's (git.example.com) and another company's (git.othercompany.example).

  1. Write the complete ~/.ssh/config for both, with separate keys.
  2. Explain what each directive does.
  3. Write the clone URL she would use for each repository.
  4. Give the command that would let her check which specific key is offered to each server.
  5. Explain what would happen if she left out IdentitiesOnly yes and had six keys loaded.

Exercise 3: diagnosis

For each of these messages, state: (a) whether the failure is one of authentication or of authorisation, (b) the two most likely causes and (c) the command or check you would start with.

  1. [email protected]: Permission denied (publickey).
  2. remote: Permission to team/task-manager.git denied to carla.
  3. remote: Support for password authentication was removed.
  4. WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
  5. ssh: connect to host git.example.com port 22: Connection timed out

Solutions

Solution 1:

# 1. Generate with a path and a comment
ssh-keygen -t ed25519 -f ~/.ssh/course_practice -C "git-course-practice"
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/carla/.ssh/course_practice
Your public key has been saved in /home/carla/.ssh/course_practice.pub
The key fingerprint is:
SHA256:4tR8mK2nQ7xVb3Lc9ZwYeF6dJ1sP5uA0eB7gN2kM3o git-course-practice

The -f option fixes the path and skips the "where shall I save it" dialogue.

# 2. Permissions
ls -l ~/.ssh/course_practice*
-rw------- 1 carla carla 464 Aug  1 10:22 /home/carla/.ssh/course_practice
-rw-r--r-- 1 carla carla 103 Aug  1 10:22 /home/carla/.ssh/course_practice.pub

The private one is 600: only its owner can read it, and OpenSSH refuses to use it with laxer permissions, because any other user on the system could copy it. The public one is 644 because it is designed to be handed around: it reveals nothing exploitable.

# 3. Fingerprint of the public key
ssh-keygen -lf ~/.ssh/course_practice.pub
256 SHA256:4tR8mK2nQ7xVb3Lc9ZwYeF6dJ1sP5uA0eB7gN2kM3o git-course-practice (ED25519)
# 4. Load and list
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/course_practice
ssh-add -l
Agent pid 6120
Enter passphrase for /home/carla/.ssh/course_practice:
Identity added: /home/carla/.ssh/course_practice (git-course-practice)
256 SHA256:4tR8mK2nQ7xVb3Lc9ZwYeF6dJ1sP5uA0eB7gN2kM3o git-course-practice (ED25519)
# 5. The two fingerprints
ssh-keygen -lf ~/.ssh/course_practice
ssh-keygen -lf ~/.ssh/course_practice.pub
256 SHA256:4tR8mK2nQ7xVb3Lc9ZwYeF6dJ1sP5uA0eB7gN2kM3o git-course-practice (ED25519)
256 SHA256:4tR8mK2nQ7xVb3Lc9ZwYeF6dJ1sP5uA0eB7gN2kM3o git-course-practice (ED25519)

Identical, and that is no accident. The fingerprint is a hash of the public key, and the public key can be derived from the private one (not the other way round). When ssh-keygen -lf is handed a private key, it extracts the public one and computes its fingerprint. That is why the fingerprint lets you match, without ambiguity, which private key on your disk corresponds to which public key on the server: it is exactly the check from section 10.

# 6. Clean-up
ssh-add -d ~/.ssh/course_practice
rm ~/.ssh/course_practice ~/.ssh/course_practice.pub

Solution 2:

# First, the two keys
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_work -C "carla-work-example"
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_othercompany -C "carla-othercompany"

1. The ~/.ssh/config file:

# task-manager team identity
Host git.example.com
    HostName git.example.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes
    AddKeysToAgent yes

# The other company's identity (invented alias)
Host git-othercompany
    HostName git.othercompany.example
    User git
    IdentityFile ~/.ssh/id_ed25519_othercompany
    IdentitiesOnly yes
    AddKeysToAgent yes
chmod 600 ~/.ssh/config

2. What each directive does:

  • Host: the name Carla types in the URL. In the first block it matches the real server; in the second it is an invented alias that exists in no DNS.
  • HostName: the real server to connect to. This is what resolves the alias.
  • User git: the SSH user. On repository platforms it is always git; the real user is worked out from the key you present.
  • IdentityFile: which private key to use with this destination.
  • IdentitiesOnly yes: use only that key, without offering the others.
  • AddKeysToAgent yes: load it into the agent on first use, to avoid repeating the passphrase.

3. The clone URLs:

# Team repository
git clone [email protected]:team/task-manager.git

# The other company's repository: the ALIAS is used
git clone git@git-othercompany:projects/billing.git

4. Checking which key is offered:

ssh -T -v [email protected] 2>&1 | grep "Offering public key"
debug1: Offering public key: /home/carla/.ssh/id_ed25519_work ED25519
ssh -T -v git@git-othercompany 2>&1 | grep "Offering public key"
debug1: Offering public key: /home/carla/.ssh/id_ed25519_othercompany ED25519

Each destination receives exactly one key, its own.

5. Without IdentitiesOnly yes: SSH would offer every key loaded in the agent, one by one, in an order you do not control. With six keys and a server whose MaxAuthTries is 6 (the usual value), it is very likely to run out of attempts before reaching the right one:

Received disconnect from 192.0.2.42 port 22:2: Too many authentication failures

An especially treacherous error, because the right key exists and is loaded: SSH simply never got round to trying it. On top of that, offering all your keys to every server you talk to is an unnecessary information leak: you reveal how many identities you have.

Solution 3:

1. Permission denied (publickey)

  • (a) Authentication. The server accepted no key at all: it does not even know who you are.
  • (b) The key is not loaded in the agent, or the one on your disk is not the one uploaded to the server. Less frequent but very disorienting: permissions too open on the private key, making OpenSSH ignore it silently.
  • (c) ssh-add -l to see what is loaded and, if that clears nothing up, ssh -vT [email protected] to see which key is offered and how the server responds.

2. Permission to team/task-manager.git denied to carla

  • (a) Authorisation. The message names Carla, so authentication worked perfectly.
  • (b) Her account has no write access to that repository, or — if she is going over HTTPS with a token — the token lacks the write scope.
  • (c) Review the account's permissions in the repository settings and the token's scopes on the platform. No Git command fixes this: it is a change on the server.

3. Support for password authentication was removed

  • (a) Authentication. A method the server no longer accepts is being sent.
  • (b) The account password was typed instead of a token; or there is an old credential stored in the manager, holding the pre-migration password.
  • (c) Generate a token with the necessary scope and, very importantly, delete the stored credential before retrying, or the manager will send the old one again:
# Remove the stored credential for that server
printf "protocol=https\nhost=git.example.com\n\n" | git credential reject

4. REMOTE HOST IDENTIFICATION HAS CHANGED!

  • (a) Authentication, but the other way round: it is the server that cannot prove its identity to you.
  • (b) The server has been reinstalled or the platform has rotated its keys (the normal case); or somebody is intercepting the connection (the serious one).
  • (c) Get the current fingerprint and compare it with the officially published one before touching anything:
ssh-keyscan git.example.com 2>/dev/null | ssh-keygen -lf -

Only if it matches the official one: ssh-keygen -R git.example.com and reconnect.

5. connect to host … port 22: Connection timed out

  • (a) Neither of the two: this is a network problem. The connection never gets established, so there is no authentication and no authorisation.
  • (b) A corporate firewall with port 22 closed (by far the most common), or the server is down.
  • (c) Check connectivity and, if it is the firewall, change route:
# Does anything reach port 22?
nc -zv git.example.com 22

# The practical fix: move to HTTPS
git remote set-url origin https://git.example.com/team/task-manager.git

Some platforms also offer SSH over port 443, which gets through those firewalls:

Host git.example.com
    HostName ssh.git.example.com
    Port 443
    User git

Conclusion

With this lesson the team can finally talk to the server. The essentials:

  • Reading a public repository requires no credentials; writing always does, because push modifies the server's references.
  • user.name and user.email authenticate nothing: they are commit metadata. Your access identity lives at another layer.
  • Authentication (who you are) and authorisation (what you may do) are different. Permission denied (publickey) is the first; 403 Forbidden is the second. Telling them apart steers the diagnosis.
  • HTTPS authenticates with a personal access token, which replaced the password because it is limited in scope, revocable, expirable, auditable and compatible with two-factor authentication. Always grant the minimum scope and use one token per machine.
  • SSH authenticates with a key pair: the private key never leaves your disk, the public one is uploaded to the server. Generate with ssh-keygen -t ed25519 -C "description", set a passphrase, upload the .pub file, test with ssh -T and use ssh-agent so you only type it once.
  • ~/.ssh/config solves the several-identities case, even two accounts on the same server, by means of invented aliases. IdentitiesOnly yes avoids Too many authentication failures.
  • HTTPS versus SSH: HTTPS wins on corporate firewalls and permission granularity; SSH wins on day-to-day convenience, on never expiring and on handling several identities. Switching between them is a git remote set-url.
  • Each system has its own credential manager: libsecret on Linux, osxkeychain on macOS and Git Credential Manager on Windows, which additionally opens the browser and manages the tokens for you: Carla's route.
  • credential.helper store keeps the token in plain text in ~/.git-credentials. Avoid it on real machines; if there is no keyring, cache with a timeout is the better option.
  • For everything else — commit signing, secret management, credentials leaked into the history — see lesson 08-05.

What comes next

Ana, Bruno and Carla can now authenticate. The channel is open in both directions and, for the first time in the course, objects can travel from one .git/ to another.

In lesson 04-04: Fetching and Pulling Changes we start using it in the receiving direction, and we tackle Git's most widespread confusion head-on: the difference between git fetch and git pull. We will see why fetch never touches your working tree, what FETCH_HEAD is, how to inspect what has arrived before integrating it, and the various modes of git pull. And at last Carla clones task-manager and catches up with her colleagues' work.

Mastering Git: From Beginner to Advanced

Module 1: Introduction to Git

Module 2: Basic Git Operations

Module 3: Branching and Merging

Module 4: Working with Remote Repositories

Module 5: Advanced Git Operations

Module 6: Git Tools and Techniques

Module 7: Collaboration and Workflow Strategies

Module 8: Git Best Practices and Tips

Module 9: Troubleshooting and Debugging

Module 10: Git in the Real World

© Copyright 2026. All rights reserved