Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Git & GitHub  Module 5 – GitHub and remotes

Set up GitHub: account, 2FA and SSH keys

Create a GitHub account, protect it with two-factor authentication, connect with an Ed25519 SSH key, check GitHub's host key and keep tokens out of URLs.

  • Beginner
  • 20 minutes
  • Examples run with git 2.54.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create a GitHub account, secure it with two-factor authentication and keep your email address private in commits
  • Generate an Ed25519 SSH key, load it into the agent and add the public key to GitHub
  • Verify GitHub's host key fingerprint before trusting a first SSH connection
  • Use HTTPS with Git Credential Manager or GitHub CLI instead of typing secrets
  • Create a fine-grained personal access token with the least access and an expiry date

Before you start

What you’ll learn · Before you start

What you will learn

  • Create a GitHub account, secure it with two-factor authentication and keep your email address private in commits
  • Generate an Ed25519 SSH key, load it into the agent and add the public key to GitHub
  • Verify GitHub's host key fingerprint before trusting a first SSH connection
  • Use HTTPS with Git Credential Manager or GitHub CLI instead of typing secrets
  • Create a fine-grained personal access token with the least access and an expiry date

Before you start

On this page

An account that contributes code on GitHub has 45 days from GitHub’s notice, and a 7-day grace period after them, to turn on two-factor authentication (2FA): Signing in with two different things: something you know, your password, and something you have, such as an authenticator app, a passkey or a security key. A stolen password alone then opens nothing. before it is locked out of github.com. After that, every git push has to prove two things: that the server really is github.com, and that the person pushing is you. SSH proves both with keys: GitHub’s host key: The key pair that identifies an SSH server. On every connection the server proves it holds the private half, and ssh compares the public half with the copy stored in known_hosts, or asks you to check its fingerprint the first time., which you check against a published key fingerprint: A short digest of a public key, such as SHA256: followed by 43 characters, used to compare keys by eye. Any change to the key gives a completely different fingerprint., and a key pair of your own. HTTPS proves you with a token that a credential helper: A program that Git asks for a user name and password when an HTTPS remote needs them. It keeps what it gets, such as a token, in a store, and erases it when the server refuses it. Git Credential Manager and GitHub CLI are two. keeps for you, so you never type or paste it.

You will protect the account, make an Ed25519 key, check GitHub’s host key before the first yes, get past a blocked port 22, set up HTTPS without typing a secret, and see where a token in a URL ends up. The sessions ran with OpenSSH 10.3 and Git 2.54.0 in throwaway folders and never contact GitHub; what only a real connection shows is quoted from GitHub’s documentation.

Two-factor authentication and a noreply address protect the account

Sign up on github.com with an email address you will keep, and pick your username with care: it appears in the address of every repository you own, github.com/USERNAME/REPOSITORY. A new personal account starts with unlimited public and private repositories (private ones with a smaller set of features), 2,000 minutes a month of GitHub Actions, 120 GitHub Codespaces core hours a month and GitHub Pages for public repositories, according to GitHub’s plans page.

Keep your email address private. Every commit records an email address, and anyone can read it from a public repository. In your account’s email settings, turn on “Keep my email address private”. GitHub then gives you a noreply address of the form ID+USERNAME@users.noreply.github.com, where ID is a number of your account, and uses it for commits you make on the website. Set the same address for commits from your computer with git config --global user.email "ID+USERNAME@users.noreply.github.com", using the exact address your email settings show.

Turn on two-factor authentication before you push anything. GitHub requires it for accounts that contribute code, and gives such an account a fixed time to enrol. Walk through the phases, from GitHub’s email to a locked account:

Timeline · 4 events in order From GitHub's two-factor notice to a locked account Walk through the four phases in order and read what each one asks of you, and what a locked account can still do.
  1. The notice

    An email starts the enrolment period 45 days

    GitHub emails an account that contributes code, and that email marks the beginning of a 45-day period in which to turn on two-factor authentication. Banners on github.com ask for it as well.

  2. Enrol

    An authenticator app first, then a backup

    Set up a time-based one-time password (TOTP) app as the main method, then add a passkey or a security key as a backup; GitHub Mobile is a good backup too. Save the 16 recovery codes: each one gets you in once, so keep them in a password manager, not beside your phone.

  3. Grace period

    Seven more days 7 days

    After the 45 days comes a grace period of 7 days. Letting it run out without two-factor authentication is what locks the account.

  4. Locked

    No github.com until two-factor is on

    A locked account cannot use github.com until two-factor authentication is set up, and every visit asks for it. Tokens it already has keep working, but it cannot authorise new apps or create new personal access tokens.

The timeline has 4 events, from The notice to Locked.

The enrolment step is where most of the protection is decided. GitHub’s guidance on the methods comes down to this:

Method Use it as Why
A time-based one-time password (TOTP) app The main method GitHub recommends it as the primary method
A passkey or a security key A backup GitHub does not take them as the primary method yet
GitHub Mobile A backup A good backup when you have no passkey or security key
Text messages (SMS) Only when nothing else works Allowed in most countries, with security risks GitHub warns about; not offered where delivery is poor
16 recovery codes The way back in Each works once; keep them in a password manager, away from your phone

The recovery codes matter most on the worst day: when the phone with the app is lost, they and the backup methods are what still let you in, because the password alone is no longer enough.

Checkpoint

Check: Two-factor authentication and the noreply address

  1. Question 1 of 2 GitHub's two-factor email has arrived for your account, which contributes code. If you never turn two-factor authentication on, when are you locked out of github.com?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: After the 45-day enrolment period and the 7-day grace period that follows it

    The notice starts a 45-day enrolment period, a 7-day grace period follows, and an account that lets that run out cannot use github.com until two-factor authentication is on. Its existing tokens keep working, but it can create no new ones.

  2. Question 2 of 2 Asha keeps her 16 recovery codes in a notes app on the same phone as her authenticator app. What is wrong with that?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Losing the phone loses the main method and the way back in at the same time

    Each recovery code gets you in once when the main method is lost. Keep them in a password manager, away from the phone, together with a backup method such as a passkey or a security key.

Git signs in over SSH with a key, or over HTTPS with a token

Git signs in whenever GitHub needs to know who you are: for every push, for fetches from private repositories, and for everything over SSH, which always uses your key. The remote’s address decides the way: git@github.com:OWNER/REPO.git goes over SSH, https://github.com/OWNER/REPO.git over HTTPS. Each proves who you are differently:

