18

I want to access my github repositories via ssh. When I access the repository for the first time, I am asked If I want to add the github ssh server to my known_hosts file, which works fine. That request also shows me the RSA key fingerprint of that server and I can manually verify that it is the same that is provided by github here.

These are the SHA256 hashes shown in OpenSSH 6.8 and newer (in base64 format):

SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 (RSA)
SHA256:br9IjFspm1vxR3iA35FWE+4VTyz1hYVLIE2t1/CeyWQ (DSA)

The problem is that I want to prevent that request by adding a public key to my known_hosts file before my first access to my git repository. This can be done by using the ssh-keyscan -t rsa www.github.com command which will give me a public key in the format required by the known_hosts file. But people mention repeatedly, that this is not safe and is vulnerable to man-in-the-middle attacks. What they do not mention is how to do it right.

So how can I use the RSA fingerprint provided on the github page to safely get the public host key of the ssh server? I am more or less looking for an option to the ssh-keyscan command that lets me add the expected rsa fingerprint and causes the command to fail if the hosts fingerprint does not match the given one.

Thank you for your time!

Steve Robinson
  • 452
  • 5
  • 9
Knitschi
  • 2,822
  • 3
  • 32
  • 51

5 Answers5

20

Warning March 2023:

"GitHub has updated its RSA SSH host key"


I would not use ssh-keyscan in that case.
Rather, I would use it and double-check the result by comparing its fingerprint with the one provided by GitHub.

And then proceed with an SSH GitHub test, to check I do get:

Hi username! You've successfully authenticated, but GitHub does not
provide shell access.

So, as recommended here, for the manual process:

ssh-keyscan github.com >> githubKey

Generate the fingerprint:

ssh-keygen -lf githubKey

Compare it with the ones provided by GitHub

Finally, copy githubKey content to your ~/.ssh/known_hosts file.


You can automate that process (still including the fingerprint step check) with wercker/step-add-to-known_hosts: it is a wercker step, but can be extrapolated as its own independent script.

- add-to-known_hosts:
    hostname: github.com
    fingerprint: 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48
    type: rsa

But that would lack the check against help.github.com/articles/github-s-ssh-key-fingerprints: see below.


Using nmap does not help much, as explained here:

using nmap to get the SSH host key fingerprint and then comparing it to what ssh-keyscan says the fingerprint: In both cases, the fingerprint comes from the same place.
It's just as vulnerable to MITM as any other of these automated solutions.

The only secure and valid way to verify an SSH public key is over some trusted out-of-band channel. (Or set up some kind of key-signing infrastructure.)

Here, help.github.com/articles/github-s-ssh-key-fingerprints remains the "trusted out-of-band channel".

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 1
    Ok, but how can I get the line that I have to add to my `known_hosts` file from the `SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8` fingerprint? Adding something like `gihub.com ssh-rsa SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8` does not work. This step in-between is exactly what I am missing. – Knitschi Apr 06 '18 at 11:00
  • 1
    @Knitschi I have revised my answer and added the missing steps. – VonC Apr 06 '18 at 11:12
6

Based on VonC's answer, the script below can verify and add the key automatically. Use it like this:

$ ./add-key.sh github.com nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8

It tells you whether it successfully verified and saved the fingerprint.
For usage info, use ./add-key.sh --help

The script:

#!/usr/bin/env bash

# Settings
knownhosts="$HOME/.ssh/known_hosts"

