Ana has her remote registered. She types git push origin main full of enthusiasm and gets this:
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
- Why a public
cloneasks for nothing and apushdoes - Authentication versus authorisation
- HTTPS with a personal access token
- SSH: the key pair
- Generating the key with
ssh-keygen -t ed25519 - Uploading the public key, testing the connection and using the agent
~/.ssh/config: several identities on the same machine- HTTPS versus SSH: a comparison table
- Credential managers by operating system
- Typical errors and how to diagnose them
- Why a public
clone asks for nothing and a push does
clone asks for nothing and a push doesThe 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.
Ana Ferrer [email protected]
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.)
- 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".
- 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):
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:
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.gitThat 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.
- 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.
- Generating the key with
ssh-keygen -t ed25519
ssh-keygen -t ed25519Carla, 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.
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:
-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.
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:
-----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.
- 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.pubA 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:
ssh -T [email protected]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:
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.
Enter passphrase for /home/carla/.ssh/id_ed25519: Identity added: /home/carla/.ssh/id_ed25519 ([email protected])
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_ed25519On Windows 11, the ssh-agent service ships with the system but starts out disabled. From PowerShell as administrator:
And then, from Git Bash:
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.
~/.ssh/config: several identities on the same machine
~/.ssh/config: several identities on the same machineA 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 yesAn 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:
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.gitSame 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:
- 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 account → SSH. 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 closed → HTTPS + 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 it → HTTPS with no credentials.
- Several accounts on the same machine → SSH 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.gitYou lose nothing: the commits are the same, the references are the same and only the route changes.
- 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-libsecretOn 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:
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:
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:
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:
And it works first time. The problem becomes obvious when you look at the file it creates:
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:
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.
- 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:
If it answers Could not open a connection to your authentication agent, the agent is not running:
If it answers The agent has no identities, it is running but empty: the ssh-add is missing.
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.pubOpenSSH 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:
- 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).
- 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 -# 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:
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:
- Generate an Ed25519 pair at a specific path,
~/.ssh/course_practice, with a descriptive comment. - Show the permissions of both files and explain why they differ.
- Show the public key's fingerprint.
- Load it into
ssh-agentand verify that it is listed. - Check that the fingerprint of the private key and that of the public one match. Explain why.
- 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).
- Write the complete
~/.ssh/configfor both, with separate keys. - Explain what each directive does.
- Write the clone URL she would use for each repository.
- Give the command that would let her check which specific key is offered to each server.
- Explain what would happen if she left out
IdentitiesOnly yesand 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.
[email protected]: Permission denied (publickey).remote: Permission to team/task-manager.git denied to carla.remote: Support for password authentication was removed.WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!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.
-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.
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.pub256 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.pubSolution 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 yes2. 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 alwaysgit; 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.git4. Checking which key is offered:
ssh -T -v [email protected] 2>&1 | grep "Offering public key"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:
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 -lto 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 reject4. 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:
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.gitSome platforms also offer SSH over port 443, which gets through those firewalls:
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
pushmodifies the server's references. user.nameanduser.emailauthenticate 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 Forbiddenis 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.pubfile, test withssh -Tand usessh-agentso you only type it once. ~/.ssh/configsolves the several-identities case, even two accounts on the same server, by means of invented aliases.IdentitiesOnly yesavoidsToo 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:
libsecreton Linux,osxkeychainon macOS and Git Credential Manager on Windows, which additionally opens the browser and manages the tokens for you: Carla's route. credential.helper storekeeps the token in plain text in~/.git-credentials. Avoid it on real machines; if there is no keyring,cachewith 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
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