SSH key or HTTPS with a credential helper?
Criterion SSH keyHTTPS with a helper
Set up A key pair; GitHub gets the public keyGit Credential Manager or GitHub CLI, signed in once in the browser
Sends A signature made with your private keyA token the helper keeps for you
Secret in ~/.ssh, behind a passphraseYour system's credential store
Port 22, or 443 at ssh.github.com443
URL git@github.com:OWNER/REPO.githttps://github.com/OWNER/REPO.git
When to choose Daily work on your own computerSSH is blocked, or you prefer signing in through the browser

Both are fine, and many people use SSH on their own laptop and HTTPS with a helper elsewhere. A third way, pasting a personal access token: A secret that GitHub issues to stand in for your password when Git, a script or a tool signs in over HTTPS. A fine-grained token is limited to chosen repositories and permissions, and should expire. when Git asks, also works, but then the token lives wherever you pasted it; keep that for scripts that cannot use a helper. What you should not do is type your account password for Git: GitHub removed password sign-in for Git, and when Git asks for a password over HTTPS, GitHub expects a personal access token. A clone can change its way at any time with git remote set-url, which the next lesson shows.

Checkpoint

Check: SSH or HTTPS

  1. Question 1 of 2 Git asks for a password while you push to GitHub over HTTPS. What does GitHub expect in that field?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A personal access token, not your account password

    When Git asks for a password over HTTPS, GitHub expects a personal access token. Better still, let a credential helper answer, so you never type or paste the token at all.

  2. Question 2 of 2 A clone's remote is git@github.com:asha/notes.git. How does Git prove who you are when you push?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: With an SSH key; ssh signs with your private key

    The remote's address decides the way: git@github.com:… goes over SSH with your key, https://github.com/… over HTTPS with a token. git remote set-url switches a clone from one to the other.

An SSH key’s private half never leaves your computer

ssh-keygen -t ed25519 makes a key pair with the Ed25519 algorithm, the type GitHub’s documentation asks for first; its fallback for systems that lack Ed25519 is ssh-keygen -t rsa -b 4096. -C adds a label, usually your email, and -f names the file. A new key is random, so the session below shows its random parts as descriptions:

A new Ed25519 key Python · ssh_key_session.py
from session import run

# A new key is random, so its fingerprint, its public key and its picture are new on every run: mask= shows them as
# descriptions instead.
NEW_KEY = (
    (r"SHA256:[A-Za-z0-9+/]{43}", "SHA256:<43 characters, new for every key>"),
    (r"(AAAAC3NzaC1lZDI1NTE5AAAAI)[A-Za-z0-9+/]{43}", r"\1<43 characters, new for every key>"),
    (r"^The key's randomart image is:$", "The key's randomart image is: (a small picture of the fingerprint, left out here)"),
    (r"^[+|]", None),
)

run("""
ssh -V
mkdir -m 700 ~/.ssh
# -N gives the passphrase here only because nobody can type it into this session. On your computer, leave -N out
# and type the passphrase when ssh-keygen asks, so that it never sits in your shell history.
ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519 -N "five random words for this demo"
ls ~/.ssh
cat ~/.ssh/id_ed25519.pub
ssh-keygen -lf ~/.ssh/id_ed25519.pub
""", mask=NEW_KEY)

Output

$ ssh -V
OpenSSH_10.3p1, LibreSSL 3.3.6
$ mkdir -m 700 ~/.ssh
# -N gives the passphrase here only because nobody can type it into this session. On your computer, leave -N out
# and type the passphrase when ssh-keygen asks, so that it never sits in your shell history.
$ ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519 -N "five random words for this demo"
Generating public/private ed25519 key pair.
Your identification has been saved in ~/.ssh/id_ed25519
Your public key has been saved in ~/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:<43 characters, new for every key> your_email@example.com
The key's randomart image is: (a small picture of the fingerprint, left out here)
$ ls ~/.ssh
id_ed25519
id_ed25519.pub
$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI<43 characters, new for every key> your_email@example.com
$ ssh-keygen -lf ~/.ssh/id_ed25519.pub
256 SHA256:<43 characters, new for every key> your_email@example.com (ED25519)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ssh_key_session.py

ssh-keygen wrote two files. id_ed25519 is the private key: it never leaves your computer, it is never pasted anywhere, and the passphrase encrypts it on disk, so a copied file cannot be used without the passphrase. id_ed25519.pub is the public key, the one line you give GitHub. Every Ed25519 public key starts with AAAAC3NzaC1lZDI1NTE5AAAAI, which is the key type and its length encoded in base64; only the 43 characters after it are your key. The fingerprint, SHA256: and 43 more characters, is a short digest of that key, and GitHub shows the same fingerprint next to the key once you have added it.

A passphrase you type every time gets tedious, so let the SSH agent: A program that holds your unlocked SSH private keys while you are signed in, so that ssh can sign with a key without asking for its passphrase every time. remember it while you are signed in. On Linux and in Git Bash on Windows, eval "$(ssh-agent -s)" starts the agent and ssh-add ~/.ssh/id_ed25519 loads the key. On macOS, ssh-add --apple-use-keychain ~/.ssh/id_ed25519 loads it and keeps the passphrase in the keychain, and these lines in ~/.ssh/config make ssh do so on first use:

~/.ssh/config on macOS Code · macos_ssh_config
# Lines for ~/.ssh/config on macOS, as GitHub's documentation gives them: ssh loads the key into the agent on first
# use and keeps its passphrase in the macOS keychain. Leave out UseKeychain if the key has no passphrase.
Host github.com
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519
Password Generator Make a passphrase of several random words for the key, generated on your device.

Then give GitHub the public key: Settings → SSH and GPG keys → New SSH key, a title such as the computer’s name, key type Authentication Key, and the contents of id_ed25519.pub pasted in. GitHub never needs the private key, and nobody who helps you does either.

Checkpoint

Check: The SSH key pair and the agent

  1. Question 1 of 2 You have typed your key's passphrase three times this morning. Which change stops ssh asking while you are signed in, and keeps the passphrase on the key?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Load the key into the SSH agent with ssh-add (on macOS with --apple-use-keychain)

    ssh-add loads the key into the agent, which signs for ssh while you are signed in. On macOS, --apple-use-keychain also keeps the passphrase in the keychain, and AddKeysToAgent yes in ~/.ssh/config loads the key on first use.

  2. Question 2 of 2 Two classmates compare their id_ed25519.pub files: both lines start with ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI. Do they have the same key?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No; that part encodes only the key type and length, and the 43 characters after it are each one's key

    AAAAC3NzaC1lZDI1NTE5AAAAI is the key type and its length in base64. The key itself is the 43 characters after it, and its fingerprint, SHA256: and 43 more characters, is the short form to compare.

