Skip to content

Commit

Permalink
GitHub proxy part 6: proxing Git using SSH transport
Browse files Browse the repository at this point in the history
  • Loading branch information
greedy52 committed Dec 10, 2024
1 parent b5d38f5 commit 73db1b8
Show file tree
Hide file tree
Showing 31 changed files with 1,062 additions and 44 deletions.
18 changes: 17 additions & 1 deletion api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4880,11 +4880,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
6 changes: 6 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 Expand Up @@ -781,6 +784,9 @@ const (
// SSHSessionJoinPrincipal is the SSH principal used when joining sessions.
// This starts with a hyphen so it isn't a valid unix login.
SSHSessionJoinPrincipal = "-teleport-internal-join"

// SSHGitPrincipal is the SSH principal used when proxying Git commands.
SSHGitPrincipal = "-teleport-git"
)

const (
Expand Down
1 change: 1 addition & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3209,6 +3209,7 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types.
if githubIdentities := req.user.GetGithubIdentities(); len(githubIdentities) > 0 {
githubUserID = githubIdentities[0].UserID
githubUsername = githubIdentities[0].Username
allowedLogins = append(allowedLogins, teleport.SSHGitPrincipal)
}

var signedSSHCert []byte
Expand Down
9 changes: 9 additions & 0 deletions lib/auth/authclient/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,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 @@ -418,6 +421,9 @@ type ReadRemoteProxyAccessPoint interface {

// GetDatabaseServers returns all registered database proxy servers.
GetDatabaseServers(ctx context.Context, namespace string, opts ...services.MarshalOption) ([]types.DatabaseServer, error)

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

// RemoteProxyAccessPoint is an API interface implemented by a certificate authority (CA) to be
Expand Down Expand Up @@ -1245,6 +1251,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
}
1 change: 1 addition & 0 deletions lib/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,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
9 changes: 9 additions & 0 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 @@ -189,6 +193,8 @@ var (
EC2InstanceConnect: Ed25519,
// GitHubProxyCASSH uses same algorithms as UserCASSH.
GitHubProxyCASSH: RSA2048,
// GitClient uses same algorithms as UserCASSH.
GitClient: RSA2048,
}

// balancedV1 strikes a balance between security, compatibility, and
Expand Down Expand Up @@ -220,6 +226,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 +258,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 +292,7 @@ var (
ProxyKubeClient: ECDSAP256,
EC2InstanceConnect: Ed25519,
GitHubProxyCASSH: ECDSAP256,
GitClient: ECDSAP256,
}

allSuites = map[types.SignatureAlgorithmSuite]suite{
Expand Down
42 changes: 42 additions & 0 deletions 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 @@ -285,6 +286,11 @@ func (r *Router) DialHost(ctx context.Context, clientSrcAddr, clientDstAddr net.
return nil, trace.Wrap(err)
}
}
} else if target.GetGitHub() != nil {
// Forward to github.com directly.
agentGetter = nil
isAgentlessNode = true
serverAddr = types.GitHubSSHServerAddr
}

} else {
Expand Down Expand Up @@ -386,6 +392,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 +411,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 +435,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 {
return getGitHubServer(ctx, org, site)
}
return getServerWithResolver(ctx, host, port, site, nil /* use default resolver */)
}

Expand Down Expand Up @@ -568,3 +588,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
}
10 changes: 9 additions & 1 deletion lib/reversetunnel/localsite.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ func (s *localSite) NodeWatcher() (*services.GenericWatcher[types.Server, readon
return s.srv.NodeWatcher, nil
}

// GitServerWatcher returns a Git server watcher for this cluster.
func (s *localSite) GitServerWatcher() (*services.GenericWatcher[types.Server, readonly.Server], error) {
return s.srv.GitServerWatcher, nil
}

// GetClient returns a client to the full Auth Server API.
func (s *localSite) GetClient() (authclient.ClientI, error) {
return s.client, nil
Expand Down Expand Up @@ -240,6 +245,10 @@ func shouldDialAndForward(params reversetunnelclient.DialParams, recConfig types
if params.TargetServer != nil && params.TargetServer.IsOpenSSHNode() {
return true
}
// forward to "github.com" from Proxy
if params.TargetServer != nil && params.TargetServer.GetGitHub() != nil {
return true
}
// proxy session recording mode is being used and an SSH session
// is being requested, the connection must be forwarded
if params.ConnType == types.NodeTunnel && services.IsRecordAtProxy(recConfig.GetMode()) {
Expand All @@ -260,7 +269,6 @@ func (s *localSite) Dial(params reversetunnelclient.DialParams) (net.Conn, error
if shouldDialAndForward(params, recConfig) {
return s.dialAndForward(params)
}

// Attempt to perform a direct TCP dial.
return s.DialTCP(params)
}
Expand Down
Loading

0 comments on commit 73db1b8

Please sign in to comment.