From 2345ca39f44ec80265514e9a4b674e000c6cb826 Mon Sep 17 00:00:00 2001 From: STeve Huang Date: Mon, 16 Dec 2024 11:22:35 -0500 Subject: [PATCH] refactor --- go.mod | 9 +- go.sum | 20 +- lib/auth/authclient/api.go | 3 - lib/proxy/router.go | 14 +- lib/reversetunnel/localsite.go | 76 +++- lib/reversetunnel/remotesite.go | 4 +- lib/reversetunnel/srv.go | 16 - lib/services/role.go | 24 ++ lib/srv/authhandlers.go | 49 +-- lib/srv/ctx.go | 3 - lib/srv/exec.go | 50 +-- lib/srv/forward/sshserver.go | 63 +--- lib/srv/git/audit.go | 52 --- lib/srv/git/forward.go | 590 ++++++++++++++++++++++++++++++++ lib/srv/git/forward_test.go | 360 +++++++++++++++++++ lib/srv/git/git.go | 108 ------ lib/srv/git/git_test.go | 140 -------- lib/srv/git/github_test.go | 15 +- lib/srv/reexec.go | 2 +- lib/sshutils/exec.go | 59 ++++ lib/sshutils/reply.go | 113 ++++++ lib/sshutils/server.go | 7 + lib/sshutils/utils.go | 20 ++ lib/utils/utils.go | 4 +- 24 files changed, 1277 insertions(+), 524 deletions(-) delete mode 100644 lib/srv/git/audit.go create mode 100644 lib/srv/git/forward.go create mode 100644 lib/srv/git/forward_test.go delete mode 100644 lib/srv/git/git.go delete mode 100644 lib/srv/git/git_test.go create mode 100644 lib/sshutils/exec.go create mode 100644 lib/sshutils/reply.go diff --git a/go.mod b/go.mod index f4b735d89b407..44be604f52fbf 100644 --- a/go.mod +++ b/go.mod @@ -100,7 +100,6 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 github.com/ghodss/yaml v1.0.0 github.com/gizak/termui/v3 v3.1.0 - github.com/go-git/go-git/v5 v5.12.0 github.com/go-jose/go-jose/v3 v3.0.3 github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-logr/logr v1.4.2 @@ -150,7 +149,6 @@ require ( github.com/keys-pub/go-libfido2 v1.5.3-0.20220306005615-8ab03fb1ec27 // replaced github.com/lib/pq v1.10.9 github.com/mailgun/mailgun-go/v4 v4.20.4 - github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-sqlite3 v1.14.24 github.com/mdlayher/netlink v1.7.2 github.com/microsoft/go-mssqldb v1.7.2 // replaced @@ -266,6 +264,7 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/apache/arrow/go/v15 v15.0.0 // indirect @@ -341,8 +340,6 @@ require ( github.com/go-errors/errors v1.4.2 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -404,7 +401,6 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect @@ -472,7 +468,6 @@ require ( github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect github.com/pingcap/log v1.1.1-0.20230317032135-a0d097d16e22 // indirect github.com/pingcap/tidb/pkg/parser v0.0.0-20240930120915-74034d4ac243 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/xattr v0.4.10 // indirect @@ -492,6 +487,7 @@ require ( github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect github.com/segmentio/asm v1.2.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect @@ -552,7 +548,6 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/component-helpers v0.31.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/metrics v0.31.1 // indirect diff --git a/go.sum b/go.sum index c25e45e5b5eb1..e691cf71c0e1f 100644 --- a/go.sum +++ b/go.sum @@ -999,6 +999,7 @@ github.com/buildkite/interpolate v0.1.3/go.mod h1:UNVe6A+UfiBNKbhAySrBbZFZFxQ+DX github.com/buildkite/roko v1.2.0 h1:hbNURz//dQqNl6Eo9awjQOVOZwSDJ8VEbBDxSfT9rGQ= github.com/buildkite/roko v1.2.0/go.mod h1:23R9e6nHxgedznkwwfmqZ6+0VJZJZ2Sg/uVcp2cP46I= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= @@ -1044,6 +1045,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8= github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -1242,14 +1244,6 @@ github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3 github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -1692,8 +1686,6 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -1825,8 +1817,6 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= @@ -1980,8 +1970,6 @@ github.com/pingcap/log v1.1.1-0.20230317032135-a0d097d16e22 h1:2SOzvGvE8beiC1Y4g github.com/pingcap/log v1.1.1-0.20230317032135-a0d097d16e22/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= github.com/pingcap/tidb/pkg/parser v0.0.0-20240930120915-74034d4ac243 h1:B3pF5adXRpuEDfSKY/bV2Lw+pPKtWH4FOaAX3Jx3X54= github.com/pingcap/tidb/pkg/parser v0.0.0-20240930120915-74034d4ac243/go.mod h1:dXcO3Ts6jUVE1VwBZp3wbVdGO4pi9MXY6IvL4L1z62g= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -2382,6 +2370,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= @@ -3117,6 +3106,7 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gG gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -3142,8 +3132,6 @@ gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/auth/authclient/api.go b/lib/auth/authclient/api.go index 92627ac122d53..aa5c772c7321f 100644 --- a/lib/auth/authclient/api.go +++ b/lib/auth/authclient/api.go @@ -422,9 +422,6 @@ 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 diff --git a/lib/proxy/router.go b/lib/proxy/router.go index 53a1bfd22d060..578f3c8e3606c 100644 --- a/lib/proxy/router.go +++ b/lib/proxy/router.go @@ -286,13 +286,15 @@ 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 } - + if target.GetKind() == types.KindGitServer { + switch target.GetSubKind() { + 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") } diff --git a/lib/reversetunnel/localsite.go b/lib/reversetunnel/localsite.go index 18279555d44b2..25ee9c449abc0 100644 --- a/lib/reversetunnel/localsite.go +++ b/lib/reversetunnel/localsite.go @@ -46,6 +46,7 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/services/readonly" "github.com/gravitational/teleport/lib/srv/forward" + "github.com/gravitational/teleport/lib/srv/git" "github.com/gravitational/teleport/lib/teleagent" "github.com/gravitational/teleport/lib/utils" proxyutils "github.com/gravitational/teleport/lib/utils/proxy" @@ -245,10 +246,6 @@ 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()) { @@ -258,6 +255,10 @@ func shouldDialAndForward(params reversetunnelclient.DialParams, recConfig types } func (s *localSite) Dial(params reversetunnelclient.DialParams) (net.Conn, error) { + if params.TargetServer != nil && params.TargetServer.GetKind() == types.KindGitServer { + return s.dialAndForwardGit(params) + } + recConfig, err := s.accessPoint.GetSessionRecordingConfig(s.srv.Context) if err != nil { return nil, trace.Wrap(err) @@ -353,6 +354,50 @@ func (s *localSite) adviseReconnect(ctx context.Context) { } } +func (s *localSite) dialAndForwardGit(params reversetunnelclient.DialParams) (_ net.Conn, retErr error) { + s.log.Debug("Dialing and forwarding git from %s to %s", params.From, params.To) + + dialStart := s.srv.Clock.Now() + targetConn, err := s.dialDirect(params) + if err != nil { + return nil, trace.ConnectionProblem(err, "failed to connect to git server") + } + + // Get a host certificate for the forwarding node from the cache. + hostCertificate, err := s.certificateCache.getHostCertificate(context.TODO(), params.Address, params.Principals) + if err != nil { + return nil, trace.Wrap(err) + } + + // Create a forwarding server that serves a single SSH connection on it. This + // server does not need to close, it will close and release all resources + // once conn is closed. + serverConfig := &git.ForwardServerConfig{ + AuthClient: s.client, + AccessPoint: s.accessPoint, + TargetConn: newMetricConn(targetConn, dialTypeDirect, dialStart, s.srv.Clock), + SrcAddr: params.From, + DstAddr: params.To, + HostCertificate: hostCertificate, + Ciphers: s.srv.Config.Ciphers, + KEXAlgorithms: s.srv.Config.KEXAlgorithms, + MACAlgorithms: s.srv.Config.MACAlgorithms, + Emitter: s.srv.Config.Emitter, + ParentContext: s.srv.Context, + LockWatcher: s.srv.LockWatcher, + HostUUID: s.srv.ID, + TargetServer: params.TargetServer, + Clock: s.clock, + } + remoteServer, err := git.NewForwardServer(serverConfig) + if err != nil { + s.log.WithError(err).Error("Failed to create git forward server") + return nil, trace.Wrap(err) + } + go remoteServer.Serve() + + return remoteServer.Dial() +} func (s *localSite) dialAndForward(params reversetunnelclient.DialParams) (_ net.Conn, retErr error) { if params.GetUserAgent == nil && !params.IsAgentlessNode { return nil, trace.BadParameter("agentless node require an agent getter") @@ -456,6 +501,18 @@ func (s *localSite) dialTunnel(dreq *sshutils.DialReq) (net.Conn, error) { return conn, nil } +func (s *localSite) dialDirect(params reversetunnelclient.DialParams) (net.Conn, error) { + dialer := proxyutils.DialerFromEnvironment(params.To.String()) + + dialTimeout := apidefaults.DefaultIOTimeout + if cnc, err := s.accessPoint.GetClusterNetworkingConfig(s.srv.Context); err != nil { + s.log.WithError(err).Warn("Failed to get cluster networking config - using default dial timeout") + } else { + dialTimeout = cnc.GetSSHDialTimeout() + } + return dialer.DialTimeout(s.srv.Context, params.To.Network(), params.To.String(), dialTimeout) +} + // tryProxyPeering determines whether the node should try to be reached over // a peer proxy. func (s *localSite) tryProxyPeering(params reversetunnelclient.DialParams) bool { @@ -649,16 +706,7 @@ func (s *localSite) getConn(params reversetunnelclient.DialParams) (conn net.Con } // If no tunnel connection was found, dial to the target host. - dialer := proxyutils.DialerFromEnvironment(params.To.String()) - - dialTimeout := apidefaults.DefaultIOTimeout - if cnc, err := s.accessPoint.GetClusterNetworkingConfig(s.srv.Context); err != nil { - s.log.WithError(err).Warn("Failed to get cluster networking config - using default dial timeout") - } else { - dialTimeout = cnc.GetSSHDialTimeout() - } - - conn, directErr = dialer.DialTimeout(s.srv.Context, params.To.Network(), params.To.String(), dialTimeout) + conn, directErr = s.dialDirect(params) if directErr != nil { directMsg := getTunnelErrorMessage(params, "direct dial", directErr) s.log.WithField("address", params.To.String()).Debugf("All attempted dial methods failed. tunnel=%q, peer=%q, direct=%q", tunnelErr, peerErr, directErr) diff --git a/lib/reversetunnel/remotesite.go b/lib/reversetunnel/remotesite.go index 13ef81cb7cb48..121bf65856b8e 100644 --- a/lib/reversetunnel/remotesite.go +++ b/lib/reversetunnel/remotesite.go @@ -87,8 +87,6 @@ type remoteSite struct { // nodeWatcher provides access the node set for the remote site nodeWatcher *services.GenericWatcher[types.Server, readonly.Server] - // gitServerWatcher provides the Git server set for the remote site - gitServerWatcher *services.GenericWatcher[types.Server, readonly.Server] // remoteCA is the last remote certificate authority recorded by the client. // It is used to detect CA rotation status changes. If the rotation @@ -173,7 +171,7 @@ func (s *remoteSite) NodeWatcher() (*services.GenericWatcher[types.Server, reado // GitServerWatcher returns the Git server watcher for the remote cluster. func (s *remoteSite) GitServerWatcher() (*services.GenericWatcher[types.Server, readonly.Server], error) { - return s.gitServerWatcher, nil + return nil, trace.NotImplemented("GitServerWatcher not implemented for remoteSite") } func (s *remoteSite) GetClient() (authclient.ClientI, error) { diff --git a/lib/reversetunnel/srv.go b/lib/reversetunnel/srv.go index 0da742376c4db..fd791bfb74e62 100644 --- a/lib/reversetunnel/srv.go +++ b/lib/reversetunnel/srv.go @@ -1287,22 +1287,6 @@ func newRemoteSite(srv *server, domainName string, sconn ssh.Conn) (*remoteSite, } go remoteSite.updateLocks(lockRetry) - - gitServerWatcher, err := services.NewGitServerWatcher(srv.ctx, services.GitServerWatcherConfig{ - ResourceWatcherConfig: services.ResourceWatcherConfig{ - Component: srv.Component, - Client: accessPoint, - // TODO(tross) update this after converting to use slog - // Logger: srv.Log, - MaxStaleness: time.Minute, - }, - GitServerGetter: accessPoint, - }) - if err != nil { - return nil, trace.Wrap(err) - } - remoteSite.gitServerWatcher = gitServerWatcher - return remoteSite, nil } diff --git a/lib/services/role.go b/lib/services/role.go index 37418d27c41a0..525407f5ab9a1 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -3567,3 +3567,27 @@ func MarshalRole(role types.Role, opts ...MarshalOption) ([]byte, error) { return nil, trace.BadParameter("unrecognized role version %T", role) } } + +type AuthPreferenceGetter interface { + GetAuthPreference(ctx context.Context) (types.AuthPreference, error) +} + +// AccessStateFromSSHCertificate populates access state based on user's SSH +// certificate and auth preference. +func AccessStateFromSSHCertificate(ctx context.Context, cert *ssh.Certificate, checker AccessChecker, authPrefGetter AuthPreferenceGetter) (AccessState, error) { + authPref, err := authPrefGetter.GetAuthPreference(ctx) + if err != nil { + return AccessState{}, trace.Wrap(err) + } + state := checker.GetAccessState(authPref) + _, state.MFAVerified = cert.Extensions[teleport.CertExtensionMFAVerified] + // Certain hardware-key based private key policies are treated as MFA verification. + if policyString, ok := cert.Extensions[teleport.CertExtensionPrivateKeyPolicy]; ok { + if keys.PrivateKeyPolicy(policyString).MFAVerified() { + state.MFAVerified = true + } + } + state.EnableDeviceVerification = true + state.DeviceVerified = dtauthz.IsSSHDeviceVerified(cert) + return state, nil +} diff --git a/lib/srv/authhandlers.go b/lib/srv/authhandlers.go index 2ac2b81097da7..213f373e63f51 100644 --- a/lib/srv/authhandlers.go +++ b/lib/srv/authhandlers.go @@ -36,16 +36,13 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" - "github.com/gravitational/teleport/api/utils/keys" apisshutils "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/auditd" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/connectmycomputer" - dtauthz "github.com/gravitational/teleport/lib/devicetrust/authz" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/observability/metrics" "github.com/gravitational/teleport/lib/services" - "github.com/gravitational/teleport/lib/srv/git" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/utils" ) @@ -234,7 +231,6 @@ func (h *AuthHandlers) CreateIdentityContext(sconn *ssh.ServerConn) (IdentityCon } identity.PreviousIdentityExpires = asTime } - identity.GitHubUserID = certificate.Extensions[teleport.CertExtensionGitHubUserID] return identity, nil } @@ -472,7 +468,7 @@ func (h *AuthHandlers) UserKeyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*s log.WarnContext(ctx, "Received unexpected cert type", "cert_type", cert.CertType) } - if h.isProxy() { + if h.isProxy() || h.c.Component == teleport.ComponentForwardingGit { return permissions, nil } @@ -495,12 +491,6 @@ func (h *AuthHandlers) UserKeyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*s if h.c.TargetServer != nil && h.c.TargetServer.IsOpenSSHNode() { err = h.canLoginWithRBAC(cert, ca, clusterName.GetClusterName(), h.c.TargetServer, teleportUser, conn.User()) } - } else if h.c.Component == teleport.ComponentForwardingGit { - if h.c.TargetServer != nil && h.c.TargetServer.GetGitHub() != nil { - err = h.canLoginWithRBAC(cert, ca, clusterName.GetClusterName(), h.c.TargetServer, teleportUser, conn.User()) - } else { - return nil, trace.BadParameter("missing server or spec for Git proxy") - } } else { // the SSH server is a Teleport node, preform an RBAC check now err = h.canLoginWithRBAC(cert, ca, clusterName.GetClusterName(), h.c.Server.GetInfo(), teleportUser, conn.User()) @@ -579,14 +569,9 @@ func (h *AuthHandlers) hostKeyCallback(hostname string, remote net.Addr, key ssh // Use the server's shutdown context. ctx := h.c.Server.Context() - switch h.c.Server.TargetMetadata().ServerSubKind { - case types.SubKindOpenSSHEICENode: - // For SubKindOpenSSHEICENode we use SSH Keys (EC2 does not support - // Certificates in ec2.SendSSHPublicKey). + // For SubKindOpenSSHEICENode we use SSH Keys (EC2 does not support Certificates in ec2.SendSSHPublicKey). + if h.c.Server.TargetMetadata().ServerSubKind == types.SubKindOpenSSHEICENode { return nil - - case types.SubKindGitHub: - return trace.Wrap(git.VerifyGitHubHostKey(hostname, remote, key)) } // If strict host key checking is enabled, reject host key fallback. @@ -658,19 +643,10 @@ func (a *ahLoginChecker) canLoginWithRBAC(cert *ssh.Certificate, ca types.CertAu return trace.Wrap(err) } - authPref, err := a.c.AccessPoint.GetAuthPreference(ctx) + state, err := services.AccessStateFromSSHCertificate(ctx, cert, accessChecker, a.c.AccessPoint) if err != nil { return trace.Wrap(err) } - state := accessChecker.GetAccessState(authPref) - _, state.MFAVerified = cert.Extensions[teleport.CertExtensionMFAVerified] - - // Certain hardware-key based private key policies are treated as MFA verification. - if policyString, ok := cert.Extensions[teleport.CertExtensionPrivateKeyPolicy]; ok { - if keys.PrivateKeyPolicy(policyString).MFAVerified() { - state.MFAVerified = true - } - } // we don't need to check the RBAC for the node if they are only allowed to join sessions if osUser == teleport.SSHSessionJoinPrincipal && @@ -688,26 +664,11 @@ func (a *ahLoginChecker) canLoginWithRBAC(cert *ssh.Certificate, ca types.CertAu } } - state.EnableDeviceVerification = true - state.DeviceVerified = dtauthz.IsSSHDeviceVerified(cert) - - // Make role matchers. - var roleMatchers []services.RoleMatcher - switch a.c.Component { - case teleport.ComponentForwardingGit: - if osUser != teleport.SSHGitPrincipal { - return trace.BadParameter("only expecting %s as login for Git commands but got %s", teleport.SSHGitPrincipal, osUser) - } - // Now continue to CheckAccess on the resource. - default: - roleMatchers = append(roleMatchers, services.NewLoginMatcher(osUser)) - } - // check if roles allow access to server if err := accessChecker.CheckAccess( target, state, - roleMatchers..., + services.NewLoginMatcher(osUser), ); err != nil { return trace.AccessDenied("user %s@%s is not authorized to login as %v@%s: %v", teleportUser, ca.GetClusterName(), osUser, clusterName, err) diff --git a/lib/srv/ctx.go b/lib/srv/ctx.go index 39a5cab277455..3318b3755f92c 100644 --- a/lib/srv/ctx.go +++ b/lib/srv/ctx.go @@ -250,9 +250,6 @@ type IdentityContext struct { // deadline in cases where both require_session_mfa and disconnect_expired_cert // are enabled. See https://github.com/gravitational/teleport/issues/18544. PreviousIdentityExpires time.Time - - // GitHubUserID is GitHub user ID attached to this user. - GitHubUserID string } // ServerContext holds session specific context, such as SSH auth agents, PTYs, diff --git a/lib/srv/exec.go b/lib/srv/exec.go index a2ea307ed2cbd..06e62caf0a5f6 100644 --- a/lib/srv/exec.go +++ b/lib/srv/exec.go @@ -31,7 +31,6 @@ import ( "slices" "strconv" "strings" - "syscall" "time" "github.com/gravitational/trace" @@ -43,7 +42,7 @@ import ( apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/services" - "github.com/gravitational/teleport/lib/srv/git" + "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/utils" ) @@ -102,16 +101,6 @@ func NewExecRequest(ctx *ServerContext, command string) (Exec, error) { Command: command, }, nil } - if ctx.srv.Component() == teleport.ComponentForwardingGit { - if err := git.CheckSSHCommand(ctx.srv.GetInfo(), command); err != nil { - return nil, trace.Wrap(err) - } - return &remoteExec{ - ctx: ctx, - command: command, - session: ctx.RemoteSession, - }, nil - } // If this is a registered OpenSSH node or proxy recoding mode is // enabled, execute the command on a remote host. This is used by @@ -192,7 +181,7 @@ func (e *localExec) Start(ctx context.Context, channel ssh.Channel) (*ExecResult return &ExecResult{ Command: e.GetCommand(), - Code: exitCode(err), + Code: sshutils.ExitCodeFromExecError(err), }, trace.ConvertSystemError(err) } // Close our half of the write pipe since it is only to be used by the child process. @@ -233,7 +222,7 @@ func (e *localExec) Wait() *ExecResult { execResult := &ExecResult{ Command: e.GetCommand(), - Code: exitCode(err), + Code: sshutils.ExitCodeFromExecError(err), } return execResult @@ -421,13 +410,11 @@ func (e *remoteExec) Wait() *ExecResult { } // Emit the result of execution to the Audit Log. - // TODO(greedy52) implement Git command auditor to replace the regular - // event. emitExecAuditEvent(e.ctx, e.command, err) return &ExecResult{ Command: e.GetCommand(), - Code: exitCode(err), + Code: sshutils.ExitCodeFromExecError(err), } } @@ -465,7 +452,7 @@ func emitExecAuditEvent(ctx *ServerContext, cmd string, execErr error) { // // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=327019 // https://bugzilla.mindrot.org/show_bug.cgi?id=1998 - ExitCode: strconv.Itoa(exitCode(execErr)), + ExitCode: strconv.Itoa(sshutils.ExitCodeFromExecError(execErr)), } if execErr != nil { @@ -626,30 +613,3 @@ func parseSecureCopy(path string) (string, string, bool, error) { return "", "", false, nil } } - -// exitCode extracts and returns the exit code from the error. -func exitCode(err error) int { - // If no error occurred, return 0 (success). - if err == nil { - return teleport.RemoteCommandSuccess - } - - var execExitErr *exec.ExitError - var sshExitErr *ssh.ExitError - switch { - // Local execution. - case errors.As(err, &execExitErr): - waitStatus, ok := execExitErr.Sys().(syscall.WaitStatus) - if !ok { - return teleport.RemoteCommandFailure - } - return waitStatus.ExitStatus() - // Remote execution. - case errors.As(err, &sshExitErr): - return sshExitErr.ExitStatus() - // An error occurred, but the type is unknown, return a generic 255 code. - default: - slog.DebugContext(context.Background(), "Unknown error returned when executing command", "error", err) - return teleport.RemoteCommandFailure - } -} diff --git a/lib/srv/forward/sshserver.go b/lib/srv/forward/sshserver.go index 4e2d6f3b69607..7919a97bc9d1b 100644 --- a/lib/srv/forward/sshserver.go +++ b/lib/srv/forward/sshserver.go @@ -53,7 +53,6 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/srv" - "github.com/gravitational/teleport/lib/srv/git" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/sshutils/x11" "github.com/gravitational/teleport/lib/teleagent" @@ -81,8 +80,6 @@ import ( // return nil, trace.Wrap(err) // } type Server struct { - component string - logger *slog.Logger id string @@ -143,7 +140,7 @@ type Server struct { // ciphers is a list of ciphers that the server supports. If omitted, // the defaults will be used. ciphers []string - // kexAlgorithms is a list of key exchange (KEX) algorithms that the + // kexAlgorithms is a list of key exchange (KEX) algorithms that the/Env // server supports. If omitted, the defaults will be used. kexAlgorithms []string // macAlgorithms is a list of message authentication codes (MAC) that @@ -260,8 +257,6 @@ type ServerConfig struct { // IsAgentlessNode indicates whether the targetServer is a Node with an OpenSSH server (no teleport agent). // This includes Nodes whose sub kind is OpenSSH and OpenSSHEphemeralKey. IsAgentlessNode bool - - component string } // CheckDefaults makes sure all required parameters are passed in. @@ -315,16 +310,6 @@ func (s *ServerConfig) CheckDefaults() error { if s.TracerProvider == nil { s.TracerProvider = tracing.DefaultProvider() } - - if s.component == "" { - switch { - case s.TargetServer != nil && s.TargetServer.GetKind() == types.KindGitServer: - s.component = teleport.ComponentForwardingGit - s.Emitter = git.NewEmitter(s.Emitter) - default: - s.component = teleport.ComponentForwardingNode - } - } return nil } @@ -345,7 +330,6 @@ func New(c ServerConfig) (*Server, error) { } s := &Server{ - component: c.component, logger: slog.With(teleport.ComponentKey, teleport.ComponentForwardingNode, "src_addr", c.SrcAddr.String(), "dst_addr", c.DstAddr.String(), @@ -391,7 +375,7 @@ func New(c ServerConfig) (*Server, error) { // Common auth handlers. authHandlerConfig := srv.AuthHandlerConfig{ Server: s, - Component: c.component, + Component: teleport.ComponentForwardingNode, Emitter: c.Emitter, AccessPoint: c.TargetClusterAccessPoint, TargetServer: c.TargetServer, @@ -469,7 +453,7 @@ func (s *Server) AdvertiseAddr() string { // Component is the type of node this server is. func (s *Server) Component() string { - return s.component + return teleport.ComponentForwardingNode } // PermitUserEnvironment is always false because it's up to the remote host @@ -522,21 +506,21 @@ func (s *Server) GetHostSudoers() srv.HostSudoers { // GetInfo returns a services.Server that represents this server. func (s *Server) GetInfo() types.Server { - spec := types.ServerSpecV2{ - Addr: s.AdvertiseAddr(), - } + var subKind string if s.targetServer != nil { - spec.Hostname = s.targetServer.GetHostname() - spec.GitHub = s.targetServer.GetGitHub() + subKind = s.targetServer.GetSubKind() } return &types.ServerV2{ Kind: types.KindNode, + SubKind: subKind, Version: types.V2, Metadata: types.Metadata{ Name: s.ID(), Namespace: s.GetNamespace(), }, - Spec: spec, + Spec: types.ServerSpecV2{ + Addr: s.AdvertiseAddr(), + }, } } @@ -615,7 +599,6 @@ func (s *Server) Serve() { ctx := context.Background() ctx, s.connectionContext = sshutils.NewConnectionContext(ctx, s.serverConn, s.sconn, sshutils.SetConnectionContextClock(s.clock)) - systemLogin := sconn.User() // Take connection and extract identity information for the user from it. s.identityContext, err = s.authHandlers.CreateIdentityContext(sconn) @@ -647,34 +630,10 @@ func (s *Server) Serve() { s.agentlessSigner = sshSigner } } - if s.targetServer != nil && s.targetServer.GetGitHub() != nil { - s.agentlessSigner, err = git.MakeGitHubSigner(ctx, git.GitHubSignerConfig{ - Server: s.targetServer, - GitHubUserID: s.identityContext.GitHubUserID, - TeleportUser: s.identityContext.TeleportUser, - IdentityExpires: s.identityContext.CertValidBefore, - AuthPreferenceGetter: s.GetAccessPoint(), - GitHubUserCertGenerator: s.authClient.IntegrationsClient(), - Clock: s.clock, - }) - if err != nil { - s.rejectChannel(chans, fmt.Sprintf("Unable to make SSH signer for GitHub: %v", err.Error())) - sconn.Close() - s.logger.WarnContext(ctx, "Unable to make SSH signer for GitHub", - "user", s.identityContext.TeleportUser, - "hostname", s.targetServer.GetHostname(), - "error", err) - return - } - - // `tsh git ssh` sends teleport.SSHGitPrincipal as user. Replace it with - // "git". - systemLogin = "git" - } // Connect and authenticate to the remote node. - s.logger.DebugContext(s.Context(), "Creating remote connection", "user", systemLogin, "client_addr", s.clientConn.RemoteAddr()) - s.remoteClient, err = s.newRemoteClient(ctx, systemLogin, netConfig) + s.logger.DebugContext(s.Context(), "Creating remote connection", "user", sconn.User(), "client_addr", s.clientConn.RemoteAddr()) + s.remoteClient, err = s.newRemoteClient(ctx, sconn.User(), netConfig) if err != nil { // Reject the connection with an error so the client doesn't hang then // close the connection. diff --git a/lib/srv/git/audit.go b/lib/srv/git/audit.go deleted file mode 100644 index 390c6c940b0c1..0000000000000 --- a/lib/srv/git/audit.go +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package git - -import ( - "context" - - "github.com/gravitational/trace" - - apievents "github.com/gravitational/teleport/api/types/events" - "github.com/gravitational/teleport/lib/events" -) - -type gitCommandEmitter struct { - events.StreamEmitter - discard apievents.Emitter -} - -// NewEmitter returns an emitter for Git proxy usage. -func NewEmitter(emitter events.StreamEmitter) events.StreamEmitter { - return &gitCommandEmitter{ - StreamEmitter: emitter, - discard: events.NewDiscardEmitter(), - } -} - -// EmitAuditEvent overloads EmitAuditEvent to only emit Git command events. -func (e *gitCommandEmitter) EmitAuditEvent(ctx context.Context, event apievents.AuditEvent) error { - switch event.GetType() { - // TODO(greedy52) enable this when available: - // case events.GitCommandEvent: - // return trace.Wrap(e.emitter.EmitAuditEvent(ctx, event)) - default: - return trace.Wrap(e.discard.EmitAuditEvent(ctx, event)) - } -} diff --git a/lib/srv/git/forward.go b/lib/srv/git/forward.go new file mode 100644 index 0000000000000..9e6cd24e81dc6 --- /dev/null +++ b/lib/srv/git/forward.go @@ -0,0 +1,590 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package git + +import ( + "context" + "io" + "log/slog" + "net" + + "github.com/google/uuid" + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + "golang.org/x/crypto/ssh" + + "github.com/gravitational/teleport" + tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh" + "github.com/gravitational/teleport/api/types" + apievents "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/bpf" + "github.com/gravitational/teleport/lib/events" + "github.com/gravitational/teleport/lib/service/servicecfg" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/srv" + "github.com/gravitational/teleport/lib/sshutils" + "github.com/gravitational/teleport/lib/utils" +) + +// ForwardServerConfig is the configuration for the ForwardServer. +type ForwardServerConfig struct { + // ParentContext is a parent context, used to signal global + // closure + ParentContext context.Context + // TargetServer is the target server that represents the git-hosting + // service. + TargetServer types.Server + // TargetConn is the TCP connection to the remote host. + TargetConn net.Conn + // AuthClient is a client connected to the Auth server of this local cluster. + AuthClient authclient.ClientI + // AccessPoint is a caching client that provides access to this local cluster. + AccessPoint srv.AccessPoint + // Emitter is audit events emitter + Emitter events.StreamEmitter + // LockWatcher is a lock watcher. + LockWatcher *services.LockWatcher + // HostCertificate is the SSH host certificate this in-memory server presents + // to the client. + HostCertificate ssh.Signer + // SrcAddr is the source address + SrcAddr net.Addr + // DstAddr is the destination address + DstAddr net.Addr + // HostUUID is the UUID of the underlying proxy that the forwarding server + // is running in. + HostUUID string + + // Ciphers is a list of ciphers that the server supports. If omitted, + // the defaults will be used. + Ciphers []string + // KEXAlgorithms is a list of key exchange (KEX) algorithms that the + // server supports. If omitted, the defaults will be used. + KEXAlgorithms []string + // MACAlgorithms is a list of message authentication codes (MAC) that + // the server supports. If omitted the defaults will be used. + MACAlgorithms []string + // FIPS mode means Teleport started in a FedRAMP/FIPS 140-2 compliant + // configuration. + FIPS bool + + // Clock is an optoinal clock to override default real time clock + Clock clockwork.Clock +} + +// CheckAndSetDefaults checks and sets default values for any missing fields. +func (c *ForwardServerConfig) CheckAndSetDefaults() error { + if c.TargetServer == nil { + return trace.BadParameter("missing parameter TargetServer") + } + if c.TargetConn == nil { + return trace.BadParameter("missing parameter TargetConn") + } + if c.AuthClient == nil { + return trace.BadParameter("missing parameter AuthClient") + } + if c.AccessPoint == nil { + return trace.BadParameter("missing parameter AccessPoint") + } + if c.Emitter == nil { + return trace.BadParameter("missing parameter Emitter") + } + if c.HostCertificate == nil { + return trace.BadParameter("missing parameter HostCertificate") + } + if c.ParentContext == nil { + return trace.BadParameter("missing parameter ParentContext") + } + if c.LockWatcher == nil { + return trace.BadParameter("missing parameter LockWatcher") + } + if c.SrcAddr == nil { + return trace.BadParameter("source address required to identify client") + } + if c.DstAddr == nil { + return trace.BadParameter("source address required to identify client") + } + if c.Clock == nil { + c.Clock = clockwork.NewRealClock() + } + return nil +} + +// ForwardServer is an in-memory SSH server that forwards git commands to remote +// git-hosting services like "github.com". +type ForwardServer struct { + events.StreamEmitter + cfg *ForwardServerConfig + logger *slog.Logger + auth *srv.AuthHandlers + reply *sshutils.Reply + id string + + // serverConn is the server side of the pipe to the client connection. + serverConn net.Conn + // clientConn is the client side of the pipe to the client connection. + clientConn net.Conn + // remoteClient is the client connected to the git-hosting service. + remoteClient *tracessh.Client + + // waitExec receives exec response. + waitExec chan error + + // verifyRemoteHost is a callback to verify remote host like "github.com". + // Can be overridden for tests. Defaults to verifyRemoteHost. + verifyRemoteHost ssh.HostKeyCallback + // makeRemoteSigner generates the client certificate for connecting to the + // remote server. Can be overridden for tests. Defaults to makeRemoteSigner. + makeRemoteSigner func(context.Context, *ForwardServerConfig, srv.IdentityContext) (ssh.Signer, error) +} + +// Dial returns the client connection of the pipe +func (s *ForwardServer) Dial() (net.Conn, error) { + return s.clientConn, nil +} + +// NewForwardServer creates a new in-memory SSH server that forwards git +// commands to remote git-hosting services like "github.com". +func NewForwardServer(cfg *ForwardServerConfig) (*ForwardServer, error) { + if err := cfg.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + serverConn, clientConn, err := utils.DualPipeNetConn(cfg.SrcAddr, cfg.DstAddr) + if err != nil { + return nil, trace.Wrap(err) + } + + logger := slog.With(teleport.ComponentKey, teleport.ComponentForwardingGit, + "src_addr", cfg.SrcAddr.String(), + "dst_addr", cfg.DstAddr.String(), + ) + s := &ForwardServer{ + StreamEmitter: cfg.Emitter, + cfg: cfg, + serverConn: serverConn, + clientConn: clientConn, + logger: logger, + reply: sshutils.NewReply(logger), + id: uuid.NewString(), + waitExec: make(chan error, 1), + verifyRemoteHost: verifyRemoteHost(cfg.TargetServer), + makeRemoteSigner: makeRemoteSigner, + } + // TODO(greedy52) extract common parts from srv.NewAuthHandlers like + // CreateIdentityContext and UserKeyAuth to a common package. + s.auth, err = srv.NewAuthHandlers(&srv.AuthHandlerConfig{ + Server: s, + Component: teleport.ComponentForwardingGit, + Emitter: s.cfg.Emitter, + AccessPoint: cfg.AccessPoint, + TargetServer: cfg.TargetServer, + FIPS: cfg.FIPS, + Clock: cfg.Clock, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return s, nil + +} + +// Serve starts an SSH server that forwards git commands. +func (s *ForwardServer) Serve() { + defer s.close() + s.logger.DebugContext(s.cfg.ParentContext, "Starting forwarding git") + defer s.logger.DebugContext(s.cfg.ParentContext, "Finished forwarding git") + server, err := sshutils.NewServer( + teleport.ComponentForwardingGit, + utils.NetAddr{}, /* empty addr, this is one time use so no use for listener*/ + sshutils.NewChanHandlerFunc(s.onChannel), + sshutils.StaticHostSigners(s.cfg.HostCertificate), + sshutils.AuthMethods{ + PublicKey: s.userKeyAuth, + }, + sshutils.SetFIPS(s.cfg.FIPS), + sshutils.SetCiphers(s.cfg.Ciphers), + sshutils.SetKEXAlgorithms(s.cfg.KEXAlgorithms), + sshutils.SetMACAlgorithms(s.cfg.MACAlgorithms), + sshutils.SetClock(s.cfg.Clock), + sshutils.SetNewConnHandler(sshutils.NewConnHandlerFunc(s.onConnection)), + ) + if err != nil { + s.logger.ErrorContext(s.cfg.ParentContext, "Failed to create git forward server", "error", err) + return + } + server.HandleConnection(s.serverConn) +} + +func (s *ForwardServer) close() { + for _, closer := range []io.Closer{ + s.serverConn, + s.clientConn, + s.cfg.TargetConn, + } { + if err := closer.Close(); err != nil && !utils.IsOKNetworkError(err) { + s.logger.WarnContext(s.cfg.ParentContext, "Failed to close", "error", err) + } + } +} + +func (s *ForwardServer) userKeyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + cert, ok := key.(*ssh.Certificate) + if !ok { + return nil, trace.BadParameter("unsupported key type") + } + if len(cert.Extensions[teleport.CertExtensionGitHubUserID]) == 0 { + return nil, trace.BadParameter("missing GitHub user ID") + } + + // Verify incoming user is "git" and override it with any valid principle + // to bypass principle validation. + if conn.User() != gitUser { + return nil, trace.BadParameter("only git is expected as user for git connections") + } + if len(cert.ValidPrincipals) > 0 { + conn = sshutils.NewSSHConnMetadataWithUser(conn, cert.ValidPrincipals[0]) + } + + // Use auth.UserKeyAuth to verify user cert is signed by UserCA. + permissions, err := s.auth.UserKeyAuth(conn, key) + if err != nil { + return nil, trace.Wrap(err) + } + + // Check RBAC on the git server resource (aka s.cfg.TargetServer). + if err := s.checkUserAccess(cert); err != nil { + s.logger.ErrorContext(s.Context(), "Permission denied", + "error", err, + "local_addr", conn.LocalAddr(), + "remote_addr", conn.RemoteAddr(), + "key", key.Type(), + "fingerprint", sshutils.Fingerprint(key), + "user", cert.KeyId, + ) + return nil, trace.Wrap(err) + } + return permissions, nil +} + +func (s *ForwardServer) checkUserAccess(cert *ssh.Certificate) error { + clusterName, err := s.cfg.AccessPoint.GetClusterName() + if err != nil { + return trace.Wrap(err) + } + accessInfo, err := services.AccessInfoFromLocalCertificate(cert) + if err != nil { + return trace.Wrap(err) + } + accessChecker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), s.cfg.AccessPoint) + if err != nil { + return trace.Wrap(err) + } + state, err := services.AccessStateFromSSHCertificate(s.Context(), cert, accessChecker, s.cfg.AccessPoint) + if err != nil { + return trace.Wrap(err) + } + return trace.Wrap(accessChecker.CheckAccess(s.cfg.TargetServer, state)) +} + +func (s *ForwardServer) onConnection(ctx context.Context, ccx *sshutils.ConnectionContext) (context.Context, error) { + s.logger.DebugContext(ctx, "On new connection") + + identityCtx, err := s.auth.CreateIdentityContext(ccx.ServerConn) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := s.initRemoteConn(ctx, ccx, identityCtx); err != nil { + s.logger.DebugContext(ctx, "onConnection failed", "error", err) + return ctx, trace.Wrap(err) + } + + // TODO(greedy52) decouple from srv.NewServerContext. We only need + // connection monitoring. + serverCtx, err := srv.NewServerContext(ctx, ccx, s, identityCtx) + if err != nil { + return nil, trace.Wrap(err) + } + + s.logger.DebugContext(ctx, "New connection accepted") + ccx.AddCloser(serverCtx) + return ctx, nil +} + +func (s *ForwardServer) onChannel(ctx context.Context, ccx *sshutils.ConnectionContext, nch ssh.NewChannel) { + s.logger.DebugContext(ctx, "On new channel", "channel", nch.ChannelType()) + + // Only expecting a session to execute a command. + if nch.ChannelType() != teleport.ChanSession { + s.reply.RejectUnknownChannel(ctx, nch) + return + } + + if s.remoteClient == nil { + s.reply.RejectWithNewRemoteSessionError(ctx, nch, trace.NotFound("missing remote client")) + return + } + remoteSession, err := s.remoteClient.NewSession(ctx) + if err != nil { + s.reply.RejectWithNewRemoteSessionError(ctx, nch, err) + return + } + defer remoteSession.Close() + + ch, in, err := nch.Accept() + if err != nil { + s.reply.RejectWithAcceptError(ctx, nch, err) + return + } + defer ch.Close() + + for { + select { + case req := <-in: + if req == nil { + s.logger.DebugContext(ctx, "Client disconnected", "remote_addr", ccx.ServerConn.RemoteAddr()) + return + } + + ok, err := s.dispatch(ctx, ch, req, remoteSession) + if err != nil { + s.reply.ReplyError(ctx, ch, req, err) + return + } + s.reply.ReplyRequest(ctx, req, ok, nil) + + case execErr := <-s.waitExec: + code := sshutils.ExitCodeFromExecError(execErr) + s.logger.DebugContext(ctx, "Exec request complete", "code", code) + s.reply.SendExitStatus(ctx, ch, code) + return + + case <-ctx.Done(): + return + } + } +} + +// dispatch executes an incoming request. If successful, it returns the ok value +// for the reply. Otherwise, it returns the error it encountered. +func (s *ForwardServer) dispatch(ctx context.Context, ch ssh.Channel, req *ssh.Request, remoteSession *tracessh.Session) (bool, error) { + s.logger.DebugContext(ctx, "Dispatching client request", "request_type", req.Type) + + switch req.Type { + case tracessh.EnvsRequest: + s.logger.DebugContext(ctx, "Ignored request", "request_type", req.Type) + return true, nil + case sshutils.ExecRequest: + return true, trace.Wrap(s.handleExec(ctx, ch, req, remoteSession)) + case sshutils.EnvRequest: + return true, trace.Wrap(s.handleEnv(ctx, req, remoteSession)) + default: + s.logger.WarnContext(ctx, "Received unsupported SSH request", "request_type", req.Type) + return false, nil + } +} + +func (s *ForwardServer) handleExec(ctx context.Context, ch ssh.Channel, req *ssh.Request, remoteSession *tracessh.Session) error { + var r sshutils.ExecReq + if err := ssh.Unmarshal(req.Payload, &r); err != nil { + return trace.Wrap(err, "failed to unmarshal exec request") + } + + /* TODO(greedy52) enable command recorder for audit log + command, err := parseSSHCommand(r.Command) + if err != nil { + return trace.Wrap(err, "parsing ssh command %q", r.Command) + } + recorder := NewCommandRecorder(command) + */ + remoteSession.Stdout = ch + remoteSession.Stderr = ch.Stderr() + remoteStdin, err := remoteSession.StdinPipe() + if err != nil { + return trace.Wrap(err, "failed to open remote session") + } + go func() { + defer remoteStdin.Close() + if _, err := io.Copy(remoteStdin, ch); err != nil { + s.logger.WarnContext(ctx, "Failed to copy git command stdin", "error", err) + } + }() + + if err := remoteSession.Start(ctx, r.Command); err != nil { + return trace.Wrap(err, "failed to start git command") + } + + go func() { + execErr := remoteSession.Wait() + s.waitExec <- execErr + }() + return nil +} + +func (s *ForwardServer) handleEnv(ctx context.Context, req *ssh.Request, remoteSession *tracessh.Session) error { + var e sshutils.EnvReqParams + if err := ssh.Unmarshal(req.Payload, &e); err != nil { + return trace.Wrap(err) + } + err := remoteSession.Setenv(ctx, e.Name, e.Value) + if err != nil { + s.logger.WarnContext(ctx, "Failed to set env on remote session", "error", err, "request", e) + } + return nil +} + +func (s *ForwardServer) initRemoteConn(ctx context.Context, ccx *sshutils.ConnectionContext, identityCtx srv.IdentityContext) error { + netConfig, err := s.cfg.AccessPoint.GetClusterNetworkingConfig(s.cfg.ParentContext) + if err != nil { + return trace.Wrap(err) + } + signer, err := s.makeRemoteSigner(ctx, s.cfg, identityCtx) + if err != nil { + return trace.Wrap(err) + } + clientConfig := &ssh.ClientConfig{ + User: gitUser, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: s.verifyRemoteHost, + Timeout: netConfig.GetSSHDialTimeout(), + } + clientConfig.Ciphers = s.cfg.Ciphers + clientConfig.KeyExchanges = s.cfg.KEXAlgorithms + clientConfig.MACs = s.cfg.MACAlgorithms + + s.remoteClient, err = tracessh.NewClientConnWithDeadline( + s.cfg.ParentContext, + s.cfg.TargetConn, + s.cfg.DstAddr.String(), + clientConfig, + ) + if err != nil { + return trace.Wrap(err) + } + ccx.AddCloser(s.remoteClient) + return nil +} + +func makeRemoteSigner(ctx context.Context, cfg *ForwardServerConfig, identityCtx srv.IdentityContext) (ssh.Signer, error) { + switch cfg.TargetServer.GetSubKind() { + case types.SubKindGitHub: + return MakeGitHubSigner(ctx, GitHubSignerConfig{ + Server: cfg.TargetServer, + TeleportUser: identityCtx.TeleportUser, + IdentityExpires: identityCtx.CertValidBefore, + GitHubUserID: identityCtx.Certificate.Extensions[teleport.CertExtensionGitHubUserID], + AuthPreferenceGetter: cfg.AccessPoint, + GitHubUserCertGenerator: cfg.AuthClient.IntegrationsClient(), + Clock: cfg.Clock, + }) + default: + return nil, trace.BadParameter("unsupported subkind %q", cfg.TargetServer.GetSubKind()) + } +} + +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) + default: + return trace.BadParameter("unsupported subkind %q", targetServer.GetSubKind()) + } + } +} + +// Below functions implement srv.Server so git.ForwardServer can be used for +// srv.NewServerContext and srv.NewAuthHandlers. +// TODO(greedy52) decouple from srv.Server. + +func (s *ForwardServer) Context() context.Context { + return s.cfg.ParentContext +} +func (s *ForwardServer) TargetMetadata() apievents.ServerMetadata { + return apievents.ServerMetadata{ + ServerVersion: teleport.Version, + ServerNamespace: s.cfg.TargetServer.GetNamespace(), + ServerAddr: s.cfg.DstAddr.String(), + ServerHostname: s.cfg.TargetServer.GetHostname(), + ForwardedBy: s.cfg.HostUUID, + ServerSubKind: s.cfg.TargetServer.GetSubKind(), + } +} +func (s *ForwardServer) GetInfo() types.Server { + return s.cfg.TargetServer +} +func (s *ForwardServer) ID() string { + return s.id +} +func (s *ForwardServer) HostUUID() string { + return s.cfg.HostUUID +} +func (s *ForwardServer) GetNamespace() string { + return s.cfg.TargetServer.GetNamespace() +} +func (s *ForwardServer) AdvertiseAddr() string { + return s.clientConn.RemoteAddr().String() +} +func (s *ForwardServer) Component() string { + return teleport.ComponentForwardingGit +} +func (s *ForwardServer) PermitUserEnvironment() bool { + return false +} +func (s *ForwardServer) GetAccessPoint() srv.AccessPoint { + return s.cfg.AccessPoint +} +func (s *ForwardServer) GetDataDir() string { + return "" +} +func (s *ForwardServer) GetPAM() (*servicecfg.PAMConfig, error) { + return nil, trace.NotImplemented("not supported for git forward server") +} +func (s *ForwardServer) GetClock() clockwork.Clock { + return s.cfg.Clock +} +func (s *ForwardServer) UseTunnel() bool { + return false +} +func (s *ForwardServer) GetBPF() bpf.BPF { + return nil +} +func (s *ForwardServer) GetUserAccountingPaths() (utmp, wtmp, btmp string) { + return +} +func (s *ForwardServer) GetLockWatcher() *services.LockWatcher { + return s.cfg.LockWatcher +} +func (s *ForwardServer) GetCreateHostUser() bool { + return false +} +func (s *ForwardServer) GetHostUsers() srv.HostUsers { + return nil +} +func (s *ForwardServer) GetHostSudoers() srv.HostSudoers { + return nil +} + +const ( + gitUser = "git" +) diff --git a/lib/srv/git/forward_test.go b/lib/srv/git/forward_test.go new file mode 100644 index 0000000000000..669e6c411b016 --- /dev/null +++ b/lib/srv/git/forward_test.go @@ -0,0 +1,360 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package git + +import ( + "context" + "io" + "log/slog" + "net" + "os" + "testing" + "time" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" + + "github.com/gravitational/teleport/api/constants" + tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/wrappers" + apisshutils "github.com/gravitational/teleport/api/utils/sshutils" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/auth/testauthority" + "github.com/gravitational/teleport/lib/backend/memory" + "github.com/gravitational/teleport/lib/cryptosuites" + "github.com/gravitational/teleport/lib/events/eventstest" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local" + "github.com/gravitational/teleport/lib/srv" + "github.com/gravitational/teleport/lib/sshutils" + "github.com/gravitational/teleport/lib/utils" +) + +func TestMain(m *testing.M) { + utils.InitLoggerForTests() + os.Exit(m.Run()) +} + +func TestForwardServer(t *testing.T) { + caSigner, err := apisshutils.MakeTestSSHCA() + require.NoError(t, err) + userCert := makeUserCert(t, caSigner) + + tests := []struct { + name string + allowedGitHubOrg string + clientLogin string + verifyRemoteHost ssh.HostKeyCallback + wantNewClientError bool + verifyWithClient func(t *testing.T, ctx context.Context, client *tracessh.Client, m *mockGitHostingService) + }{ + { + name: "success", + allowedGitHubOrg: "*", + clientLogin: "git", + verifyRemoteHost: ssh.InsecureIgnoreHostKey(), + wantNewClientError: false, + verifyWithClient: func(t *testing.T, ctx context.Context, client *tracessh.Client, m *mockGitHostingService) { + session, err := client.NewSession(ctx) + require.NoError(t, err) + defer session.Close() + + gitCommand := "git-upload-pack 'org/my-repo.git'" + session.Stderr = io.Discard + session.Stdout = io.Discard + err = session.Run(ctx, gitCommand) + require.NoError(t, err) + require.Equal(t, gitCommand, m.receivedExec.Command) + }, + }, + { + name: "failed RBAC", + allowedGitHubOrg: "no-org-allowed", + clientLogin: "git", + verifyRemoteHost: ssh.InsecureIgnoreHostKey(), + wantNewClientError: true, + }, + { + name: "failed client login check", + allowedGitHubOrg: "*", + clientLogin: "not-git", + verifyRemoteHost: ssh.InsecureIgnoreHostKey(), + wantNewClientError: true, + }, + { + name: "failed remote host check", + allowedGitHubOrg: "*", + clientLogin: "git", + verifyRemoteHost: func(string, net.Addr, ssh.PublicKey) error { + return trace.AccessDenied("fake a remote host check error") + }, + verifyWithClient: func(t *testing.T, ctx context.Context, client *tracessh.Client, m *mockGitHostingService) { + // Connection is accepted but anything following fails. + _, err := client.NewSession(ctx) + require.Error(t, err) + }, + }, + { + name: "invalid channel type", + allowedGitHubOrg: "*", + clientLogin: "git", + verifyRemoteHost: ssh.InsecureIgnoreHostKey(), + verifyWithClient: func(t *testing.T, ctx context.Context, client *tracessh.Client, m *mockGitHostingService) { + _, _, err := client.OpenChannel(ctx, "unknown", nil) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown channel type") + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mockEmitter := &eventstest.MockRecorderEmitter{} + mockGitService := newMockGitHostingService(t, caSigner) + hostCert, err := apisshutils.MakeRealHostCert(caSigner) + require.NoError(t, err) + targetConn, err := net.Dial("tcp", mockGitService.Addr()) + require.NoError(t, err) + + s, err := NewForwardServer(&ForwardServerConfig{ + TargetServer: makeGitServer(t, "org"), + TargetConn: targetConn, + AuthClient: mockAuthClient{}, + AccessPoint: mockAccessPoint{ + ca: caSigner, + allowedGitHubOrg: test.allowedGitHubOrg, + }, + Emitter: mockEmitter, + HostCertificate: hostCert, + ParentContext: ctx, + LockWatcher: makeLockWatcher(t), + SrcAddr: utils.MustParseAddr("127.0.0.1:12345"), + DstAddr: utils.MustParseAddr("127.0.0.1:2222"), + }) + require.NoError(t, err) + + s.verifyRemoteHost = test.verifyRemoteHost + s.makeRemoteSigner = func(context.Context, *ForwardServerConfig, srv.IdentityContext) (ssh.Signer, error) { + // mock server does not validate this, just put whatever. + return userCert, nil + } + go s.Serve() + + clientDialConn, err := s.Dial() + require.NoError(t, err) + + conn, chCh, reqCh, err := ssh.NewClientConn( + clientDialConn, + "127.0.0.1:222", + &ssh.ClientConfig{ + User: test.clientLogin, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(userCert), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 5 * time.Second, + }, + ) + if test.wantNewClientError { + require.Error(t, err) + return + } + require.NoError(t, err) + client := tracessh.NewClient(conn, chCh, reqCh) + defer client.Close() + + test.verifyWithClient(t, ctx, client, mockGitService) + }) + } + +} + +func makeUserCert(t *testing.T, caSigner ssh.Signer) ssh.Signer { + t.Helper() + keygen := testauthority.New() + clientPrivateKey, err := cryptosuites.GeneratePrivateKeyWithAlgorithm(cryptosuites.ECDSAP256) + require.NoError(t, err) + clientCertBytes, err := keygen.GenerateUserCert(services.UserCertParams{ + CASigner: caSigner, + PublicUserKey: clientPrivateKey.MarshalSSHPublicKey(), + Username: "alice", + AllowedLogins: []string{"does-not-matter"}, + GitHubUserID: "1234567", + CertificateFormat: constants.CertificateFormatStandard, + Traits: wrappers.Traits{}, + Roles: []string{"editor"}, + }) + require.NoError(t, err) + clientAuthorizedCert, _, _, _, err := ssh.ParseAuthorizedKey(clientCertBytes) + require.NoError(t, err) + clientSigner, err := apisshutils.SSHSigner(clientAuthorizedCert.(*ssh.Certificate), clientPrivateKey) + require.NoError(t, err) + return clientSigner +} + +func makeLockWatcher(t *testing.T) *services.LockWatcher { + t.Helper() + backend, err := memory.New(memory.Config{}) + require.NoError(t, err) + lockWatcher, err := services.NewLockWatcher(context.Background(), services.LockWatcherConfig{ + ResourceWatcherConfig: services.ResourceWatcherConfig{ + Component: "git.test", + Client: local.NewEventsService(backend), + }, + LockGetter: local.NewAccessService(backend), + }) + require.NoError(t, err) + return lockWatcher +} + +func makeGitServer(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 +} + +type mockGitHostingService struct { + *sshutils.Server + *sshutils.Reply + receivedExec sshutils.ExecReq +} + +func newMockGitHostingService(t *testing.T, caSigner ssh.Signer) *mockGitHostingService { + t.Helper() + hostCert, err := apisshutils.MakeRealHostCert(caSigner) + require.NoError(t, err) + m := &mockGitHostingService{ + Reply: &sshutils.Reply{}, + } + server, err := sshutils.NewServer( + "git.test", + utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"}, + m, + sshutils.StaticHostSigners(hostCert), + sshutils.AuthMethods{NoClient: true}, + sshutils.SetNewConnHandler(m), + ) + require.NoError(t, err) + require.NoError(t, server.Start()) + t.Cleanup(func() { + server.Close() + }) + m.Server = server + return m +} +func (m *mockGitHostingService) HandleNewConn(ctx context.Context, ccx *sshutils.ConnectionContext) (context.Context, error) { + slog.DebugContext(ctx, "mock git service receives new connection") + return ctx, nil +} +func (m *mockGitHostingService) HandleNewChan(ctx context.Context, ccx *sshutils.ConnectionContext, nch ssh.NewChannel) { + slog.DebugContext(ctx, "mock git service receives new chan") + ch, in, err := nch.Accept() + if err != nil { + m.RejectWithAcceptError(ctx, nch, err) + return + } + defer ch.Close() + for { + select { + case req := <-in: + if req == nil { + return + } + + if err := ssh.Unmarshal(req.Payload, &m.receivedExec); err != nil { + m.ReplyError(ctx, ch, req, err) + return + } + if req.WantReply { + m.ReplyRequest(ctx, req, true, nil) + } + slog.DebugContext(ctx, "mock git service receives new exec request", "req", m.receivedExec) + m.SendExitStatus(ctx, ch, 0) + return + + case <-ctx.Done(): + return + } + } +} + +type mockAuthClient struct { + authclient.ClientI +} + +type mockAccessPoint struct { + srv.AccessPoint + ca ssh.Signer + allowedGitHubOrg string +} + +func (m mockAccessPoint) GetClusterName(...services.MarshalOption) (types.ClusterName, error) { + return types.NewClusterName(types.ClusterNameSpecV2{ + ClusterName: "git.test", + ClusterID: "git.test", + }) +} +func (m mockAccessPoint) GetClusterNetworkingConfig(context.Context) (types.ClusterNetworkingConfig, error) { + return types.DefaultClusterNetworkingConfig(), nil +} +func (m mockAccessPoint) GetSessionRecordingConfig(context.Context) (types.SessionRecordingConfig, error) { + return types.DefaultSessionRecordingConfig(), nil +} +func (m mockAccessPoint) GetAuthPreference(context.Context) (types.AuthPreference, error) { + return types.DefaultAuthPreference(), nil +} +func (m mockAccessPoint) GetRole(_ context.Context, name string) (types.Role, error) { + return types.NewRole(name, types.RoleSpecV6{ + Allow: types.RoleConditions{ + GitHubPermissions: []types.GitHubPermission{{ + Organizations: []string{m.allowedGitHubOrg}, + }}, + }, + }) +} +func (m mockAccessPoint) GetCertAuthorities(_ context.Context, caType types.CertAuthType, _ bool) ([]types.CertAuthority, error) { + if m.ca == nil { + return nil, trace.NotFound("no certificate authority found") + } + ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{ + Type: caType, + ClusterName: "git.test", + ActiveKeys: types.CAKeySet{ + SSH: []*types.SSHKeyPair{{ + PublicKey: ssh.MarshalAuthorizedKey(m.ca.PublicKey()), + }}, + }, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return []types.CertAuthority{ca}, nil +} diff --git a/lib/srv/git/git.go b/lib/srv/git/git.go deleted file mode 100644 index d9a4028289be9..0000000000000 --- a/lib/srv/git/git.go +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package git - -import ( - "strings" - - "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/gravitational/trace" - "github.com/mattn/go-shellwords" - - "github.com/gravitational/teleport/api/types" -) - -// CheckSSHCommand performs basic checks against the SSH command. -func CheckSSHCommand(server types.Server, command string) error { - cmd, err := parseSSHCommand(command) - if err != nil { - return trace.Wrap(err, "parsing ssh command %q", command) - } - // Only supporting GitHub for now. - if server.GetGitHub() == nil { - return trace.BadParameter("missing GitHub spec") - } - if server.GetGitHub().Organization != cmd.repository.owner() { - return trace.AccessDenied("expect organization %q but got %q", server.GetGitHub().Organization, cmd.repository.owner()) - } - return nil -} - -type repository string - -// owner returns the first part of the repository. If repository does not have -// multiple parts, empty will be returned. -// -// For GitHub, owner is either the user or the organization that owns the repo. -func (r repository) owner() string { - if owner, _, ok := strings.Cut(string(r), "/"); ok { - return owner - } - return "" -} - -// command is the Git command to be executed. -type command struct { - service string - repository repository -} - -// parseSSHCommand parses the provided SSH command and returns the plumbing -// command details. -func parseSSHCommand(sshCommand string) (*command, error) { - args, err := shellwords.Parse(sshCommand) - if err != nil { - return nil, trace.Wrap(err) - } - if len(args) == 0 { - return nil, trace.BadParameter("invalid ssh command %s", sshCommand) - } - - // There are a number of plumbing commands but only upload-pack and - // receive-pack are expected over SSH transport. - // https://git-scm.com/docs/pack-protocol#_transports - switch args[0] { - // git-receive-pack - Receive what is pushed into the repository - // Example: git-upload-pack 'my-org/my-repo.git' - // https://git-scm.com/docs/git-receive-pack - case transport.ReceivePackServiceName: - if len(args) != 2 { - return nil, trace.CompareFailed("expecting 2 arguments for %q, got %d", args[0], len(args)) - } - return &command{ - service: args[0], - repository: repository(args[1]), - }, nil - - // git-upload-pack - Send objects packed back to git-fetch-pack - // Example: git-upload-pack 'my-org/my-repo.git' - // https://git-scm.com/docs/git-upload-pack - case transport.UploadPackServiceName: - if len(args) < 2 { - return nil, trace.CompareFailed("expecting more than one arguments for %q, got %d", args[0], len(args)) - } - - return &command{ - service: args[0], - repository: repository(args[len(args)-1]), - }, nil - default: - return nil, trace.BadParameter("unsupported command %q", sshCommand) - } -} diff --git a/lib/srv/git/git_test.go b/lib/srv/git/git_test.go deleted file mode 100644 index e615adfefce27..0000000000000 --- a/lib/srv/git/git_test.go +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package git - -import ( - "testing" - - "github.com/gravitational/trace" - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport/api/types" -) - -func TestCheckSSHCommand(t *testing.T) { - server, err := types.NewGitHubServer(types.GitHubServerMetadata{ - Integration: "my-org", - Organization: "my-org", - }) - require.NoError(t, err) - - tests := []struct { - name string - server types.Server - sshCommand string - checkError require.ErrorAssertionFunc - }{ - { - name: "success", - server: server, - sshCommand: "git-upload-pack 'my-org/my-repo.git'", - checkError: require.NoError, - }, - { - name: "org does not match", - server: server, - sshCommand: "git-upload-pack 'some-other-org/my-repo.git'", - checkError: func(t require.TestingT, err error, i ...interface{}) { - require.True(t, trace.IsAccessDenied(err), i...) - }, - }, - { - name: "invalid command", - server: server, - sshCommand: "not-git-command", - checkError: func(t require.TestingT, err error, i ...interface{}) { - require.True(t, trace.IsBadParameter(err), i...) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.checkError(t, CheckSSHCommand(tt.server, tt.sshCommand)) - }) - } -} - -func Test_parseSSHCommand(t *testing.T) { - tests := []struct { - name string - input string - checkError require.ErrorAssertionFunc - wantOutput *command - }{ - { - name: "git-upload-pack", - input: "git-upload-pack 'my-org/my-repo.git'", - checkError: require.NoError, - wantOutput: &command{ - service: "git-upload-pack", - repository: "my-org/my-repo.git", - }, - }, - { - name: "git-upload-pack with double quote", - input: "git-upload-pack \"my-org/my-repo.git\"", - checkError: require.NoError, - wantOutput: &command{ - service: "git-upload-pack", - repository: "my-org/my-repo.git", - }, - }, - { - name: "git-upload-pack with args", - input: "git-upload-pack --strict 'my-org/my-repo.git'", - checkError: require.NoError, - wantOutput: &command{ - service: "git-upload-pack", - repository: "my-org/my-repo.git", - }, - }, - { - name: "missing quote", - input: "git-upload-pack 'my-org/my-repo.git", - checkError: require.Error, - }, - { - name: "git-receive-pack", - input: "git-receive-pack 'my-org/my-repo.git'", - checkError: require.NoError, - wantOutput: &command{ - service: "git-receive-pack", - repository: "my-org/my-repo.git", - }, - }, - { - name: "missing args", - input: "git-receive-pack", - checkError: require.Error, - }, - { - name: "unsupported", - input: "git-cat-file", - checkError: require.Error, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - output, err := parseSSHCommand(tt.input) - tt.checkError(t, err) - require.Equal(t, tt.wantOutput, output) - }) - } -} diff --git a/lib/srv/git/github_test.go b/lib/srv/git/github_test.go index ff61bad7fc44e..4a5ff77d523b7 100644 --- a/lib/srv/git/github_test.go +++ b/lib/srv/git/github_test.go @@ -32,8 +32,7 @@ import ( integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils/keys" - "github.com/gravitational/teleport/lib/fixtures" + apisshutils "github.com/gravitational/teleport/api/utils/sshutils" ) type fakeAuthPreferenceGetter struct { @@ -53,11 +52,7 @@ func (f fakeGitHubUserCertGenerator) GenerateGitHubUserCert(_ context.Context, i return nil, trace.CompareFailed("expect ttl %v but got %v", f.checkTTL, input.Ttl.AsDuration()) } - signer, err := keys.ParsePrivateKey([]byte(fixtures.SSHCAPrivateKey)) - if err != nil { - return nil, trace.Wrap(err) - } - caSigner, err := ssh.NewSignerFromKey(signer) + caSigner, err := apisshutils.MakeTestSSHCA() if err != nil { return nil, trace.Wrap(err) } @@ -83,11 +78,7 @@ func (f fakeGitHubUserCertGenerator) GenerateGitHubUserCert(_ context.Context, i func TestMakeGitHubSigner(t *testing.T) { clock := clockwork.NewFakeClock() - server, err := types.NewGitHubServer(types.GitHubServerMetadata{ - Integration: "org", - Organization: "org", - }) - require.NoError(t, err) + server := makeGitServer(t, "org") tests := []struct { name string diff --git a/lib/srv/reexec.go b/lib/srv/reexec.go index 1cb4efec635d0..9e3b4a9983e08 100644 --- a/lib/srv/reexec.go +++ b/lib/srv/reexec.go @@ -405,7 +405,7 @@ func RunCommand() (errw io.Writer, code int, err error) { } } - return io.Discard, exitCode(err), trace.Wrap(err) + return io.Discard, sshutils.ExitCodeFromExecError(err), trace.Wrap(err) } // waitForShell waits either for the command to return or the kill signal from the parent Teleport process. diff --git a/lib/sshutils/exec.go b/lib/sshutils/exec.go new file mode 100644 index 0000000000000..025a1394ddfcf --- /dev/null +++ b/lib/sshutils/exec.go @@ -0,0 +1,59 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package sshutils + +import ( + "context" + "errors" + "log/slog" + "os/exec" + "syscall" + + "golang.org/x/crypto/ssh" + + "github.com/gravitational/teleport" +) + +// ExitCodeFromExecError extracts and returns the exit code from the +// error. +func ExitCodeFromExecError(err error) int { + // If no error occurred, return 0 (success). + if err == nil { + return teleport.RemoteCommandSuccess + } + + var execExitErr *exec.ExitError + var sshExitErr *ssh.ExitError + switch { + // Local execution. + case errors.As(err, &execExitErr): + waitStatus, ok := execExitErr.Sys().(syscall.WaitStatus) + if !ok { + return teleport.RemoteCommandFailure + } + return waitStatus.ExitStatus() + // Remote execution. + case errors.As(err, &sshExitErr): + return sshExitErr.ExitStatus() + // An error occurred, but the type is unknown, return a generic 255 code. + default: + slog.DebugContext(context.Background(), "Unknown error returned when executing command", "error", err) + return teleport.RemoteCommandFailure + } +} diff --git a/lib/sshutils/reply.go b/lib/sshutils/reply.go new file mode 100644 index 0000000000000..dddde0cc86798 --- /dev/null +++ b/lib/sshutils/reply.go @@ -0,0 +1,113 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package sshutils + +import ( + "context" + "errors" + "fmt" + "io" + "log/slog" + + "golang.org/x/crypto/ssh" + + "github.com/gravitational/teleport/lib/utils" +) + +// Reply is a helper to handle replying/rejecting and log messages when needed. +type Reply struct { + log *slog.Logger +} + +// NewReply creates a new reply helper for SSH servers. +func NewReply(log *slog.Logger) *Reply { + return &Reply{log: log} +} + +// RejectChannel rejects the channel with provided message. +func (r *Reply) RejectChannel(ctx context.Context, nch ssh.NewChannel, reason ssh.RejectionReason, msg string) { + err := nch.Reject(reason, msg) + if err != nil { + r.log.WarnContext(ctx, "Failed to reject channel", "error", err) + } +} + +// RejectUnknownChannel rejects the channel with reason ssh.UnknownChannelType. +func (r *Reply) RejectUnknownChannel(ctx context.Context, nch ssh.NewChannel) { + channelType := nch.ChannelType() + r.log.WarnContext(ctx, "Unknown channel type", "channel", channelType) + r.RejectChannel(ctx, nch, ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %v", channelType)) +} + +// RejectWithAcceptError rejects the channel when ssh.NewChannel.Accept fails. +func (r *Reply) RejectWithAcceptError(ctx context.Context, nch ssh.NewChannel, err error) { + r.log.WarnContext(ctx, "Unable to accept channel", "channel", nch.ChannelType(), "error", err) + r.RejectChannel(ctx, nch, ssh.ConnectionFailed, fmt.Sprintf("unable to accept channel: %v", err)) +} + +// RejectWithNewRemoteSessionError rejects the channel when the corresponding +// remote session fails to create. +func (r *Reply) RejectWithNewRemoteSessionError(ctx context.Context, nch ssh.NewChannel, remoteError error) { + r.log.WarnContext(ctx, "Remote session open failed", "error", remoteError) + reason, msg := ssh.ConnectionFailed, fmt.Sprintf("remote session open failed: %v", remoteError) + var e *ssh.OpenChannelError + if errors.As(remoteError, &e) { + reason, msg = e.Reason, e.Message + } + r.RejectChannel(ctx, nch, reason, msg) +} + +// ReplyError replies an error to an ssh.Request. +func (r *Reply) ReplyError(ctx context.Context, ch ssh.Channel, req *ssh.Request, err error) { + r.log.ErrorContext(ctx, "failure handling SSH request", "request_type", req.Type, "error", err) + // Terminate the error with a newline when writing to remote channel's + // stderr so the output does not mix with the rest of the output if the remote + // side is not doing additional formatting for extended data. + // See github.com/gravitational/teleport/issues/4542 + message := utils.FormatErrorWithNewline(err) + r.writeStderr(ctx, ch, message) + if req.WantReply { + if err := req.Reply(false, []byte(message)); err != nil { + r.log.ErrorContext(ctx, "failed sending error Reply on SSH channel", "error", err) + } + } +} + +func (r *Reply) writeStderr(ctx context.Context, ch ssh.Channel, msg string) { + if _, err := io.WriteString(ch.Stderr(), msg); err != nil { + r.log.WarnContext(ctx, "Failed writing to stderr of SSH channel", "error", err) + } +} + +// ReplyRequest replies to an ssh.Request with provided ok and payload. +func (r *Reply) ReplyRequest(ctx context.Context, req *ssh.Request, ok bool, payload []byte) { + if req.WantReply { + if err := req.Reply(ok, payload); err != nil { + r.log.ErrorContext(ctx, "failed replying OK to SSH request", "request_type", req.Type, "error", err) + } + } +} + +// SendExitStatus sends an exit-status. +func (r *Reply) SendExitStatus(ctx context.Context, ch ssh.Channel, code int) { + _, err := ch.SendRequest("exit-status", false, ssh.Marshal(struct{ C uint32 }{C: uint32(code)})) + if err != nil { + r.log.InfoContext(ctx, "Failed to send exit status", "error", err) + } +} diff --git a/lib/sshutils/server.go b/lib/sshutils/server.go index 7020c302342c6..2d8f7cb85d4bc 100644 --- a/lib/sshutils/server.go +++ b/lib/sshutils/server.go @@ -715,6 +715,13 @@ type NewConnHandler interface { HandleNewConn(ctx context.Context, ccx *ConnectionContext) (context.Context, error) } +// NewConnHandlerFunc wraps a function to satisfy NewConnHandler interface. +type NewConnHandlerFunc func(ctx context.Context, ccx *ConnectionContext) (context.Context, error) + +func (f NewConnHandlerFunc) HandleNewConn(ctx context.Context, ccx *ConnectionContext) (context.Context, error) { + return f(ctx, ccx) +} + type AuthMethods struct { PublicKey PublicKeyFunc Password PasswordFunc diff --git a/lib/sshutils/utils.go b/lib/sshutils/utils.go index 5f08a40748fbe..7dcd762629e74 100644 --- a/lib/sshutils/utils.go +++ b/lib/sshutils/utils.go @@ -23,6 +23,7 @@ import ( "strconv" "github.com/gravitational/trace" + "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/lib/utils" ) @@ -42,3 +43,22 @@ func SplitHostPort(addrString string) (string, uint32, error) { } return addr.Host(), uint32(addr.Port(0)), nil } + +// SSHConnMetadataWithUser overrides an ssh.ConnMetadata with provided user. +type SSHConnMetadataWithUser struct { + ssh.ConnMetadata + user string +} + +// NewSSHConnMetadataWithUser overrides an ssh.ConnMetadata with provided user. +func NewSSHConnMetadataWithUser(conn ssh.ConnMetadata, user string) SSHConnMetadataWithUser { + return SSHConnMetadataWithUser{ + ConnMetadata: conn, + user: user, + } +} + +// User returns the user ID for this connection. +func (s SSHConnMetadataWithUser) User() string { + return s.user +} diff --git a/lib/utils/utils.go b/lib/utils/utils.go index 1990f39ad23e2..432bb46a400bb 100644 --- a/lib/utils/utils.go +++ b/lib/utils/utils.go @@ -359,7 +359,7 @@ func ReadPath(path string) ([]byte, error) { abs, err := filepath.EvalSymlinks(s) if err != nil { if errors.Is(err, fs.ErrPermission) { - //do not convert to system error as this loses the ability to compare that it is a permission error + // do not convert to system error as this loses the ability to compare that it is a permission error return nil, err } return nil, trace.ConvertSystemError(err) @@ -367,7 +367,7 @@ func ReadPath(path string) ([]byte, error) { bytes, err := os.ReadFile(abs) if err != nil { if errors.Is(err, fs.ErrPermission) { - //do not convert to system error as this loses the ability to compare that it is a permission error + // do not convert to system error as this loses the ability to compare that it is a permission error return nil, err } return nil, trace.ConvertSystemError(err)