GitHub’s host key proves the server before your key proves you

Keys work both ways. Your key proves you to GitHub, and GitHub’s host key proves github.com to you, on every connection:

An SSH connection checks both sides: GitHub's host key is compared with your known_hosts, then your key signs and GitHub checks it.Your computer~/.ssh/known_hosts:GitHub's host key,checked once by yougithub.comGitHub's host key:its fingerprints arepublished in GitHub's docsYour computer~/.ssh/id_ed25519:your private key,which never leaves itgithub.comyour public key,added in Settings,SSH and GPG keys1. GitHub shows its key;ssh compares it2. your key signs;GitHub checks it

The two checks of every SSH connection to GitHub

Text description of the diagram

The diagram has two columns, one for each check. In each, a box for your computer and a box for github.com are joined by an arrow.

  1. Left column, GitHub proves itself first: github.com shows its host key, whose fingerprints GitHub publishes in its documentation, and ssh compares it with the key stored in ~/.ssh/known_hosts on your computer. On a first connection nothing is stored yet, so you compare the fingerprint with GitHub's list yourself before you accept it.
  2. Right column, then you prove yourself: ssh uses your private key, ~/.ssh/id_ed25519, which never leaves your computer, to sign, and GitHub checks the signature with the public key you added in Settings, SSH and GPG keys.

Only the first connection needs you, because ssh has no stored key for github.com yet. Step through it, from the key github.com offers to the greeting, and find the one step where ssh cannot decide for you:

Interactive · message by message A first SSH connection to GitHub, key by key Step through ssh -T git@github.com and watch which key proves what, and the one moment that needs your check.
A first SSH connection to GitHub, key by key, step 8 of 8. Messages so far: ssh on your computer to github.com: connect to port 22; github.com to ssh on your computer: host key and signature; ssh on your computer to You: trust SHA256:+DiY3…OqU?; You to ssh on your computer: yes, it matches; ssh on your computer to github.com: signed with your key; github.com to You: Hi USERNAME!.Youat the terminalssh on your computergithub.com in known_hostsgithub.comGitHub's host keyconnect to port 22host key and signaturetrust SHA256:+DiY3…OqU?yes, it matchessigned with your keyHi USERNAME!a changed key stops ssh

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. You connect. You run ssh -T git@github.com to test your key. ssh opens a connection to github.com on port 22.
  2. GitHub proves its key. github.com sends its Ed25519 host key, with a signature that only the holder of the matching private key can make. ssh checks the signature, so the server holds that key. Whether it is GitHub's key is the next question.
  3. Nothing stored yet. ssh looks for github.com in ~/.ssh/known_hosts and finds no entry, because this is the first connection. It shows the key's fingerprint and asks you whether to trust it.
  4. You compare. You compare all 43 characters after SHA256: with the Ed25519 line on GitHub's page of SSH key fingerprints. They match, so you answer yes. A key that is not on the list gets no.
  5. The key is stored. ssh adds GitHub's key to ~/.ssh/known_hosts and says so in one line. From now on it checks the key on every connection by itself.
  6. Your key signs. Now you prove who you are. ssh signs with your private key, ~/.ssh/id_ed25519, which never leaves your computer; the passphrase decrypts it, or the agent already holds it decrypted.
  7. GitHub checks. GitHub checks the signature with the public key you added under SSH and GPG keys, and greets you by your username. It offers no shell, so ssh ends with exit status 1, which is normal for this test.
  8. Every later connection. Next time ssh compares the key github.com offers with the stored one without asking. A different key stops it with REMOTE HOST IDENTIFICATION HAS CHANGED, and then you check GitHub's page before you change anything.

GitHub’s documentation shows the same exchange like this (not run here, because these sessions never use the network):

A first connection, as GitHub's documentation shows it Text · first_connection.txt
$ ssh -T git@github.com
The authenticity of host 'github.com (IP ADDRESS)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
Are you sure you want to continue connecting (yes/no)? yes
Hi USERNAME! You've successfully authenticated, but GitHub does not provide shell access.

Before you type yes, compare the fingerprint with the list on GitHub’s page “GitHub’s SSH key fingerprints”. If they do not match, type no: you may be talking to a machine that only claims to be github.com. You can make the same check offline with ssh-keygen. github_known_hosts below holds GitHub’s published Ed25519 host key, and lookalike_known_hosts the same line with its last character changed:

Fingerprints of a published key and of a lookalike Python · host_key_session.py
from session import files, run

# The two known_hosts files of this example's folder: GitHub's published key, and a key that differs in one character.
files("github_known_hosts", "lookalike_known_hosts")

run("""
ssh-keygen -lf github_known_hosts
ssh-keygen -lf lookalike_known_hosts
# Look a host up in a known_hosts file, with the fingerprint of the key stored for it:
ssh-keygen -l -F github.com -f github_known_hosts
""")

Output

$ ssh-keygen -lf github_known_hosts
256 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com (ED25519)
$ ssh-keygen -lf lookalike_known_hosts
256 SHA256:Xpui/jdQlpifVdYhm+ZUa03EcWPgy67Z4ikMikUUptc github.com (ED25519)
# Look a host up in a known_hosts file, with the fingerprint of the key stored for it:
$ ssh-keygen -l -F github.com -f github_known_hosts
# Host github.com found: line 2
github.com ED25519 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 host_key_session.py

The published key’s fingerprint, SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU, matches the Ed25519 entry on GitHub’s page. One changed character gave SHA256:Xpui/jdQlpifVdYhm+ZUa03EcWPgy67Z4ikMikUUptc, with nothing in common with it, which is why comparing a few characters at each end is not enough: compare them all. ssh-keygen -l -F github.com finds the entry stored for a host in a known_hosts file and prints its fingerprint in the same way.

Once a key is stored, ssh compares it on every connection, and stops with “REMOTE HOST IDENTIFICATION HAS CHANGED!” when it differs. That has happened for real: GitHub replaced its RSA host key after the private key was briefly exposed in a public repository, and people who connected with RSA saw the warning, while Ed25519 and ECDSA users saw nothing. The right response is the same as on day one: check the new fingerprint against GitHub’s page, then remove the old entry with ssh-keygen -R github.com. A changed key that GitHub has not announced is a reason to stop.