if [ "x$1" == "x-h" ] || [ "x$1" == "x--help" ] || [ ${#1} == 0 ]; then
    echo "Usage: $0 <host> <fingerprint> [<port>]"
    echo "Example: $0 github.com nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8"
    echo "The default port is 22."
    echo "The script will download the ssh keys from <host>, check if any match"
    echo "the <fingerprint>, and add that one to $knownhosts."
    exit 1
fi

# Argument handling
host=$1
fingerprint=$2
port=$(if [ -n "$3" ]; then echo "$3"; else echo 22; fi)

# Download the actual key (you cannot convert a fingerprint to the original key)
keys="$(ssh-keyscan -p $port $host |& grep -v ^\#)";
echo "$keys" | grep -v "^$host" # Show any errors
keys="$(echo "$keys" | grep "^$host")"; # Remove errors from the variable
if [ ${#keys} -lt 20 ]; then echo Error downloading keys; exit 2; fi

# Find which line contains the key matching this fingerprint
line=$(ssh-keygen -lf <(echo "$keys") | grep -n "$fingerprint" | cut -b 1-1)

if [ ${#line} -gt 0 ]; then  # If there was a matching fingerprint (todo: shouldn't this be -ge or so?)
    # Take that line
    key=$(head -$line <(echo "$keys") | tail -1)
    # Check if the key part (column 3) of that line is already in $knownhosts
    if [ -n "$(grep "$(echo "$key" | awk '{print $3}')" $knownhosts)" ]; then
        echo "Key already in $knownhosts."
        exit 3
    else
        # Add it to known hosts
        echo "$key" >> $knownhosts
        # And tell the user what kind of key they just added
        keytype=$(echo "$key" | awk '{print $2}')
        echo Fingerprint verified and $keytype key added to $knownhosts
    fi
else  # If there was no matching fingerprint
    echo MITM? These are the received fingerprints:
    ssh-keygen -lf <(echo "$keys")
    echo Generated from these received keys:
    echo "$keys"
    exit 1
fi
Luc
  • 5,339
  • 2
  • 48
  • 48
  • Nice addition to my answer. +1 – VonC Jan 08 '19 at 12:01
  • Would be awesome to have this also check if it's already there, so it can be run in a CI setup (e.g. on every commit) and not endlessly append to the known_hosts file. – Chris Trahey Feb 05 '19 at 03:23
  • @ChrisTrahey Good idea! Implemented. – Luc Feb 05 '19 at 10:37
  • 1
    This is good until `ssh-keyscan -p $port $host 2> /dev/null` fails and doesn't tell you why. I think using `ssh-keyscan github.com 2>&1 | grep -v '#'` would be a better idea. – NorseGaud Jan 07 '20 at 16:26
  • 1
    @NorseGaud Not exactly, and your proposal doesn't work. First off, the script will currently tell you that downloading failed and you can debug from there, but I agree that showing the error is better. The proposal of using `2>&1` just captures stderr (and can be written more simply as `command |& grep`) so when put into a variable (as the script does), the issue of suppressing stderr remains. One somehow has to pull stdout and stderr apart. I've edited the script to do that. – Luc Jan 08 '20 at 10:29
2

My one-liner allows for error reporting on failure:

touch ~/.ssh/known_hosts && if [ $(grep -c 'github.com ssh-rsa' ~/.ssh/known_hosts) -lt 1 ]; then KEYS=$(KEYS=$(ssh-keyscan github.com 2>&1 | grep -v '#'); ssh-keygen -lf <(echo $KEYS) || echo $KEYS); if [[ $KEYS =~ '(RSA)' ]]; then if [ $(curl -s https://help.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints | grep -c $(echo $KEYS | awk '{print $2}')) -gt 0 ]; then echo '[GitHub key successfully verified]' && ssh-keyscan github.com 1>~/.ssh/known_hosts; fi; else echo \"ssh-keygen -lf failed:\\n$KEYS\"; exit 1; fi; unset KEYS; fi
NorseGaud
  • 355
  • 1
  • 11
  • 4
    Note that this works only for `github.com` and is not a general solution for securely adding keys based on fingerprints. I'm also not sure why it has to be one huge, unreadable line... – Luc Jan 08 '20 at 10:29
  • @Luc One liner allows you to throw it into CI scripts and not bloat them. – NorseGaud Apr 23 '20 at 13:41
  • 7
    I would hate to audit your CI setup :P – Luc Apr 23 '20 at 14:52
2

GitHub now offers this information in its Meta API, see About GitHub's IP addresses. The JSON output includes the public SSH keys, so assuming your HTTPS client correctly verifies the certificate chain, you can fetch the keys from there.

Below is an Ansible task that accomplishes this:

# Copyright 2022 Google LLC.
# SPDX-License-Identifier: Apache-2.0
- name: Add github.com public keys to known_hosts
  ansible.builtin.known_hosts:
    path: /etc/ssh/ssh_known_hosts
    name: github.com
    # Download the keys from the GitHub API and prepend 'github.com' to them to
    # match the known_hosts format.
    key: |
      {% for key in (lookup('ansible.builtin.url',
                            'https://api.github.com/meta',
                            split_lines=False, validate_certs=True)
                     |from_json)['ssh_keys'] %}
      github.com {{ key }}
      {% endfor %}

Petr
  • 62,528
  • 13
  • 153
  • 317
0

I've used the following line taken from here

ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts

Aliaksei
  • 1,094
  • 11
  • 20