diff --git a/lib/auth/middleware.go b/lib/auth/middleware.go index aa5ba9eb1d3c3..826ad05a837bd 100644 --- a/lib/auth/middleware.go +++ b/lib/auth/middleware.go @@ -20,6 +20,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/json" "math" "net" "net/http" @@ -51,6 +52,14 @@ import ( "github.com/gravitational/teleport/lib/utils" ) +const ( + // TeleportImpersonateUserHeader is a header that specifies teleport user identity + // that the proxy is impersonating. + TeleportImpersonateUserHeader = "Teleport-Impersonate-User" + // TeleportImpersonateIPHeader is a header that specifies the real user IP address. + TeleportImpersonateIPHeader = "Teleport-Impersonate-IP" +) + // TLSServerConfig is a configuration for TLS server type TLSServerConfig struct { // Listener is a listener to bind to @@ -344,6 +353,11 @@ type Middleware struct { Limiter *limiter.Limiter // GRPCMetrics is the configured grpc metrics for the interceptors GRPCMetrics *om.ServerMetrics + // EnableCredentialsForwarding allows the middleware to receive impersonation + // identity from the client if it presents a valid proxy certificate. + // This is used by the proxy to forward the identity of the user who + // connected to the proxy to the next hop. + EnableCredentialsForwarding bool } // Wrap sets next handler in chain @@ -420,7 +434,6 @@ func (a *Middleware) withAuthenticatedUser(ctx context.Context) (context.Context ctx = authz.ContextWithUser(ctx, identityGetter) return ctx, nil - } func certFromConnState(state *tls.ConnectionState) *x509.Certificate { @@ -578,17 +591,7 @@ func (a *Middleware) GetUser(connState tls.ConnectionState) (authz.IdentityGette Identity: *identity, }, nil } - return authz.RemoteUser{ - ClusterName: certClusterName, - Username: identity.Username, - Principals: identity.Principals, - KubernetesGroups: identity.KubernetesGroups, - KubernetesUsers: identity.KubernetesUsers, - DatabaseNames: identity.DatabaseNames, - DatabaseUsers: identity.DatabaseUsers, - RemoteRoles: identity.Groups, - Identity: *identity, - }, nil + return newRemoteUserFromIdentity(*identity, certClusterName), nil } // code below expects user or service from local cluster, to distinguish between // interactive users and services (e.g. proxies), the code below @@ -607,10 +610,7 @@ func (a *Middleware) GetUser(connState tls.ConnectionState) (authz.IdentityGette } // otherwise assume that is a local role, no need to pass the roles // as it will be fetched from the local database - return authz.LocalUser{ - Username: identity.Username, - Identity: *identity, - }, nil + return newLocalUserFromIdentity(*identity), nil } func findPrimarySystemRole(roles []string) *types.SystemRole { @@ -652,10 +652,43 @@ func (a *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + remoteAddr := r.RemoteAddr + // If the request is coming from a trusted proxy and the proxy is sending a + // TeleportImpersonateHeader, we will impersonate the user in the header + // instead of the user in the TLS certificate. + // This is used by the proxy to impersonate the end user when making requests + // without re-signing the client certificate. + impersonateUser := r.Header.Get(TeleportImpersonateUserHeader) + if impersonateUser != "" { + if !isProxyRole(user) { + trace.WriteError(w, trace.AccessDenied("Credentials forwarding is only permitted for Proxy")) + return + } + // If the service is not configured to allow credentials forwarding, reject the request. + if !a.EnableCredentialsForwarding { + trace.WriteError(w, trace.AccessDenied("Credentials forwarding is not permitted by this service")) + return + } + + if user, err = a.extractIdentityFromImpersonationHeader(impersonateUser); err != nil { + trace.WriteError(w, err) + return + } + remoteAddr = r.Header.Get(TeleportImpersonateIPHeader) + } + + // If the request is coming from a trusted proxy, we already know the user + // and we will impersonate him. At this point, we need to remove the + // TeleportImpersonateHeader from the request, otherwise the proxy will + // attempt sending the request to upstream servers with the impersonation + // header from a fake user. + r.Header.Del(TeleportImpersonateUserHeader) + r.Header.Del(TeleportImpersonateIPHeader) + // determine authenticated user based on the request parameters ctx := r.Context() ctx = authz.ContextWithUserCertificate(ctx, certFromConnState(r.TLS)) - clientSrcAddr, err := utils.ParseAddr(r.RemoteAddr) + clientSrcAddr, err := utils.ParseAddr(remoteAddr) if err == nil { ctx = authz.ContextWithClientAddr(ctx, clientSrcAddr) } @@ -778,3 +811,113 @@ func ClientCertPool(client AccessCache, clusterName string, caTypes ...types.Cer func DefaultClientCertPool(client AccessCache, clusterName string) (*x509.CertPool, int64, error) { return ClientCertPool(client, clusterName, types.HostCA, types.UserCA) } + +// isProxyRole returns true if the certificate role is a proxy role. +func isProxyRole(identity authz.IdentityGetter) bool { + switch id := identity.(type) { + case authz.RemoteBuiltinRole: + return id.Role == types.RoleProxy + case authz.BuiltinRole: + return id.Role == types.RoleProxy + default: + return false + } +} + +// extractIdentityFromImpersonationHeader extracts the identity from the impersonation +// header and returns it. If the impersonation header holds an identity of a +// system role, an error is returned. +func (a *Middleware) extractIdentityFromImpersonationHeader(impersonate string) (authz.IdentityGetter, error) { + // Unmarshal the impersonated user from the header. + var impersonatedIdentity tlsca.Identity + if err := json.Unmarshal([]byte(impersonate), &impersonatedIdentity); err != nil { + return nil, trace.Wrap(err) + } + + switch { + case findPrimarySystemRole(impersonatedIdentity.Groups) != nil: + // make sure that this user does not have system role + // since system roles are not allowed to be impersonated. + return nil, trace.AccessDenied("can not impersonate a system role") + case impersonatedIdentity.TeleportCluster != a.ClusterName: + // if the impersonated user is from a different cluster, we need to + // use him as remote user. + return newRemoteUserFromIdentity(impersonatedIdentity, impersonatedIdentity.TeleportCluster), nil + default: + // otherwise assume that is a local role, no need to pass the roles + // as it will be fetched from the local database + return newLocalUserFromIdentity(impersonatedIdentity), nil + } +} + +// newRemoteUserFromIdentity creates a new remote user from the identity. +func newRemoteUserFromIdentity(identity tlsca.Identity, clusterName string) authz.RemoteUser { + return authz.RemoteUser{ + ClusterName: clusterName, + Username: identity.Username, + Principals: identity.Principals, + KubernetesGroups: identity.KubernetesGroups, + KubernetesUsers: identity.KubernetesUsers, + DatabaseNames: identity.DatabaseNames, + DatabaseUsers: identity.DatabaseUsers, + RemoteRoles: identity.Groups, + Identity: identity, + } +} + +// newLocalUserFromIdentity creates a new local user from the identity. +func newLocalUserFromIdentity(identity tlsca.Identity) authz.LocalUser { + return authz.LocalUser{ + Username: identity.Username, + Identity: identity, + } +} + +// ImpersonatorRoundTripper is a round tripper that impersonates a user with +// the identity provided. +type ImpersonatorRoundTripper struct { + http.RoundTripper +} + +// NewImpersonatorRoundTripper returns a new impersonator round tripper. +func NewImpersonatorRoundTripper(rt http.RoundTripper) *ImpersonatorRoundTripper { + return &ImpersonatorRoundTripper{ + RoundTripper: rt, + } +} + +// RoundTrip implements http.RoundTripper interface to include the identity +// in the request header. +func (r *ImpersonatorRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + identity, err := authz.UserFromContext(req.Context()) + if err != nil { + return nil, trace.Wrap(err) + } + b, err := json.Marshal(identity.GetIdentity()) + if err != nil { + return nil, trace.Wrap(err) + } + req.Header.Set(TeleportImpersonateUserHeader, string(b)) + defer req.Header.Del(TeleportImpersonateUserHeader) + + clientSrcAddr, err := authz.ClientAddrFromContext(req.Context()) + if err != nil { + return nil, trace.Wrap(err) + } + + req.Header.Set(TeleportImpersonateIPHeader, clientSrcAddr.String()) + defer req.Header.Del(TeleportImpersonateIPHeader) + + return r.RoundTripper.RoundTrip(req) +} + +// CloseIdleConnections ensures that the returned [net.RoundTripper] +// has a CloseIdleConnections method. +func (r *ImpersonatorRoundTripper) CloseIdleConnections() { + type closeIdler interface { + CloseIdleConnections() + } + if c, ok := r.RoundTripper.(closeIdler); ok { + c.CloseIdleConnections() + } +} diff --git a/lib/auth/middleware_test.go b/lib/auth/middleware_test.go index fb0d1ac80700b..9811a442564bf 100644 --- a/lib/auth/middleware_test.go +++ b/lib/auth/middleware_test.go @@ -17,11 +17,15 @@ limitations under the License. package auth import ( + "bytes" "context" "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/json" "net" + "net/http" + "net/http/httptest" "testing" "time" @@ -194,7 +198,6 @@ func TestMiddlewareGetUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - m := &Middleware{ ClusterName: localClusterName, } @@ -352,8 +355,10 @@ func TestWrapContextWithUser(t *testing.T) { } conn := &testConn{ - state: tls.ConnectionState{PeerCertificates: tt.peers, - HandshakeComplete: !tt.needsHandshake}, + state: tls.ConnectionState{ + PeerCertificates: tt.peers, + HandshakeComplete: !tt.needsHandshake, + }, remoteAddr: utils.MustParseAddr("127.0.0.1:4242"), } @@ -381,3 +386,303 @@ func subject(t *testing.T, id tlsca.Identity) pkix.Name { s.Names = s.ExtraNames return s } + +func TestMiddleware_ServeHTTP(t *testing.T) { + t.Parallel() + localClusterName := "local" + remoteClusterName := "remote" + s := newTestServices(t) + + // Set up local cluster name in the backend. + cn, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{ + ClusterName: localClusterName, + }) + require.NoError(t, err) + require.NoError(t, s.UpsertClusterName(cn)) + + now := time.Date(2020, time.November, 5, 0, 0, 0, 0, time.UTC) + localUserIdentity := tlsca.Identity{ + Username: "foo", + Groups: []string{"devs"}, + TeleportCluster: localClusterName, + Expires: now, + Usage: []string{}, + Principals: []string{}, + } + + remoteUserIdentity := tlsca.Identity{ + Username: "foo", + Groups: []string{"devs"}, + TeleportCluster: remoteClusterName, + Expires: now, + Usage: []string{}, + Principals: []string{}, + } + + proxyIdentity := tlsca.Identity{ + Username: "proxy...", + Groups: []string{string(types.RoleProxy)}, + TeleportCluster: localClusterName, + Expires: now, + Usage: []string{}, + Principals: []string{}, + } + + dbIdentity := tlsca.Identity{ + Username: "db...", + Groups: []string{string(types.RoleDatabase)}, + TeleportCluster: localClusterName, + Expires: now, + Usage: []string{}, + Principals: []string{}, + } + + type args struct { + impersonateIdentity *tlsca.Identity + peers []*x509.Certificate + sourceIPAddr string + impersonatedIPAddr string + } + type want struct { + user authz.IdentityGetter + userIPAddr string + } + tests := []struct { + name string + args args + want want + credentialsForwardingDennied bool + enableCredentialsForwarding bool + }{ + { + name: "local user without impersonation", + args: args{ + peers: []*x509.Certificate{{ + Subject: subject(t, localUserIdentity), + NotAfter: now, + Issuer: pkix.Name{Organization: []string{localClusterName}}, + }}, + sourceIPAddr: "127.0.0.1:6514", + }, + want: want{ + user: authz.LocalUser{ + Username: localUserIdentity.Username, + Identity: localUserIdentity, + }, + userIPAddr: "127.0.0.1:6514", + }, + credentialsForwardingDennied: false, + enableCredentialsForwarding: true, + }, + { + name: "remote user without impersonation", + args: args{ + peers: []*x509.Certificate{{ + Subject: subject(t, remoteUserIdentity), + NotAfter: now, + Issuer: pkix.Name{Organization: []string{remoteClusterName}}, + }}, + sourceIPAddr: "127.0.0.1:6514", + }, + want: want{ + user: authz.RemoteUser{ + Username: remoteUserIdentity.Username, + Identity: remoteUserIdentity, + RemoteRoles: remoteUserIdentity.Groups, + ClusterName: remoteClusterName, + Principals: []string{}, + }, + userIPAddr: "127.0.0.1:6514", + }, + credentialsForwardingDennied: false, + enableCredentialsForwarding: true, + }, + { + name: "proxy without impersonation", + args: args{ + peers: []*x509.Certificate{{ + Subject: subject(t, proxyIdentity), + NotAfter: now, + Issuer: pkix.Name{Organization: []string{localClusterName}}, + }}, + sourceIPAddr: "127.0.0.1:6514", + }, + want: want{ + user: authz.BuiltinRole{ + Username: proxyIdentity.Username, + Identity: proxyIdentity, + Role: types.RoleProxy, + ClusterName: localClusterName, + }, + userIPAddr: "127.0.0.1:6514", + }, + credentialsForwardingDennied: false, + enableCredentialsForwarding: true, + }, + { + name: "db without impersonation", + args: args{ + peers: []*x509.Certificate{{ + Subject: subject(t, dbIdentity), + NotAfter: now, + Issuer: pkix.Name{Organization: []string{localClusterName}}, + }}, + sourceIPAddr: "127.0.0.1:6514", + }, + want: want{ + user: authz.BuiltinRole{ + Username: dbIdentity.Username, + Identity: dbIdentity, + Role: types.RoleDatabase, + ClusterName: localClusterName, + }, + userIPAddr: "127.0.0.1:6514", + }, + credentialsForwardingDennied: false, + enableCredentialsForwarding: true, + }, + { + name: "proxy with impersonation", + args: args{ + peers: []*x509.Certificate{{ + Subject: subject(t, proxyIdentity), + NotAfter: now, + Issuer: pkix.Name{Organization: []string{localClusterName}}, + }}, + impersonateIdentity: &localUserIdentity, + sourceIPAddr: "127.0.0.1:6514", + impersonatedIPAddr: "127.0.0.2:6514", + }, + want: want{ + user: authz.LocalUser{ + Username: localUserIdentity.Username, + Identity: localUserIdentity, + }, + userIPAddr: "127.0.0.2:6514", + }, + credentialsForwardingDennied: false, + enableCredentialsForwarding: true, + }, + { + name: "proxy with remote user impersonation", + args: args{ + peers: []*x509.Certificate{{ + Subject: subject(t, proxyIdentity), + NotAfter: now, + Issuer: pkix.Name{Organization: []string{localClusterName}}, + }}, + impersonateIdentity: &remoteUserIdentity, + sourceIPAddr: "127.0.0.1:6514", + impersonatedIPAddr: "127.0.0.2:6514", + }, + want: want{ + user: authz.RemoteUser{ + Username: remoteUserIdentity.Username, + Identity: remoteUserIdentity, + RemoteRoles: remoteUserIdentity.Groups, + ClusterName: remoteClusterName, + Principals: []string{}, + }, + userIPAddr: "127.0.0.2:6514", + }, + credentialsForwardingDennied: false, + enableCredentialsForwarding: true, + }, + { + name: "db with impersonation but disabled forwarding", + args: args{ + peers: []*x509.Certificate{{ + Subject: subject(t, dbIdentity), + NotAfter: now, + Issuer: pkix.Name{Organization: []string{localClusterName}}, + }}, + impersonateIdentity: &localUserIdentity, + }, + credentialsForwardingDennied: true, + enableCredentialsForwarding: true, + }, + { + name: "proxy with remote user impersonation", + args: args{ + peers: []*x509.Certificate{{ + Subject: subject(t, proxyIdentity), + NotAfter: now, + Issuer: pkix.Name{Organization: []string{localClusterName}}, + }}, + impersonateIdentity: &remoteUserIdentity, + sourceIPAddr: "127.0.0.1:6514", + impersonatedIPAddr: "127.0.0.2:6514", + }, + credentialsForwardingDennied: false, + enableCredentialsForwarding: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Middleware{ + ClusterName: localClusterName, + Handler: &fakeHTTPHandler{ + t: t, + expectedUser: tt.want.user, + mustPanicIfCalled: tt.credentialsForwardingDennied, + userIP: tt.want.userIPAddr, + }, + EnableCredentialsForwarding: tt.enableCredentialsForwarding, + } + r := &http.Request{ + Header: make(http.Header), + TLS: &tls.ConnectionState{ + PeerCertificates: tt.args.peers, + }, + RemoteAddr: tt.args.sourceIPAddr, + } + if tt.args.impersonateIdentity != nil { + data, err := json.Marshal(tt.args.impersonateIdentity) + require.NoError(t, err) + r.Header.Set(TeleportImpersonateUserHeader, string(data)) + r.Header.Set(TeleportImpersonateIPHeader, tt.args.impersonatedIPAddr) + } + rsp := httptest.NewRecorder() + a.ServeHTTP(rsp, r) + if tt.credentialsForwardingDennied { + require.True(t, + bytes.Contains( + rsp.Body.Bytes(), + []byte("Credentials forwarding is only permitted for Proxy"), + ), + ) + } + if !tt.enableCredentialsForwarding { + require.True(t, + bytes.Contains( + rsp.Body.Bytes(), + []byte("Credentials forwarding is not permitted by this service"), + ), + ) + } + }) + } +} + +type fakeHTTPHandler struct { + t *testing.T + expectedUser authz.IdentityGetter + mustPanicIfCalled bool + userIP string +} + +func (h *fakeHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if h.mustPanicIfCalled { + panic("handler should not be called") + } + user, err := authz.UserFromContext(r.Context()) + require.NoError(h.t, err) + require.Equal(h.t, h.expectedUser, user) + clientSrcAddr, err := authz.ClientAddrFromContext(r.Context()) + require.NoError(h.t, err) + require.Equal(h.t, h.userIP, clientSrcAddr.String()) + // Ensure that the Teleport-Impersonate-User header is not set on the request + // after the middleware has run. + require.Empty(h.t, r.Header.Get(TeleportImpersonateUserHeader)) + require.Empty(h.t, r.Header.Get(TeleportImpersonateIPHeader)) +}