Key idea

Compare the whole fingerprint with GitHub’s published list once, before the first yes; from then on ssh checks the stored key on every connection and stops when it changes.

Checkpoint

Check: Checking GitHub's host key

  1. Question 1 of 2 Your first ssh -T git@github.com shows ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU and asks whether to continue. What do you do before typing yes?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Compare all 43 characters after SHA256 with the Ed25519 entry on GitHub's page of SSH key fingerprints

    On a first connection ssh cannot check the server for you. The fingerprint GitHub publishes for its Ed25519 key is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU; if every character matches, yes stores the key, and ssh checks it on every later connection by itself.

  2. Question 2 of 2 GitHub has announced that it replaced its RSA host key, and ssh now stops with REMOTE HOST IDENTIFICATION HAS CHANGED for your RSA entry. What is the right order?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Check the new fingerprint against GitHub's page, then remove the old entry with ssh-keygen -R github.com

    ssh stops because the stored key and the offered key differ. With an announced change, compare the new fingerprint with GitHub's page, then remove the old entry; a change nobody announced is a reason to stop.

SSH still reaches GitHub on port 443 when port 22 is blocked

Some college, office and hotel networks block outgoing connections to port 22, and SSH to github.com then hangs until it times out. GitHub also accepts SSH on port 443, the port HTTPS uses, at a different host name: ssh.github.com, not github.com. Test it with ssh -T -p 443 git@ssh.github.com. To use it for every Git command, add this to ~/.ssh/config:

~/.ssh/config: SSH to GitHub through port 443 Code · port443_config
# Lines for ~/.ssh/config when a network blocks port 22: SSH to GitHub then goes to ssh.github.com on port 443.
Host github.com
    Hostname ssh.github.com
    Port 443
    User git

ssh -G prints the settings ssh would use for a host and exits without connecting, so it shows whether the file does what you meant:

What ssh would do, with and without the file Python · ssh_config_session.py
from session import files, run

files("port443_config")

# ssh -G prints the settings ssh would use for a host and exits without connecting (-T: no terminal is needed).
run("""
ssh -T -G -F port443_config github.com | grep -E "^(hostname|port|user) "
ssh -T -G -F /dev/null github.com | grep -E "^(hostname|port|user) "
""", mask=((r"^user (?!git$).*", "user <your user name on this computer>"),))

Output

$ ssh -T -G -F port443_config github.com | grep -E "^(hostname|port|user) "
user git
hostname ssh.github.com
port 443
$ ssh -T -G -F /dev/null github.com | grep -E "^(hostname|port|user) "
user <your user name on this computer>
hostname github.com
port 22

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ssh_config_session.py

With the file, a connection to github.com goes to ssh.github.com on port 443 as user git; without it, to port 22 under your own user name. Your remote URLs keep saying github.com: the Host block changes where ssh connects, not what Git asks for. The first connection through port 443 may say that the host key was not found or was found under another name: it is the same key, so check the fingerprint against GitHub’s page as before.

Checkpoint

Check: SSH over port 443

  1. Question 1 of 2 A network blocks port 22. Which command tests SSH to GitHub through port 443?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ssh -T -p 443 git@ssh.github.com

    Port 443 is the port HTTPS uses, so networks rarely block it. GitHub accepts SSH there at ssh.github.com, not at github.com.

  2. Question 2 of 2 You added a Host github.com block with Hostname ssh.github.com and Port 443 to ~/.ssh/config. How do you see where ssh would connect, without connecting?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ssh -G github.com, which prints hostname ssh.github.com and port 443

    ssh -G github.com prints the settings that apply to the host: with the file, hostname ssh.github.com, port 443 and user git; without it, port 22 and your own user name.

A credential helper signs in once and keeps the token

A credential helper is a program Git asks for a user name and password when an HTTPS remote needs them. The good ones sign you in once, through your browser or a sign-in window and with your two-factor authentication, and keep the resulting token in your system’s credential store. Step through two pushes with one, and a third after the token has been revoked:

Interactive · message by message HTTPS with a credential helper, the first push and the next Step through two pushes over HTTPS and see who asks for the token, who keeps it, and why you never type it.
HTTPS with a credential helper, the first push and the next, step 6 of 6. Messages so far: Git to Credential helper: credentials for github.com?; Credential helper to github.com: sign in with 2FA; github.com to Credential helper: a new token; Git to github.com: push with the token; Credential helper to Git: the stored token; Git to Credential helper: erase this token.Gitgit push over HTTPSCredential helpernothing storedgithub.comcredentials for github.com?sign in with 2FAa new tokenpush with the tokenthe stored tokenerase this token

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The push needs a sign-in. git push sends its first request to https://github.com/OWNER/REPO.git, and GitHub answers that it needs credentials. Git does not prompt you: it asks the credential helper for a user name and password for github.com.
  2. Nothing stored yet. The helper has nothing stored for github.com, so it opens a sign-in window or your browser. You sign in there once, with your password and your two-factor method, never in the terminal.
  3. A token for the helper. GitHub gives the helper a token, and the helper hands it to Git in place of a password. The token never appears on your screen.
  4. The push goes through. Git repeats the request with the token, and GitHub accepts the push. Git then tells the helper that the credentials worked, and the helper keeps the token in the system's credential store, such as the macOS keychain.
  5. The next push. The next time Git needs credentials for github.com, the helper answers from the store at once. No window opens and nothing is typed.
  6. A token that stops working. When GitHub refuses the token, because it was revoked or has expired, Git tells the helper to erase it. The next push signs in through the browser again and stores a new one.

Two helpers cover most setups:

  • Git Credential Manager (GCM, MIT licence) comes with Git for Windows; on macOS it installs with brew install --cask git-credential-manager, and on Linux as a .NET tool or a package, followed by git-credential-manager configure. Git calls it on its own when it needs credentials for an HTTPS remote.
  • GitHub CLI: gh auth login signs in through a web browser by default and stores a token in the system credential store, falling back to a plain-text file only when there is none. gh auth setup-git then makes Git use gh as its credential helper.

Either way, the token’s whole life happens between the helper and GitHub, as the figure showed: you never see it, and when you revoke it, the helper erases its copy at the next refused push and signs you in again.

Checkpoint

