Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GitHub proxy part 6: proxing Git using SSH transport #49980

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4892,11 +4892,27 @@ func (c *Client) UserTasksServiceClient() *usertaskapi.Client {
return usertaskapi.NewClient(usertaskv1.NewUserTaskServiceClient(c.conn))
}

// GitServerClient returns a client for managing git servers
// GitServerClient returns a client for managing Git servers
func (c *Client) GitServerClient() *gitserverclient.Client {
return gitserverclient.NewClient(gitserverpb.NewGitServerServiceClient(c.conn))
}

// ListGitServers returns all Git servers matching filter.
// This method is available from GitServerClient but also defined here for
// Client to satisfy some read only interfaces like
// authclient.ReadProxyAccessPoint.
func (c *Client) ListGitServers(ctx context.Context, pageSize int, pageToken string) ([]types.Server, string, error) {
return c.GitServerClient().ListGitServers(ctx, pageSize, pageToken)
}

// GetGitServer returns Git servers by name.
// This method is available from GitServerClient but also defined here for
// Client to satisfy some read only interfaces like
// authclient.ReadProxyAccessPoint.
func (c *Client) GetGitServer(ctx context.Context, name string) (types.Server, error) {
return c.GitServerClient().GetGitServer(ctx, name)
}

// GetCertAuthority retrieves a CA by type and domain.
func (c *Client) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool) (types.CertAuthority, error) {
ca, err := c.TrustClient().GetCertAuthority(ctx, &trustpb.GetCertAuthorityRequest{
Expand Down
3 changes: 3 additions & 0 deletions api/types/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import (
const (
GithubURL = "https://github.com"
GithubAPIURL = "https://api.github.com"

// GitHubSSHServerAddr is the public SSH address for "github.com".
GitHubSSHServerAddr = "github.com:22"
)

// GithubConnector defines an interface for a Github OAuth2 connector
Expand Down
3 changes: 3 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ const (
// ComponentUpdater represents the teleport-update binary.
ComponentUpdater = "updater"

// ComponentForwardingGit represents the SSH proxy that forwards Git commands.
ComponentForwardingGit = "git:forward"

// VerboseLogsEnvVar forces all logs to be verbose (down to DEBUG level)
VerboseLogsEnvVar = "TELEPORT_DEBUG"

Expand Down
6 changes: 6 additions & 0 deletions lib/auth/authclient/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ type ReadProxyAccessPoint interface {

// GetAutoUpdateAgentRollout gets the AutoUpdateAgentRollout from the backend.
GetAutoUpdateAgentRollout(ctx context.Context) (*autoupdate.AutoUpdateAgentRollout, error)

// GitServerGetter defines a service to get Git servers.
services.GitServerGetter
}

// SnowflakeSessionWatcher is watcher interface used by Snowflake web session watcher.
Expand Down Expand Up @@ -1252,6 +1255,9 @@ type Cache interface {

// GetPluginStaticCredentialsByLabels will get a list of plugin static credentials resource by matching labels.
GetPluginStaticCredentialsByLabels(ctx context.Context, labels map[string]string) ([]types.PluginStaticCredentials, error)

// GitServerGetter defines methods for fetching Git servers.
services.GitServerGetter
}

type NodeWrapper struct {
Expand Down
3 changes: 3 additions & 0 deletions lib/auth/authclient/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -1897,4 +1897,7 @@ type ClientI interface {

// GitServerClient returns git server client.
GitServerClient() *gitserver.Client

// GitServerGetter defines a service to get Git servers.
services.GitServerGetter
Comment on lines +1904 to +1905
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ClientI already has a way to get a gitserver.Client which has GetGitServer and ListGitServers, why do we need to extend ClientI with methods at the top level?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are casting ClientI to ProxyAccessPoint in a few places, which forces authclient to have same interface as Cache. Also gitserver.Client is not suitable for "AccessPoint".

one other option is to have a gitserver.ReadOnlyClient or decouple ClientI from ProxyAccessPoint somehow. any recommendation is welcome.

}
1 change: 1 addition & 0 deletions lib/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ func ForRemoteProxy(cfg Config) Config {
{Kind: types.KindDatabaseServer},
{Kind: types.KindDatabaseService},
{Kind: types.KindKubeServer},
{Kind: types.KindGitServer},
}
cfg.QueueSize = defaults.ProxyQueueSize
return cfg
Expand Down
11 changes: 9 additions & 2 deletions lib/cryptosuites/suites.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ const (
// GitHubProxyCASSH represents the SSH key for GitHub proxy CAs.
GitHubProxyCASSH

// GitClient represents a key used to forward Git commands to Git services
// like GitHub.
GitClient

// keyPurposeMax is 1 greater than the last valid key purpose, used to test that all values less than this
// are valid for each suite.
keyPurposeMax
Expand Down Expand Up @@ -187,8 +191,8 @@ var (
ProxyKubeClient: RSA2048,
// EC2InstanceConnect has always used Ed25519 by default.
EC2InstanceConnect: Ed25519,
// GitHubProxyCASSH uses same algorithms as UserCASSH.
GitHubProxyCASSH: RSA2048,
GitHubProxyCASSH: Ed25519,
GitClient: Ed25519,
}

// balancedV1 strikes a balance between security, compatibility, and
Expand Down Expand Up @@ -220,6 +224,7 @@ var (
ProxyKubeClient: ECDSAP256,
EC2InstanceConnect: Ed25519,
GitHubProxyCASSH: Ed25519,
GitClient: Ed25519,
}

// fipsv1 is an algorithm suite tailored for FIPS compliance. It is based on
Expand Down Expand Up @@ -251,6 +256,7 @@ var (
ProxyKubeClient: ECDSAP256,
EC2InstanceConnect: ECDSAP256,
GitHubProxyCASSH: ECDSAP256,
GitClient: ECDSAP256,
}

// hsmv1 in an algorithm suite tailored for clusters using an HSM or KMS
Expand Down Expand Up @@ -284,6 +290,7 @@ var (
ProxyKubeClient: ECDSAP256,
EC2InstanceConnect: Ed25519,
GitHubProxyCASSH: ECDSAP256,
GitClient: ECDSAP256,
greedy52 marked this conversation as resolved.
Show resolved Hide resolved
}

allSuites = map[types.SignatureAlgorithmSuite]suite{
Expand Down
46 changes: 45 additions & 1 deletion lib/proxy/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"context"
"errors"
"fmt"
"math/rand/v2"
"net"
"os"
"sync"
Expand Down Expand Up @@ -286,7 +287,14 @@ func (r *Router) DialHost(ctx context.Context, clientSrcAddr, clientDstAddr net.
}
}
}

if target.GetKind() == types.KindGitServer {
switch target.GetSubKind() {
greedy52 marked this conversation as resolved.
Show resolved Hide resolved
case types.SubKindGitHub:
serverAddr = types.GitHubSSHServerAddr
default:
return nil, trace.NotImplemented("unsupported git server subkind %q", target.GetSubKind())
}
}
} else {
return nil, trace.ConnectionProblem(errors.New("connection problem"), "direct dialing to nodes not found in inventory is not supported")
}
Expand Down Expand Up @@ -386,6 +394,7 @@ func (r *Router) getRemoteCluster(ctx context.Context, clusterName string, check
type site interface {
GetNodes(ctx context.Context, fn func(n readonly.Server) bool) ([]types.Server, error)
GetClusterNetworkingConfig(ctx context.Context) (types.ClusterNetworkingConfig, error)
GetGitServers(context.Context, func(readonly.Server) bool) ([]types.Server, error)
}

// remoteSite is a site implementation that wraps
Expand All @@ -404,6 +413,16 @@ func (r remoteSite) GetNodes(ctx context.Context, fn func(n readonly.Server) boo
return watcher.CurrentResourcesWithFilter(ctx, fn)
}

// GetGitServers uses the wrapped sites GitServerWatcher to filter git servers.
func (r remoteSite) GetGitServers(ctx context.Context, fn func(n readonly.Server) bool) ([]types.Server, error) {
watcher, err := r.site.GitServerWatcher()
if err != nil {
return nil, trace.Wrap(err)
}

return watcher.CurrentResourcesWithFilter(ctx, fn)
}

// GetClusterNetworkingConfig uses the wrapped sites cache to retrieve the ClusterNetworkingConfig
func (r remoteSite) GetClusterNetworkingConfig(ctx context.Context) (types.ClusterNetworkingConfig, error) {
ap, err := r.site.CachingAccessPoint()
Expand All @@ -418,6 +437,9 @@ func (r remoteSite) GetClusterNetworkingConfig(ctx context.Context) (types.Clust
// getServer attempts to locate a node matching the provided host and port in
// the provided site.
func getServer(ctx context.Context, host, port string, site site) (types.Server, error) {
if org, ok := types.GetGitHubOrgFromNodeAddr(host); ok {
greedy52 marked this conversation as resolved.
Show resolved Hide resolved
return getGitHubServer(ctx, org, site)
}
return getServerWithResolver(ctx, host, port, site, nil /* use default resolver */)
}

Expand Down Expand Up @@ -568,3 +590,25 @@ func (r *Router) GetSiteClient(ctx context.Context, clusterName string) (authcli
}
return site.GetClient()
}

func getGitHubServer(ctx context.Context, gitHubOrg string, site site) (types.Server, error) {
servers, err := site.GetGitServers(ctx, func(s readonly.Server) bool {
github := s.GetGitHub()
return github != nil && github.Organization == gitHubOrg
})
if err != nil {
return nil, trace.Wrap(err)
}

switch len(servers) {
case 0:
return nil, trace.NotFound("unable to locate Git server for GitHub organization %s", gitHubOrg)
case 1:
return servers[0], nil
default:
// It's unusual but possible to have multiple servers per organization
// (e.g. possibly a second Git server for a manual CA rotation). Pick a
// random one.
return servers[rand.N(len(servers))], nil
}
}
53 changes: 51 additions & 2 deletions lib/proxy/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"

Expand All @@ -43,8 +44,9 @@ import (
)

type testSite struct {
cfg types.ClusterNetworkingConfig
nodes []types.Server
cfg types.ClusterNetworkingConfig
nodes []types.Server
gitServers []types.Server
}

func (t testSite) GetClusterNetworkingConfig(ctx context.Context) (types.ClusterNetworkingConfig, error) {
Expand All @@ -61,6 +63,16 @@ func (t testSite) GetNodes(ctx context.Context, fn func(n readonly.Server) bool)

return out, nil
}
func (t testSite) GetGitServers(ctx context.Context, fn func(n readonly.Server) bool) ([]types.Server, error) {
var out []types.Server
for _, s := range t.gitServers {
if fn(s) {
out = append(out, s)
}
}

return out, nil
}

type server struct {
name string
Expand Down Expand Up @@ -326,6 +338,11 @@ func TestGetServers(t *testing.T) {
},
})

gitServers := []types.Server{
makeGitHubServer(t, "org1"),
makeGitHubServer(t, "org2"),
}

// ensure tests don't have order-dependence
rand.Shuffle(len(servers), func(i, j int) {
servers[i], servers[j] = servers[j], servers[i]
Expand Down Expand Up @@ -462,6 +479,28 @@ func TestGetServers(t *testing.T) {
require.Empty(t, srv)
},
},
{
name: "git server",
site: testSite{cfg: &unambiguousCfg, gitServers: gitServers},
host: "org2.github-org",
errAssertion: require.NoError,
serverAssertion: func(t *testing.T, srv types.Server) {
require.NotNil(t, srv)
require.NotNil(t, srv.GetGitHub())
assert.Equal(t, "org2", srv.GetGitHub().Organization)
},
},
{
name: "git server not found",
site: testSite{cfg: &unambiguousCfg, gitServers: gitServers},
host: "org-not-found.github-org",
errAssertion: func(t require.TestingT, err error, i ...interface{}) {
require.True(t, trace.IsNotFound(err), i...)
},
serverAssertion: func(t *testing.T, srv types.Server) {
require.Nil(t, srv)
},
},
}

ctx := context.Background()
Expand Down Expand Up @@ -874,3 +913,13 @@ func TestRouter_DialSite(t *testing.T) {
})
}
}

func makeGitHubServer(t *testing.T, org string) types.Server {
t.Helper()
server, err := types.NewGitHubServer(types.GitHubServerMetadata{
Integration: org,
Organization: org,
})
require.NoError(t, err)
return server
}
Loading
Loading