---
title: "Enforce SSH instead of HTTPS for Git operations"
description: "GitHub stopped accepting passwords for Git in August 2021. A token in a credential helper solves the typing but not the expiry; a key solves both, and reaches the HTTPS URLs you did not write yourself."
author: "Omar Albeik"
date: 2021-08-23
type: note
topics: [software, engineering]
language: en
reading_time_minutes: 4
canonical_url: https://omaralbeik.com/en/blog/enforce-ssh-instead-of-http-for-git-operations
translation_url: https://omaralbeik.com/ar/blog/enforce-ssh-instead-of-http-for-git-operations
source_url: https://omaralbeik.com/en/blog/enforce-ssh-instead-of-http-for-git-operations.md
---

# Enforce SSH instead of HTTPS for Git operations

Ten days ago GitHub stopped accepting account passwords for Git operations over
HTTPS. If you push over HTTPS you now paste a personal access token instead: a
longer secret, kept wherever the password was, expiring on a schedule you have
to remember.

A credential helper takes the typing out of that — `osxkeychain`, Git
Credential Manager, `gh auth login` — and if tokens suit you, that is a fine
answer. What it cannot take out is the expiry. Every few months the token
lapses and you are back on the settings page, generating another one and
trying to remember which scopes it had.

An SSH key has no expiry date. You generate a pair once, keep the private half
on your machine, hand the public half to the host, and the arrangement holds
until you revoke it. It also reaches somewhere a credential helper cannot: the
HTTPS URLs you did not write, in repositories and lockfiles belonging to other
people.

The first two steps below are in every guide. The two after them are the ones
that make this stick.

## Generate a key

```bash
ssh-keygen -t ed25519 -C "personal github" -f ~/.ssh/personal-github
```

Ed25519 rather than RSA: shorter keys, faster handshakes, and no key-size
argument to get wrong. For anything older than OpenSSH 6.5, `-t rsa -b 4096`
is the fallback.

Name the key after what it unlocks — `personal-github`, `work-gitlab` — and run
`ssh-keygen` once per account. GitHub refuses a public key already registered
to another account, so two accounts force two keys whether you planned for them
or not. Generate separately on each machine as well, and keep every private
half where it was made: a key that exists in one place is a key you can revoke
in one place. `id_ed25519_2` tells you nothing on the day you need to revoke it.

Use a passphrase. Loaded into `ssh-agent` it costs one prompt per session
rather than one per push.

## Add the public key to the host

Copy the public half, the file ending in `.pub`. The one without an extension
stays where it was generated:

```bash
# macOS
pbcopy < ~/.ssh/personal-github.pub

# Linux
xclip -selection clipboard < ~/.ssh/personal-github.pub
```

Then paste it into your account settings:
[GitHub](https://docs.github.com/en/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account),
[GitLab](https://docs.gitlab.com/ee/ssh/), or
[Bitbucket](https://support.atlassian.com/bitbucket-cloud/docs/set-up-an-ssh-key/).

## Tell SSH which key to use

Left to itself, SSH tries the default names — `id_ed25519`, `id_rsa` — so a key
called `personal-github` is never offered. In `~/.ssh/config`:

```ssh-config
Host github.com
  User git
  IdentityFile ~/.ssh/personal-github
  IdentitiesOnly yes
  AddKeysToAgent yes

Host gitlab.com
  User git
  IdentityFile ~/.ssh/work-gitlab
  IdentitiesOnly yes
  AddKeysToAgent yes
```

`IdentitiesOnly yes` earns its line. Without it SSH offers every key the agent
holds, and GitHub authenticates you as whichever account matches the first one
that fits — so a repository you have access to comes back as "not found". It
reads as a permissions bug and is not one.

On macOS, `UseKeychain yes` in each block keeps the passphrase in the login
keychain across restarts. It belongs to Apple's OpenSSH rather than upstream,
so if Homebrew's `openssh` is ahead on your `PATH` it will refuse to start.
Load the key into the keychain once with `ssh-add -K ~/.ssh/personal-github`.

Connect once, both to check the key and to answer the question SSH asks the
first time it meets a host:

```bash
ssh -T git@github.com
```

The prompt shows the server's host key fingerprint. Compare it against the
fingerprints GitHub publishes before answering `yes`: that prompt is the whole
of SSH's server authentication, and it is the easiest thing on this page to
answer by reflex. After it, a greeting with your username means the key works.

If the connection hangs instead, the network is probably blocking port 22.
GitHub answers SSH on 443 as well:

```ssh-config
Host github.com
  Hostname ssh.github.com
  Port 443
  # ...the rest of the block as above
```

Worth setting before you need it. After the next step, HTTPS is no longer
available as a fallback.

## Rewrite HTTPS URLs to SSH

A key does nothing while your remotes still say `https://`. Rather than fixing
them one repository at a time, tell Git to substitute, in `~/.gitconfig`:

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

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

[url "ssh://git@bitbucket.org/"]
  insteadOf = https://bitbucket.org/
```

Every HTTPS URL for those hosts is now rewritten before the request leaves your
machine, including the URLs you did not write yourself. Submodules are the case
that pays for the whole thing: a `.gitmodules` committed by someone else,
pointing at HTTPS, in a repository you have no say over. The same goes for any
dependency that names a Git repository by HTTPS URL — a pod with a `:git =>`
line, a `go get` under `GOPRIVATE`, an npm dependency spelled `git+https://`.

Two things it leaves alone, both correctly. Package managers that fetch an
archive rather than clone never hand Git a URL at all: CocoaPods moved to a CDN
in 1.8, the Go proxy serves public modules, and npm's `github:user/repo`
shorthand resolves to a codeload tarball. And this is `~/.gitconfig` on your
machine, so a CI runner with its own home directory will never see it. That is
the outcome you want. A build server should authenticate with a scoped token
over HTTPS, not with your personal key.

## Check the rewrite

Substitution happens inside Git, before the network, so you can watch it
without connecting to anything:

```bash
git ls-remote --get-url https://github.com/omaralbeik/SwifterSwift.git
```

If that prints `ssh://git@github.com/omaralbeik/SwifterSwift.git`, the rewrite
is live. Reach for this rather than cloning a public repository as a test:
public clones over HTTPS succeed without credentials either way, so a clean
clone proves nothing about whether the substitution fired.

That is the whole arrangement — Git rewrites the URL, SSH picks the key that
belongs to the host. Nothing expires, and nothing to paste.