Check: Credential helpers

  1. Question 1 of 2 Where does a credential helper such as GitHub CLI keep the token it got for Git?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In the system's credential store, such as the macOS keychain

    The helper signs you in once, keeps the token in the system's credential store and hands it to Git whenever Git asks, so it never sits in a file of the repository or on your screen.

  2. Question 2 of 2 You revoked the token that Git Credential Manager had stored. What happens at your next git push over HTTPS?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: GitHub refuses the token, Git tells the helper to erase it, and you sign in through the browser again

    Git's credential protocol reports success and failure to the helper: an accepted token is kept, a refused one is erased. That is why revoking a token causes no lasting trouble when a helper holds it.

A fine-grained token opens only what one job needs

When you do need a personal access token, for a script or a tool that cannot use a helper, GitHub recommends fine-grained tokens over classic ones. Create one under Settings → Developer settings → Personal access tokens → Fine-grained tokens. For a script that writes to one repository of yours, fill the form in like this:

Field Choose Why
Token name What it is for, such as notes backup script You will know which one to revoke
Expiration A date just after the job ends GitHub highly recommends an expiry
Resource owner Your account The token reaches only what this owner owns
Repository access Only select repositories, then the one repository Every other repository stays closed
Permissions Contents: read and write Only what the script needs

Every token can also read all public repositories, whatever you select. GitHub removes tokens that have not been used for a year, and a few things still need a classic token, such as writing to a public repository that neither you nor an organisation you belong to owns; for those, use a credential helper or SSH rather than a broad classic token.

Checkpoint

Check: Fine-grained tokens

  1. One question You gave a fine-grained token access to one private repository and nothing else. What else can it read?

    Choose one answer.

    Show the answer: One question

    Answer: Every public repository on GitHub

    A fine-grained token opens only the repositories and permissions you choose, plus the read-only access to public repositories that every token has.

A token in a URL leaks, and a leaked one is revoked first

It is tempting to put a token straight into the remote URL so that Git never asks. This session shows where it then ends up, using a token that is made up:

A token in a remote URL, and the fix Python · token_url_session.py
from session import run

# The token in this example is made up: it matches no real token format and opens nothing.
run("""
git init -q notes
cd notes
git remote add origin https://your-username:MADE-UP-TOKEN-FOR-THIS-DEMO@github.com/your-username/notes.git
git remote -v
git config --show-origin --get remote.origin.url
# The fix: remove the token from the URL (then revoke the token on GitHub):
git remote set-url origin https://github.com/your-username/notes.git
git config --show-origin --get remote.origin.url
""")

Output

$ git init -q notes
$ cd notes
$ git remote add origin https://your-username:MADE-UP-TOKEN-FOR-THIS-DEMO@github.com/your-username/notes.git
$ git remote -v
origin	https://your-username:MADE-UP-TOKEN-FOR-THIS-DEMO@github.com/your-username/notes.git (fetch)
origin	https://your-username:MADE-UP-TOKEN-FOR-THIS-DEMO@github.com/your-username/notes.git (push)
$ git config --show-origin --get remote.origin.url
file:.git/config	https://your-username:MADE-UP-TOKEN-FOR-THIS-DEMO@github.com/your-username/notes.git
# The fix: remove the token from the URL (then revoke the token on GitHub):
$ git remote set-url origin https://github.com/your-username/notes.git
$ git config --show-origin --get remote.origin.url
file:.git/config	https://github.com/your-username/notes.git

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 token_url_session.py

git remote add stored the whole URL, token included, in .git/config as plain text, and git remote -v prints it to anyone looking at your screen, a screenshot, a recorded demo or a build log. The command also sits in your shell history, and the token travels with every copy of the folder you zip and send to someone. git remote set-url removes it from the configuration, but the token must be treated as known: revoke it on GitHub, then let a credential helper sign you in instead.

The same holds for a token, a password or a private key pasted into a chat, committed or shown in a screenshot: treat it as compromised. The OWASP Secrets Management Cheat Sheet puts revocation first: exposed keys should be revoked immediately, a replacement issued, and only then the exposed value removed from where it leaked. Step through that order with GitHub’s own settings:

Interactive · stage by stage A token was exposed. Respond in this order Step through the four moves in order. Each one closes a door that the move before it would leave open.
A token was exposed. Respond in this order, step 5 of 5. Showing A token is exposed, Revoke it, Read the security log, Issue a replacement, Clean up where it leaked. In focus: Clean up where it leaked.A token is exposedchat, commit, screenshotRevoke itdelete the token or keyRead the security logactions you do not knowIssue a replacementleast access, an expiryClean up where it leakedmessage, file, history

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Exposed means known. A token pasted into a chat, committed, shown in a screenshot or written into a remote URL may already have been copied. From that moment treat it as known to someone else.
  2. Revoke first. Delete the token, or the SSH key, in your GitHub settings. Until then anyone who copied it can use it, and nothing you clean up later takes that copy back.
  3. Look for misuse. Open Settings, Archives, Security log. It lists the last 90 days of actions on your account, the token and SSH key events among them. Look for anything you do not recognise.
  4. A replacement. Create a new token with only the repositories and permissions the job needs, and an expiry. Better still, let a credential helper hold it, so nobody handles it by hand again.
  5. Clean up last. Only now remove the value from the chat, the file, the URL or the history. Deleting the message first changes nothing for whoever already copied the token.

Secret & Credential Leak Scanner Paste a config file, a log or a diff and see whether it holds a key or a token before you share it.

Key idea

A token belongs in a credential helper’s store, never in a URL, a file or a chat, and one that has been seen is revoked before anything else is cleaned up.

Checkpoint

Check: Tokens in URLs and leaked secrets

  1. One question You ran git remote set-url origin https://github.com/you/notes.git to take your token out of the remote URL. Is the token safe now?

    Choose one answer.

    Show the answer: One question

    Answer: No; it may be in your shell history, a screenshot or a copy of the folder, so revoke it

    git remote set-url removes the token from the configuration, but it was stored in plain text and printed by git remote -v, so revoke it on GitHub and let a credential helper sign you in from now on.

Practise: check a host key by its fingerprint

The exercise turns the check you make by eye into code: compute a key’s fingerprint from a known_hosts line and look it up in a published list. The sample tests use GitHub’s key and the lookalike of this lesson:

Exercise · Easy · Python

Compute an SSH key fingerprint and check it against a published list

Write two functions in fingerprint.py, so that a program can check a host key the way you check one by eye.

fingerprint(line) takes one line of a known_hosts file (github.com ssh-ed25519 AAAA…) or of a .pub file (ssh-ed25519 AAAA… comment) and returns the key's SHA256 fingerprint exactly as ssh-keygen -l prints it: SHA256: followed by the base64 of the SHA-256 digest of the decoded key, without the = padding. The key is the word after the key type, which starts with ssh-, ecdsa- or sk-. A line with no key on it (a comment, a blank line, a key type with nothing after it) raises ValueError.

