Skip to content

Commit

Permalink
GitHub proxy: download GitHub server fingerprints
Browse files Browse the repository at this point in the history
  • Loading branch information
greedy52 committed Jan 8, 2025
1 parent e4dc0c5 commit aa15ac6
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 18 deletions.
2 changes: 1 addition & 1 deletion lib/srv/git/forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ func verifyRemoteHost(targetServer types.Server) ssh.HostKeyCallback {
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
switch targetServer.GetSubKind() {
case types.SubKindGitHub:
return VerifyGitHubHostKey(hostname, remote, key)
return githubFingerprints.checkServerKey(key)
default:
return trace.BadParameter("unsupported subkind %q", targetServer.GetSubKind())
}
Expand Down
144 changes: 127 additions & 17 deletions lib/srv/git/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ package git

import (
"context"
"net"
"encoding/json"
"log/slog"
"maps"
"net/http"
"slices"
"sync"
"time"

"github.com/gravitational/trace"
Expand All @@ -30,35 +34,141 @@ import (
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/durationpb"

"github.com/gravitational/teleport"
integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/sshutils"
)

// knownGithubDotComFingerprints contains a list of known GitHub fingerprints.
//
// https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
//
// TODO(greedy52) these fingerprints can change (e.g. GitHub changed its RSA
// key in 2023 because of an incident). Instead of hard-coding the values, we
// should try to periodically (e.g. once per day) poll them from the API.
var knownGithubDotComFingerprints = []string{
"SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s",
"SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM",
"SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU",
type githubMetadataClient interface {
fetchETag() (string, error)
fetchFingerprints() ([]string, string, error)
}

// VerifyGitHubHostKey is an ssh.HostKeyCallback that verifies the host key
// belongs to "github.com".
func VerifyGitHubHostKey(_ string, _ net.Addr, key ssh.PublicKey) error {
// githubFingerprintManager downloads SSH fingerprints from the GitHub meta API
// and does a lazy refresh every hour. The fingerprints are used to verify
// GitHub server when forwarding Git commands to it.
type githubFingerprintManager struct {
mu sync.RWMutex
fingerprints []string
lastCheck time.Time
etag string

clock clockwork.Clock
client githubMetadataClient
}

func newGithubFingerprintManager() *githubFingerprintManager {
return &githubFingerprintManager{
clock: clockwork.NewRealClock(),
client: newGithubMetadataTTPClient(),
}
}

func (g *githubFingerprintManager) checkServerKey(key ssh.PublicKey) error {
actualFingerprint := ssh.FingerprintSHA256(key)
if slices.Contains(knownGithubDotComFingerprints, actualFingerprint) {
return nil
for _, fingerprint := range g.getKnownFingerprints() {
if sshutils.EqualFingerprints(actualFingerprint, fingerprint) {
return nil
}
}
return trace.BadParameter("cannot verify github.com: unknown fingerprint %v algo %v", actualFingerprint, key.Type())
}

func (g *githubFingerprintManager) getKnownFingerprints() []string {
const refreshDuration = time.Hour
g.mu.RLock()
if g.clock.Now().Sub(g.lastCheck) < refreshDuration {
defer g.mu.RUnlock()
return g.fingerprints
}
g.mu.RUnlock()

g.mu.Lock()
defer g.mu.Unlock()
if g.clock.Now().Sub(g.lastCheck) < refreshDuration {
return g.fingerprints
}

logger := slog.With(teleport.ComponentKey, "git:github")

// Check if eTag is the same to avoid downloading the whole thing which
// contains a lot of irrelevant info.
if g.etag != "" {
etag, err := g.client.fetchETag()
switch {
case err != nil:
logger.WarnContext(context.Background(), "Failed to fetch eTag from GitHub meta API", "error", err)
// Don't give up yet if HEAD fails.

case etag == g.etag:
g.lastCheck = g.clock.Now()
logger.DebugContext(context.Background(), "ETag did not change for GitHub meta API")
return g.fingerprints

default:
logger.DebugContext(context.Background(), "ETag changed for GitHub meta API", "new", etag)
}
}

fingerprints, etag, err := g.client.fetchFingerprints()
if err != nil {
logger.WarnContext(context.Background(), "Failed to fetch fingerprints from GitHub meta API", "error", err)
return g.fingerprints
}
logger.DebugContext(context.Background(), "Found SSH fingerprints from Github meta API", "fingerprints", fingerprints, "etag", etag)
g.etag = etag
g.fingerprints = fingerprints
g.lastCheck = g.clock.Now()
return g.fingerprints
}

var githubFingerprints = newGithubFingerprintManager()

type githubMetadataHTTPClient struct {
api string
client *http.Client
}

func newGithubMetadataTTPClient() *githubMetadataHTTPClient {
return &githubMetadataHTTPClient{
api: "https://api.github.com/meta",
client: &http.Client{
Timeout: defaults.HTTPRequestTimeout,
},
}
}

func (c *githubMetadataHTTPClient) fetchETag() (string, error) {
resp, err := http.Head(c.api)
if err != nil {
return "", trace.Wrap(err)
}
return resp.Header.Get("ETag"), nil
}

func (c *githubMetadataHTTPClient) fetchFingerprints() ([]string, string, error) {
resp, err := http.Get(c.api)
if err != nil {
return nil, "", trace.Wrap(err)
}
defer resp.Body.Close()

// Meta API reference:
// https://docs.github.com/en/rest/meta/meta?apiVersion=2022-11-28#get-github-meta-information
meta := struct {
// Fingerprints lists the fingerprints by algo type.
Fingerprints map[string]string `json:"ssh_key_fingerprints"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&meta); err != nil {
return nil, "", trace.Wrap(err)
}

return slices.Collect(maps.Values(meta.Fingerprints)), resp.Header.Get("ETag"), nil
}

// AuthPreferenceGetter is an interface for retrieving the current configured
// cluster auth preference.
type AuthPreferenceGetter interface {
Expand Down

0 comments on commit aa15ac6

Please sign in to comment.