is_published(line, published) returns True when the key on the line has one of the fingerprints in published, a list of strings such as the ones GitHub publishes.

  • For GitHub's Ed25519 line, fingerprint returns SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
  • For the lookalike line of this lesson, it returns SHA256:Xpui/jdQlpifVdYhm+ZUa03EcWPgy67Z4ikMikUUptc, and is_published returns False for it.

The sample tests also try a .pub line, a line that names a host twice, and lines that hold no key.

Starter code · fingerprint.py

import base64
import hashlib


def fingerprint(line):
    """Return the SHA256 fingerprint of the key on a known_hosts or .pub line, as ssh-keygen -l prints it."""
    # Replace this line with your code.
    return "SHA256:"


def is_published(line, published):
    """Return True when the key on the line has one of the fingerprints in `published`."""
    # Replace this line with your code.
    return True
The sample tests · test_fingerprint.py
from fingerprint import fingerprint, is_published

GITHUB = "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"
LOOKALIKE = "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJm"
# The fingerprints GitHub publishes for its RSA, ECDSA and Ed25519 host keys.
PUBLISHED = [
    "SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s",
    "SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM",
    "SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU",
]


def test_github_key():
    """computes the fingerprint of GitHub's published Ed25519 key"""
    assert fingerprint(GITHUB) == "SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU"


def test_one_character_changes_everything():
    """gives a completely different fingerprint for a key that differs in one character"""
    assert fingerprint(LOOKALIKE) == "SHA256:Xpui/jdQlpifVdYhm+ZUa03EcWPgy67Z4ikMikUUptc"


def test_pub_line_and_two_host_names():
    """reads a .pub line (key type first) and a line that names the host twice"""
    pub = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl laptop key"
    two = "github.com,ssh.github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"
    assert fingerprint(pub) == fingerprint(GITHUB)
    assert fingerprint(two) == fingerprint(GITHUB)


def test_published_or_not():
    """accepts only a key whose fingerprint is on the list"""
    assert is_published(GITHUB, PUBLISHED) is True
    assert is_published(LOOKALIKE, PUBLISHED) is False
    assert is_published(GITHUB, []) is False


def test_lines_without_a_key():
    """raises ValueError for a comment, a blank line and a key type with no key after it"""
    for line in ("# GitHub's keys", "", "github.com ssh-ed25519"):
        try:
            fingerprint(line)
        except ValueError:
            continue
        assert False, f"no ValueError for {line!r}"
A hint

Split the line into words and find the first word that starts with one of the key-type prefixes; the key is the word after it. base64.b64decode turns the key into bytes, hashlib.sha256(...).digest() gives the 32-byte digest, and base64.b64encode(...).decode() turns that back into text, from which .rstrip("=") removes the padding.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Each session above is a short Python script that types its commands through the helper below. Save the two files in one folder and start one, say python3 ssh_key_session.py: the key it makes lands in a temporary home folder that is deleted afterwards, so your own ~/.ssh is never read or changed.

session.py, which types the commands of every session Python · session.py
"""Shows a terminal session with Git: each line of a script is typed at a prompt (after "$"), and what the command
printed appears under it.

Every session starts in a new, empty folder, shown as ~/demo, with a home folder of its own, so your own Git settings
take no part: its ~/.gitconfig holds only a name, an email address and init.defaultBranch = main. Standard output and
standard error appear together, as in a terminal, and a command that fails adds a line with its exit status, after
which the session goes on. "cd <folder>" changes the folder of the commands that follow, as in a shell; a line that
starts with "#" is printed as a comment. Nothing uses the network: the "remote" repositories are folders.

So that every run prints the same text:
  * each command runs one minute after the one before (GIT_AUTHOR_DATE and GIT_COMMITTER_DATE), so the commit IDs
    are the same on every run (yours will differ, because your commits carry your own times);
  * the session's folder is shown as ~/demo and its home folder as ~, and spaces at the ends of lines are dropped;
  * mask= rewrites text that is new on every run, such as a freshly made key, as a description of it.
On a Mac, the Command Line Tools' own git is used directly when it is there (the /usr/bin/git launcher in front of it
can print cache warnings when it runs without a writable home folder).
"""

import atexit
import os
import re
import shlex
import shutil
import subprocess
import tempfile

# The example's own folder (files() copies from it) and the session's folders, which are removed at the end.
HERE = os.getcwd()
_BASE = os.path.realpath(tempfile.mkdtemp(prefix="git-session-"))
atexit.register(shutil.rmtree, _BASE, ignore_errors=True)
ROOT = os.path.join(_BASE, "demo")
HOME = os.path.join(_BASE, "home")
for folder in (ROOT, HOME, os.path.join(_BASE, "tmp")):
    os.makedirs(folder)
with open(os.path.join(HOME, ".gitconfig"), "w", encoding="utf-8") as f:
    f.write("[user]\n\tname = Learner\n\temail = learner@example.com\n[init]\n\tdefaultBranch = main\n")

_MAC_GIT = "/Library/Developer/CommandLineTools/usr/bin"
_path = os.environ.get("PATH", "/usr/bin:/bin")
if os.path.exists(os.path.join(_MAC_GIT, "git")):
    _path = _MAC_GIT + os.pathsep + _path
ENV = {
    "PATH": _path,
    "HOME": HOME,
    "XDG_CONFIG_HOME": os.path.join(HOME, ".config"),
    "TMPDIR": os.path.join(_BASE, "tmp"),
    "GIT_CONFIG_NOSYSTEM": "1",
    "GIT_TERMINAL_PROMPT": "0",
    "LANG": os.environ.get("LANG", "C.UTF-8"),
    "LC_ALL": os.environ.get("LC_ALL", os.environ.get("LANG", "C.UTF-8")),
    "TZ": "UTC",
}
# A fixed clock: the first command runs at this second (India Standard Time), each later one a minute after.
_state = {"clock": 1767240000, "cwd": ROOT}


def _tidy(line: str) -> str:
    """The line with the session's folders written the way the lesson shows them."""
    for real, shown in ((ROOT, "~/demo"), (HOME, "~"), (_BASE, "…")):
        line = line.replace(real, shown)
    return line.rstrip()


def files(*names: str) -> None:
    """Copies files of the example's folder into ~/demo, the folder the session starts in."""
    for name in names:
        shutil.copy(os.path.join(HERE, name), os.path.join(ROOT, name))


def setup(script: str) -> None:
    """Runs the commands of `script` without showing them (the lesson shows them in the example's code); a command that
    fails stops the example."""
    run(script, show=False)


def run(script: str, mask: tuple = (), show: bool = True) -> None:
    """Types each line of `script` at the prompt and prints what it printed.

    mask: (pattern, replacement) pairs applied to each line of output; a replacement of None leaves the line out.
    """
    for command in script.strip("\n").splitlines():
        command = command.strip()
        if not command or command.startswith("#"):
            if show:
                print(command)
            continue
        if show:
            print(f"$ {command}")
        words = shlex.split(command)
        if words[0] == "cd" and len(words) == 2:
            where = HOME + words[1][1:] if words[1].startswith("~") else words[1]
            target = os.path.normpath(os.path.join(_state["cwd"], where))
            if os.path.isdir(target):
                _state["cwd"] = target
            elif show:
                print(f"cd: no such file or directory: {words[1]}")
                print("[exit status 1]")
            else:
                raise SystemExit(f"set-up command failed: {command}")
            continue
        _state["clock"] += 60
        stamp = f"{_state['clock']} +0530"
        env = {**ENV, "GIT_AUTHOR_DATE": stamp, "GIT_COMMITTER_DATE": stamp}
        result = subprocess.run(command, shell=True, cwd=_state["cwd"], env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30)
        if not show:
            if result.returncode != 0:
                raise SystemExit(f"set-up command failed: {command}\n{_tidy(result.stdout)}")
            continue
        for line in result.stdout.splitlines():
            line = _tidy(line)
            for pattern, replacement in mask:
                if replacement is not None:
                    line = re.sub(pattern, replacement, line)
                elif re.search(pattern, line):
                    break
            else:
                print(line)
        if result.returncode != 0:
            print(f"[exit status {result.returncode}]")

Then test yourself on the eight ideas of this lesson, from the account to a leaked token. Every answer is explained at once, and the questions keep coming back to the idea you have missed most:

Practice that adapts to you

Practise this lesson

19 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 19 Your phone with the authenticator app is stolen. What gets you back into your GitHub account?

    Choose one answer.

    Show the answer: Question 1 of 19

    Answer: A recovery code, or the passkey or security key you registered as a backup

    Two-factor authentication means the password alone is not enough. A backup method, or one of the 16 single-use recovery codes kept in a password manager, is what lets you in without the phone.

  2. Question 2 of 19 You turned on "Keep my email address private". Which user.email setting keeps your real address out of the commits you make on your laptop?

    Choose one answer.

    Show the answer: Question 2 of 19

    Answer: The ID+USERNAME@users.noreply.github.com address shown in your email settings

    Git writes user.email into every commit on your computer, before GitHub ever sees it. The noreply address keeps your real address private and still links the commits to your account, which a made-up address would not.

  3. Question 3 of 19 Which of these remote addresses makes Git sign in with your SSH key?

    Choose one answer.

    Show the answer: Question 3 of 19

    Answer: git@github.com:OWNER/REPO.git

    GitHub shows two addresses for every repository: git@github.com:OWNER/REPO.git for SSH and https://github.com/OWNER/REPO.git for HTTPS. The address of the remote decides how Git signs in.

  4. Question 4 of 19 A network blocks port 22, and you would rather not change any ssh settings on this computer. Which way of signing in still works for git push?

    Choose one answer.

    Show the answer: Question 4 of 19

    Answer: HTTPS with a credential helper, over port 443

    Both ways reach GitHub over port 443: SSH at ssh.github.com with a few lines of ssh settings, or HTTPS with a credential helper and no ssh settings at all.

  5. Question 5 of 19 ssh-keygen made id_ed25519 and id_ed25519.pub. Which one do you paste into GitHub's SSH key settings?

    Choose one answer.

    Show the answer: Question 5 of 19

    Answer: id_ed25519.pub, the public key

    GitHub needs only the public key, to check signatures your private key makes. The private key never leaves your computer; anyone who has it, and its passphrase, can push as you.

  6. Question 6 of 19 A classmate asks you to send them your id_ed25519 file so that they can push to your repository from their laptop. What is the right answer?

    Choose one answer.

    Show the answer: Question 6 of 19

    Answer: No; add them as a collaborator, and they push with a key of their own

    A private key identifies one person on one computer, and anyone holding it can act as you everywhere the key is trusted. GitHub gives each person their own access: invite them as a collaborator, and they add their own public key to their own account.

  7. Question 7 of 19 Two host keys differ only in their last character. How do their SHA256 fingerprints compare?

    Choose one answer.

    Show the answer: Question 7 of 19

    Answer: They look completely different

    A fingerprint is a cryptographic digest of the key, so any change gives an unrelated result. In this lesson's session, one changed character turned SHA256:+DiY3… into SHA256:Xpui/…. Compare every character, not only a few at each end.

  8. Question 8 of 19 On your first ssh -T git@github.com, the fingerprint shown is not on GitHub's page "GitHub's SSH key fingerprints". What do you do?

    Choose one answer.

    Show the answer: Question 8 of 19

    Answer: Answer no, and work out what is between you and GitHub before connecting again

    The fingerprint identifies the server's key, not yours. A key that is not on GitHub's published list may belong to a machine pretending to be github.com, and answering yes would store it as trusted.

  9. Question 9 of 19 ssh stops with "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!" for github.com, and GitHub has announced no change of its host keys. What do you do?

    Choose one answer.

    Show the answer: Question 9 of 19

    Answer: Stop, and connect only after the key offered matches GitHub's published fingerprints

    The stored key no longer matches the key the server offers. With an announced change, you check the new fingerprint and replace the old entry; without one, something between you and GitHub may be pretending to be it, and removing the old key would only hide the warning.

  10. Question 10 of 19 Which ~/.ssh/config block sends every SSH connection for github.com through port 443?

    Choose one answer.

    Show the answer: Question 10 of 19

    Answer: Host github.com with Hostname ssh.github.com, Port 443 and User git

    GitHub accepts SSH on port 443 at ssh.github.com. The Host github.com block points the name your remotes use at that host and port, so no URL has to change.

  11. Question 11 of 19 On the campus network, git push over SSH hangs and then times out, while github.com opens fine in the browser. What should you try?

    Choose one answer.

    Show the answer: Question 11 of 19

    Answer: SSH over port 443 to ssh.github.com, with a Host block in ~/.ssh/config

    A network that blocks port 22 still lets HTTPS through on port 443, and GitHub accepts SSH there at ssh.github.com; github.com itself does not answer SSH on 443. HTTPS with a credential helper is the other way out.

  12. Question 12 of 19 Your first connection through port 443 says the host key for [ssh.github.com]:443 is not known. What do you do?

    Choose one answer.

    Show the answer: Question 12 of 19

    Answer: Check its fingerprint against GitHub's published list, as for any first connection

    ssh stores host keys by name and port, so the same GitHub key on [ssh.github.com]:443 counts as a new entry. Compare its fingerprint with GitHub's page before you accept it, exactly as on the first connection to github.com.

  13. Question 13 of 19 You signed in with gh auth login, but git push over HTTPS still asks for a user name. Which command makes Git use GitHub CLI's sign-in?

    Choose one answer.

    Show the answer: Question 13 of 19

    Answer: gh auth setup-git

    gh auth login stores a token for gh itself. gh auth setup-git then makes Git ask gh for credentials, so Git uses that token without you typing it.

  14. Question 14 of 19 Which credential helper comes with Git for Windows?

    Choose one answer.

    Show the answer: Question 14 of 19

    Answer: Git Credential Manager

    Git Credential Manager comes with Git for Windows; on macOS it installs with Homebrew, and on Linux as a .NET tool or a package. GitHub CLI is a separate install.

  15. Question 15 of 19 A script on your laptop needs to read and write one private repository of yours over HTTPS. Which token fits best?

    Choose one answer.

    Show the answer: Question 15 of 19

    Answer: A fine-grained token with only that repository, Contents read and write, and an expiry date

    Least privilege: the token should open only what the script needs, and stop working on its own. GitHub recommends fine-grained tokens, recommends an expiry, and no longer accepts account passwords for Git.

  16. Question 16 of 19 A script needs a token for a two-week project that writes to one repository. Which expiry do you choose?

    Choose one answer.

    Show the answer: Question 16 of 19

    Answer: A custom date just after the two weeks

    GitHub highly recommends an expiry: a token that stops working on its own limits the damage of a leak nobody noticed. Match it to the job, and create a new one if the project runs longer.

  17. Question 17 of 19 A reporting tool's setup page asks for a classic token with the repo scope and no expiry, but it only reads one private repository. What do you give it?

    Choose one answer.

    Show the answer: Question 17 of 19

    Answer: A fine-grained token for that one repository, with Contents read-only and an expiry

    Give a tool the least access its job needs: here read access to one repository, through a fine-grained token that expires. Ask for a classic token only for the few things that still need one.

  18. Question 18 of 19 You run git remote add origin https://you:TOKEN@github.com/you/notes.git. Where can the token now be found? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 18 of 19

    Answer:

    • In your shell history
    • In the output of git remote -v
    • In .git/config, as plain text

    The URL, token and all, is stored in the repository's configuration, printed by git remote -v and kept in your shell history. A credential helper keeps the token out of all three.

  19. Question 19 of 19 You pasted a personal access token into a group chat by mistake. What is the first thing to do?

    Choose one answer.

    Show the answer: Question 19 of 19

    Answer: Delete the token in your GitHub settings, then create a new one

    Anyone who saw or copied the message can use the token until it is revoked, so revocation comes first. Then check your security log, issue a replacement with the least access it needs, and clean up the message.

Interview questions

Warm-up (fresher): Why should you check a host key fingerprint before answering “yes” to a first SSH connection to GitHub?

Because that “yes” is the only moment ssh cannot check the server for you. On a first connection there is no stored key for github.com, so ssh shows the fingerprint of whatever key the server offered and asks whether to trust it. If an attacker on the network were answering instead of GitHub, accepting their key would let them see and change your Git traffic, and ssh would accept that key quietly from then on, because it is now the stored one. Comparing the fingerprint with the list GitHub publishes on its documentation site, all 43 characters, turns the blind trust into a check. After that, ssh compares the stored key on every connection and warns loudly when it changes, as it did for RSA users when GitHub replaced that key.

Two harder questions start from this lesson: a token pasted into a busy team chat, and a network that blocks port 22.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A teammate pasted a fine-grained personal access token with write access to your team's main repository into a busy team chat ten minutes ago. Walk through what you do in the next fifteen minutes, in order, and what you change so that it cannot happen again.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    On a college network, git push over SSH hangs for about a minute and then fails, while github.com opens normally in the browser. How do you confirm what is wrong, and how do you keep using your SSH key on that network?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An account that contributes code must turn on two-factor authentication within GitHub’s deadline: a TOTP app first, a passkey or a security key as a backup, and the 16 recovery codes kept in a password manager.
  • Your noreply address, ID+USERNAME@users.noreply.github.com, set as user.email, keeps your real address out of your commits.
  • ssh-keygen -t ed25519 makes a key pair whose private half stays on your computer behind a passphrase that the agent remembers, and GitHub gets only the .pub file.
  • Before the first yes, the whole of GitHub’s host key fingerprint is compared with the list GitHub publishes, and when port 22 is blocked, SSH goes to ssh.github.com on port 443.
  • Over HTTPS, Git Credential Manager or GitHub CLI signs you in and keeps the token, and a script that needs a token of its own gets a fine-grained one, limited to one repository, with an expiry.
  • A token never goes into a remote URL, and a secret that has been exposed is revoked before anything else is cleaned up.

Terms in this lesson

credential helper
A program that Git asks for a user name and password when an HTTPS remote needs them. It keeps what it gets, such as a token, in a store, and erases it when the server refuses it. Git Credential Manager and GitHub CLI are two.
host key
The key pair that identifies an SSH server. On every connection the server proves it holds the private half, and ssh compares the public half with the copy stored in known_hosts, or asks you to check its fingerprint the first time.
key fingerprint
A short digest of a public key, such as SHA256: followed by 43 characters, used to compare keys by eye. Any change to the key gives a completely different fingerprint.
personal access token
A secret that GitHub issues to stand in for your password when Git, a script or a tool signs in over HTTPS. A fine-grained token is limited to chosen repositories and permissions, and should expire.
SSH agent
A program that holds your unlocked SSH private keys while you are signed in, so that ssh can sign with a key without asking for its passphrase every time.
two-factor authentication (2FA)
Signing in with two different things: something you know, your password, and something you have, such as an authenticator app, a passkey or a security key. A stolen password alone then opens nothing.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.