diff --git a/integration/kube_integration_test.go b/integration/kube_integration_test.go index 6e85d21494e84..4bf8ecc856fe5 100644 --- a/integration/kube_integration_test.go +++ b/integration/kube_integration_test.go @@ -167,7 +167,11 @@ func (s *KubeSuite) TestKubeExec(c *check.C) { defer t.Stop(true) // impersonating client requests will be denied - impersonatingProxyClient, impersonatingProxyClientConfig, err := kubeProxyClient(t, username, &rest.ImpersonationConfig{UserName: "bob", Groups: []string{"system: masters"}}) + impersonatingProxyClient, impersonatingProxyClientConfig, err := kubeProxyClient(kubeProxyConfig{ + t: t, + username: username, + impersonation: &rest.ImpersonationConfig{UserName: "bob", Groups: []string{"system: masters"}}, + }) c.Assert(err, check.IsNil) // try get request to fetch available pods @@ -177,7 +181,7 @@ func (s *KubeSuite) TestKubeExec(c *check.C) { c.Assert(err, check.NotNil) // set up kube configuration using proxy - proxyClient, proxyClientConfig, err := kubeProxyClient(t, username, nil) + proxyClient, proxyClientConfig, err := kubeProxyClient(kubeProxyConfig{t: t, username: username}) c.Assert(err, check.IsNil) // try get request to fetch available pods @@ -294,7 +298,7 @@ func (s *KubeSuite) TestKubePortForward(c *check.C) { defer t.Stop(true) // set up kube configuration using proxy - _, proxyClientConfig, err := kubeProxyClient(t, username, nil) + _, proxyClientConfig, err := kubeProxyClient(kubeProxyConfig{t: t, username: username}) c.Assert(err, check.IsNil) // pick the first kube-dns pod and run port forwarding on it @@ -339,7 +343,11 @@ func (s *KubeSuite) TestKubePortForward(c *check.C) { c.Assert(len(addr), check.Not(check.Equals), 0) // impersonating client requests will be denied - _, impersonatingProxyClientConfig, err := kubeProxyClient(t, username, &rest.ImpersonationConfig{UserName: "bob", Groups: []string{"system: masters"}}) + _, impersonatingProxyClientConfig, err := kubeProxyClient(kubeProxyConfig{ + t: t, + username: username, + impersonation: &rest.ImpersonationConfig{UserName: "bob", Groups: []string{"system: masters"}}, + }) c.Assert(err, check.IsNil) localPort = s.ports.Pop() @@ -356,8 +364,271 @@ func (s *KubeSuite) TestKubePortForward(c *check.C) { c.Assert(err.Error(), check.Matches, ".*impersonation request has been denied.*") } -// TestKubeTrustedClusters tests scenario with trusted clsuters -func (s *KubeSuite) TestKubeTrustedClusters(c *check.C) { +// TestKubeTrustedClustersClientCert tests scenario with trusted clusters +// using metadata encoded in the certificate +func (s *KubeSuite) TestKubeTrustedClustersClientCert(c *check.C) { + + clusterMain := "cluster-main" + mainConf := s.teleKubeConfig(Host) + main := NewInstance(InstanceConfig{ + ClusterName: clusterMain, + HostID: HostID, + NodeName: Host, + Ports: s.ports.PopIntSlice(5), + Priv: s.priv, + Pub: s.pub, + }) + + // main cluster has a role and user called main-kube + username := s.me.Username + mainRole, err := services.NewRole("main-kube", services.RoleSpecV3{ + Allow: services.RoleConditions{ + Logins: []string{username}, + KubeGroups: []string{teleport.KubeSystemMasters}, + }, + }) + main.AddUserWithRole(username, mainRole) + + clusterAux := "cluster-aux" + auxConf := s.teleKubeConfig(Host) + aux := NewInstance(InstanceConfig{ + ClusterName: clusterAux, + HostID: HostID, + NodeName: Host, + Ports: s.ports.PopIntSlice(5), + Priv: s.priv, + Pub: s.pub, + }) + + lib.SetInsecureDevMode(true) + defer lib.SetInsecureDevMode(false) + + mainConf.Proxy.Kube.Enabled = true + err = main.CreateEx(nil, mainConf) + c.Assert(err, check.IsNil) + + err = aux.CreateEx(nil, auxConf) + c.Assert(err, check.IsNil) + + // auxiliary cluster has a role aux-kube + // connect aux cluster to main cluster + // using trusted clusters, so remote user will be allowed to assume + // role specified by mapping remote role "aux-kube" to local role "main-kube" + auxRole, err := services.NewRole("aux-kube", services.RoleSpecV3{ + Allow: services.RoleConditions{ + Logins: []string{username}, + // Note that main cluster can pass it's kubernetes groups + // to the remote cluster, and remote cluster + // can choose to use them by using special variable + KubeGroups: []string{teleport.TraitInternalKubeGroupsVariable}, + }, + }) + c.Assert(err, check.IsNil) + err = aux.Process.GetAuthServer().UpsertRole(auxRole) + c.Assert(err, check.IsNil) + trustedClusterToken := "trusted-clsuter-token" + err = main.Process.GetAuthServer().UpsertToken( + services.MustCreateProvisionToken(trustedClusterToken, []teleport.Role{teleport.RoleTrustedCluster}, time.Time{})) + c.Assert(err, check.IsNil) + trustedCluster := main.Secrets.AsTrustedCluster(trustedClusterToken, services.RoleMap{ + {Remote: mainRole.GetName(), Local: []string{auxRole.GetName()}}, + }) + c.Assert(err, check.IsNil) + + // start both clusters + err = main.Start() + c.Assert(err, check.IsNil) + defer main.Stop(true) + + err = aux.Start() + c.Assert(err, check.IsNil) + defer aux.Stop(true) + + // try and upsert a trusted cluster + var upsertSuccess bool + for i := 0; i < 10; i++ { + log.Debugf("Will create trusted cluster %v, attempt %v", trustedCluster, i) + _, err = aux.Process.GetAuthServer().UpsertTrustedCluster(trustedCluster) + if err != nil { + if trace.IsConnectionProblem(err) { + log.Debugf("retrying on connection problem: %v", err) + continue + } + c.Fatalf("got non connection problem %v", err) + } + upsertSuccess = true + break + } + // make sure we upsert a trusted cluster + c.Assert(upsertSuccess, check.Equals, true) + + // wait for both sites to see each other via their reverse tunnels (for up to 10 seconds) + abortTime := time.Now().Add(time.Second * 10) + for len(main.Tunnel.GetSites()) < 2 && len(main.Tunnel.GetSites()) < 2 { + time.Sleep(time.Millisecond * 2000) + if time.Now().After(abortTime) { + c.Fatalf("two clusters do not see each other: tunnels are not working") + } + } + + // impersonating client requests will be denied + impersonatingProxyClient, impersonatingProxyClientConfig, err := kubeProxyClient(kubeProxyConfig{ + t: main, + username: username, + impersonation: &rest.ImpersonationConfig{UserName: "bob", Groups: []string{"system: masters"}}, + routeToCluster: clusterAux, + }) + c.Assert(err, check.IsNil) + + // try get request to fetch available pods + _, err = impersonatingProxyClient.Core().Pods(kubeSystemNamespace).List(metav1.ListOptions{ + LabelSelector: kubeDNSLabels.AsSelector().String(), + }) + c.Assert(err, check.NotNil) + + // set up kube configuration using main proxy + proxyClient, proxyClientConfig, err := kubeProxyClient(kubeProxyConfig{ + t: main, + username: username, + routeToCluster: clusterAux, + }) + c.Assert(err, check.IsNil) + + // try get request to fetch available pods + pods, err := proxyClient.Core().Pods(kubeSystemNamespace).List(metav1.ListOptions{ + LabelSelector: kubeDNSLabels.AsSelector().String(), + }) + c.Assert(len(pods.Items), check.Not(check.Equals), int(0)) + + // Exec through proxy and collect output + pod := pods.Items[0] + + out := &bytes.Buffer{} + err = kubeExec(proxyClientConfig, kubeExecArgs{ + podName: pod.Name, + podNamespace: pod.Namespace, + container: kubeDNSContainer, + command: []string{"/bin/cat", "/var/run/secrets/kubernetes.io/serviceaccount/namespace"}, + stdout: out, + }) + c.Assert(err, check.IsNil) + + data := out.Bytes() + c.Assert(string(data), check.Equals, string(pod.Namespace)) + + // interactive command, allocate pty + term := NewTerminal(250) + // lets type "echo hi" followed by "enter" and then "exit" + "enter": + term.Type("\aecho hi\n\r\aexit\n\r\a") + + out = &bytes.Buffer{} + err = kubeExec(proxyClientConfig, kubeExecArgs{ + podName: pod.Name, + podNamespace: pod.Namespace, + container: kubeDNSContainer, + command: []string{"/bin/sh"}, + stdout: out, + tty: true, + stdin: &term, + }) + c.Assert(err, check.IsNil) + + // verify the session stream output + sessionStream := out.String() + comment := check.Commentf("%q", sessionStream) + c.Assert(strings.Contains(sessionStream, "echo hi"), check.Equals, true, comment) + c.Assert(strings.Contains(sessionStream, "exit"), check.Equals, true, comment) + + // verify traffic capture and upload, wait for the upload to hit + var sessionID string + timeoutC := time.After(10 * time.Second) +loop: + for { + select { + case event := <-main.UploadEventsC: + sessionID = event.SessionID + break loop + case <-timeoutC: + c.Fatalf("Timeout waiting for upload of session to complete") + } + } + + // read back the entire session and verify that it matches the stated output + capturedStream, err := main.Process.GetAuthServer().GetSessionChunk(defaults.Namespace, session.ID(sessionID), 0, events.MaxChunkBytes) + c.Assert(err, check.IsNil) + + c.Assert(string(capturedStream), check.Equals, sessionStream) + + // impersonating kube exec should be denied + // interactive command, allocate pty + term = NewTerminal(250) + term.Type("\aecho hi\n\r\aexit\n\r\a") + out = &bytes.Buffer{} + err = kubeExec(impersonatingProxyClientConfig, kubeExecArgs{ + podName: pod.Name, + podNamespace: pod.Namespace, + container: kubeDNSContainer, + command: []string{"/bin/sh"}, + stdout: out, + tty: true, + stdin: &term, + }) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Matches, ".*impersonation request has been denied.*") + + // forward local port to target port 53 of the dnsmasq container + localPort := s.ports.Pop() + + forwarder, err := newPortForwarder(proxyClientConfig, kubePortForwardArgs{ + ports: []string{fmt.Sprintf("%v:53", localPort)}, + podName: pod.Name, + podNamespace: pod.Namespace, + }) + c.Assert(err, check.IsNil) + go func() { + err := forwarder.ForwardPorts() + if err != nil { + c.Fatalf("Forward ports exited with error: %v.", err) + } + }() + + select { + case <-time.After(5 * time.Second): + c.Fatalf("Timeout waiting for port forwarding.") + case <-forwarder.readyC: + } + defer close(forwarder.stopC) + + resolver := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return net.Dial("tcp", fmt.Sprintf("localhost:%v", localPort)) + }, + } + addr, err := resolver.LookupHost(context.TODO(), "kubernetes.default.svc.cluster.local") + c.Assert(err, check.IsNil) + c.Assert(len(addr), check.Not(check.Equals), 0) + + // impersonating client requests will be denied + localPort = s.ports.Pop() + impersonatingForwarder, err := newPortForwarder(impersonatingProxyClientConfig, kubePortForwardArgs{ + ports: []string{fmt.Sprintf("%v:53", localPort)}, + podName: pod.Name, + podNamespace: pod.Namespace, + }) + c.Assert(err, check.IsNil) + + // This request should be denied + err = impersonatingForwarder.ForwardPorts() + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Matches, ".*impersonation request has been denied.*") + +} + +// TestKubeTrustedClustersSNI tests scenario with trusted clsuters +// using SNI-forwarding +// DELETE IN(4.3.0) +func (s *KubeSuite) TestKubeTrustedClustersSNI(c *check.C) { clusterMain := "cluster-main" mainConf := s.teleKubeConfig(Host) @@ -467,7 +738,11 @@ func (s *KubeSuite) TestKubeTrustedClusters(c *check.C) { } // impersonating client requests will be denied - impersonatingProxyClient, impersonatingProxyClientConfig, err := kubeProxyClient(main, username, &rest.ImpersonationConfig{UserName: "bob", Groups: []string{"system: masters"}}) + impersonatingProxyClient, impersonatingProxyClientConfig, err := kubeProxyClient(kubeProxyConfig{ + t: main, + username: username, + impersonation: &rest.ImpersonationConfig{UserName: "bob", Groups: []string{"system: masters"}}, + }) c.Assert(err, check.IsNil) // try get request to fetch available pods @@ -477,7 +752,7 @@ func (s *KubeSuite) TestKubeTrustedClusters(c *check.C) { c.Assert(err, check.NotNil) // set up kube configuration using main proxy - proxyClient, proxyClientConfig, err := kubeProxyClient(main, username, nil) + proxyClient, proxyClientConfig, err := kubeProxyClient(kubeProxyConfig{t: main, username: username}) c.Assert(err, check.IsNil) // try get request to fetch available pods @@ -666,7 +941,7 @@ func (s *KubeSuite) runKubeDisconnectTest(c *check.C, tc disconnectTestCase) { defer t.Stop(true) // set up kube configuration using proxy - proxyClient, proxyClientConfig, err := kubeProxyClient(t, username, nil) + proxyClient, proxyClientConfig, err := kubeProxyClient(kubeProxyConfig{t: t, username: username}) c.Assert(err, check.IsNil) // try get request to fetch available pods @@ -758,9 +1033,16 @@ func tlsClientConfig(cfg *rest.Config) (*tls.Config, error) { return tlsConfig, nil } +type kubeProxyConfig struct { + t *TeleInstance + username string + impersonation *rest.ImpersonationConfig + routeToCluster string +} + // kubeProxyClient returns kubernetes client using local teleport proxy -func kubeProxyClient(t *TeleInstance, username string, impersonation *rest.ImpersonationConfig) (*kubernetes.Clientset, *rest.Config, error) { - authServer := t.Process.GetAuthServer() +func kubeProxyClient(cfg kubeProxyConfig) (*kubernetes.Clientset, *rest.Config, error) { + authServer := cfg.t.Process.GetAuthServer() clusterName, err := authServer.GetClusterName() if err != nil { return nil, nil, trace.Wrap(err) @@ -775,7 +1057,10 @@ func kubeProxyClient(t *TeleInstance, username string, impersonation *rest.Imper } cert, key, err := auth.GenerateCertificate(authServer, - auth.TestIdentity{I: auth.LocalUser{Username: username}}) + auth.TestIdentity{ + I: auth.LocalUser{Username: cfg.username}, + RouteToCluster: cfg.routeToCluster, + }) if err != nil { return nil, nil, trace.Wrap(err) } @@ -786,11 +1071,11 @@ func kubeProxyClient(t *TeleInstance, username string, impersonation *rest.Imper KeyData: key, } config := &rest.Config{ - Host: "https://" + t.Config.Proxy.Kube.ListenAddr.Addr, + Host: "https://" + cfg.t.Config.Proxy.Kube.ListenAddr.Addr, TLSClientConfig: tlsClientConfig, } - if impersonation != nil { - config.Impersonate = *impersonation + if cfg.impersonation != nil { + config.Impersonate = *cfg.impersonation } client, err := kubernetes.NewForConfig(config) if err != nil { diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index 50fcbd4b4d173..6c0043461e071 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -17,7 +17,6 @@ limitations under the License. package auth import ( - "context" "encoding/json" "fmt" "io/ioutil" @@ -27,6 +26,7 @@ import ( "time" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/auth/proto" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/httplib" "github.com/gravitational/teleport/lib/services" @@ -261,6 +261,7 @@ func (s *APIServer) withAuth(handler HandlerWithAuthFunc) httprouter.Handle { authServer: s.AuthServer, user: authContext.User, checker: authContext.Checker, + identity: authContext.Identity, sessions: s.SessionService, alog: s.AuthServer.IAuditLog, } @@ -686,7 +687,12 @@ func (s *APIServer) generateUserCert(auth ClientI, w http.ResponseWriter, r *htt if err != nil { return nil, trace.Wrap(err) } - certs, err := auth.GenerateUserCerts(context.TODO(), req.Key, req.User, req.TTL, certificateFormat) + certs, err := auth.GenerateUserCerts(r.Context(), proto.UserCertsRequest{ + PublicKey: req.Key, + Username: req.User, + Expires: s.Now().UTC().Add(req.TTL), + Format: certificateFormat, + }) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 053071ce88319..b574b4dd3ec4d 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -401,6 +401,9 @@ type certRequest struct { // the cert can be only used against kubernetes endpoint, and not auth endpoint, // no usage means unrestricted (to keep backwards compatibility) usage []string + // routeToCluster is an optional cluster name to route the certificate requests to, + // this cluster name will be used to route the requests to in case of kubernetes + routeToCluster string } // GenerateUserTestCerts is used to generate user certificate, used internally for tests @@ -515,10 +518,11 @@ func (s *AuthServer) generateUserCert(req certRequest) (*certs, error) { return nil, trace.Wrap(err) } identity := tlsca.Identity{ - Username: req.user.GetName(), - Groups: req.roles.RoleNames(), - Principals: allowedLogins, - Usage: req.usage, + Username: req.user.GetName(), + Groups: req.roles.RoleNames(), + Principals: allowedLogins, + Usage: req.usage, + RouteToCluster: req.routeToCluster, } certRequest := tlsca.CertificateRequest{ Clock: s.clock, diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 07c3ac8e8a940..577b9a2252c16 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/session" + "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/trace" @@ -35,12 +36,15 @@ import ( "github.com/tstranex/u2f" ) +// AuthWithRoles is a wrapper around auth service +// methods that focuses on authorizing every request type AuthWithRoles struct { authServer *AuthServer checker services.AccessChecker user services.User sessions session.Service alog events.IAuditLog + identity tlsca.Identity } func (a *AuthWithRoles) actionWithContext(ctx *services.Context, namespace string, resource string, action string) error { @@ -808,14 +812,37 @@ func (a *AuthWithRoles) NewKeepAliver(ctx context.Context) (services.KeepAliver, return nil, trace.NotImplemented("not implemented") } -func (a *AuthWithRoles) GenerateUserCerts(ctx context.Context, key []byte, username string, ttl time.Duration, compatibility string) (*proto.Certs, error) { - // This endpoint is only accessible to tctl. - if !a.hasBuiltinRole(string(teleport.RoleAdmin)) { +// GenerateUserCerts generates users certificates +func (a *AuthWithRoles) GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) { + switch { + case a.hasBuiltinRole(string(teleport.RoleAdmin)): + case req.Username == a.user.GetName(): + // user is requesting TTL for themselves, + // limit the TTL to the duration of the session, to prevent + // users renewing their certificates forever + if a.identity.Expires.IsZero() { + log.Warningf("Encountered identity with no expiry: %v and denied request. Must be internal logic error.", a.identity) + return nil, trace.AccessDenied("access denied") + } + req.Expires = a.identity.Expires + if req.Expires.Before(a.authServer.GetClock().Now()) { + return nil, trace.AccessDenied("access denied: client credentials have expired, please relogin.") + } + default: + err := trace.AccessDenied("user %q has requested to generate certs for %q.", a.user.GetName(), req.Username) + log.Warning(err) + a.authServer.EmitAuditEvent(events.UserLocalLoginFailure, events.EventFields{ + events.LoginMethod: events.LoginMethodClientCert, + events.AuthAttemptSuccess: false, + // log the original internal error in audit log + events.AuthAttemptErr: trace.Unwrap(err).Error(), + }) + // this error is vague on purpose, it should not happen unless someone is trying something out of loop return nil, trace.AccessDenied("this request can be only executed by an admin") } // Extract the user and role set for whom the certificate will be generated. - user, err := a.GetUser(username) + user, err := a.GetUser(req.Username) if err != nil { return nil, trace.Wrap(err) } @@ -829,10 +856,11 @@ func (a *AuthWithRoles) GenerateUserCerts(ctx context.Context, key []byte, usern certs, err := a.authServer.generateUserCert(certRequest{ user: user, roles: checker, - ttl: ttl, - compatibility: compatibility, - publicKey: key, - overrideRoleTTL: true, + ttl: req.Expires.Sub(a.authServer.GetClock().Now()), + compatibility: req.Format, + publicKey: req.PublicKey, + overrideRoleTTL: a.hasBuiltinRole(string(teleport.RoleAdmin)), + routeToCluster: req.RouteToCluster, }) if err != nil { return nil, trace.Wrap(err) @@ -1534,16 +1562,13 @@ func NewAdminAuthServer(authServer *AuthServer, sessions session.Service, alog e } // NewAuthWithRoles creates new auth server with access control -func NewAuthWithRoles(authServer *AuthServer, - checker services.AccessChecker, - user services.User, - sessions session.Service, - alog events.IAuditLog) *AuthWithRoles { +func NewAuthWithRoles(ctx AuthContext, authServer *AuthServer, sessions session.Service, alog events.IAuditLog) *AuthWithRoles { return &AuthWithRoles{ authServer: authServer, - checker: checker, + checker: ctx.Checker, sessions: sessions, - user: user, + user: ctx.User, + identity: ctx.Identity, alog: alog, } } diff --git a/lib/auth/clt.go b/lib/auth/clt.go index d20e7d9ccda0d..541d91f846b32 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -1543,17 +1543,12 @@ func (c *Client) GetSignupTokenData(token string) (user string, otpQRCode []byte // GenerateUserCerts takes the public key in the OpenSSH `authorized_keys` plain // text format, signs it using User Certificate Authority signing key and // returns the resulting certificates. -func (c *Client) GenerateUserCerts(ctx context.Context, key []byte, user string, ttl time.Duration, compatibility string) (*proto.Certs, error) { +func (c *Client) GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) { clt, err := c.grpc() if err != nil { return nil, trace.Wrap(err) } - certs, err := clt.GenerateUserCerts(ctx, &proto.UserCertsRequest{ - Key: key, - Username: user, - Ttl: ttl, - Compatibility: compatibility, - }) + certs, err := clt.GenerateUserCerts(ctx, &req) if err != nil { return nil, trail.FromGRPC(err) } @@ -2605,7 +2600,7 @@ type IdentityService interface { // GenerateUserCerts takes the public key in the OpenSSH `authorized_keys` plain // text format, signs it using User Certificate Authority signing key and // returns the resulting certificates. - GenerateUserCerts(ctx context.Context, key []byte, user string, ttl time.Duration, compatibility string) (*proto.Certs, error) + GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) // GetSignupTokenData returns token data for a valid token GetSignupTokenData(token string) (user string, otpQRCode []byte, e error) diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index cc23948185fca..cfc7a2b0ce442 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -139,7 +139,7 @@ func (g *GRPCServer) GenerateUserCerts(ctx context.Context, req *proto.UserCerts if err != nil { return nil, trail.ToGRPC(err) } - certs, err := auth.AuthWithRoles.GenerateUserCerts(ctx, req.Key, req.Username, req.Ttl, req.Compatibility) + certs, err := auth.AuthWithRoles.GenerateUserCerts(ctx, *req) if err != nil { return nil, trail.ToGRPC(err) } @@ -174,6 +174,7 @@ func (g *GRPCServer) authenticate(ctx context.Context) (*grpcContext, error) { authServer: g.AuthServer, user: authContext.User, checker: authContext.Checker, + identity: authContext.Identity, sessions: g.SessionService, alog: g.AuthServer.IAuditLog, }, diff --git a/lib/auth/helpers.go b/lib/auth/helpers.go index 323f458e2822c..601da63d3a441 100644 --- a/lib/auth/helpers.go +++ b/lib/auth/helpers.go @@ -261,11 +261,12 @@ func GenerateCertificate(authServer *AuthServer, identity TestIdentity) ([]byte, return nil, nil, trace.Wrap(err) } certs, err := authServer.generateUserCert(certRequest{ - publicKey: pub, - user: user, - roles: roles, - ttl: identity.TTL, - usage: identity.AcceptedUsage, + publicKey: pub, + user: user, + roles: roles, + ttl: identity.TTL, + usage: identity.AcceptedUsage, + routeToCluster: identity.RouteToCluster, }) if err != nil { return nil, nil, trace.Wrap(err) @@ -467,9 +468,10 @@ func NewTestTLSServer(cfg TestTLSServerConfig) (*TestTLSServer, error) { // TestIdentity is test identity spec used to generate identities in tests type TestIdentity struct { - I interface{} - TTL time.Duration - AcceptedUsage []string + I interface{} + TTL time.Duration + AcceptedUsage []string + RouteToCluster string } // TestUser returns TestIdentity for local user diff --git a/lib/auth/init.go b/lib/auth/init.go index 976bf2c1d2493..624d836ed46ad 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -687,7 +687,7 @@ func ReadTLSIdentityFromKeyPair(keyBytes, certBytes []byte, caCertsBytes [][]byt return nil, trace.Wrap(err, "failed to parse TLS certificate") } - id, err := tlsca.FromSubject(cert.Subject) + id, err := tlsca.FromSubject(cert.Subject, cert.NotAfter) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/middleware.go b/lib/auth/middleware.go index 523842ff07c38..7ca09d61ad5c0 100644 --- a/lib/auth/middleware.go +++ b/lib/auth/middleware.go @@ -188,7 +188,7 @@ func (a *AuthMiddleware) Wrap(h http.Handler) { } // GetUser returns authenticated user based on request metadata set by HTTP server -func (a *AuthMiddleware) GetUser(r *http.Request) (interface{}, error) { +func (a *AuthMiddleware) GetUser(r *http.Request) (IdentityGetter, error) { peers := r.TLS.PeerCertificates if len(peers) > 1 { // when turning intermediaries on, don't forget to verify @@ -210,6 +210,7 @@ func (a *AuthMiddleware) GetUser(r *http.Request) (interface{}, error) { Role: teleport.RoleNop, Username: string(teleport.RoleNop), ClusterName: localClusterName.GetClusterName(), + Identity: tlsca.Identity{}, }, nil } clientCert := peers[0] @@ -219,7 +220,7 @@ func (a *AuthMiddleware) GetUser(r *http.Request) (interface{}, error) { return nil, trace.AccessDenied("access denied: invalid client certificate") } - identity, err := tlsca.FromSubject(clientCert.Subject) + identity, err := tlsca.FromSubject(clientCert.Subject, clientCert.NotAfter) if err != nil { return nil, trace.Wrap(err) } @@ -252,6 +253,7 @@ func (a *AuthMiddleware) GetUser(r *http.Request) (interface{}, error) { Role: *systemRole, Username: identity.Username, ClusterName: certClusterName, + Identity: *identity, }, nil } return RemoteUser{ @@ -260,6 +262,7 @@ func (a *AuthMiddleware) GetUser(r *http.Request) (interface{}, error) { Principals: identity.Principals, KubernetesGroups: identity.KubernetesGroups, RemoteRoles: identity.Groups, + Identity: *identity, }, nil } // code below expects user or service from local cluster, to distinguish between @@ -274,12 +277,14 @@ func (a *AuthMiddleware) GetUser(r *http.Request) (interface{}, error) { Role: *systemRole, Username: identity.Username, ClusterName: localClusterName.GetClusterName(), + Identity: *identity, }, nil } // otherwise assume that is a local role, no need to pass the roles // as it will be fetched from the local database return LocalUser{ Username: identity.Username, + Identity: *identity, }, nil } diff --git a/lib/auth/permissions.go b/lib/auth/permissions.go index 8ca3d7176aa18..4278aba2789f6 100644 --- a/lib/auth/permissions.go +++ b/lib/auth/permissions.go @@ -22,6 +22,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/trace" "github.com/vulcand/predicate/builder" @@ -98,6 +99,8 @@ type AuthContext struct { User services.User // Checker is access checker Checker services.AccessChecker + // Identity is x509 derived identity + Identity tlsca.Identity } // Authorize authorizes user based on identity supplied via context @@ -106,6 +109,20 @@ func (a *authorizer) Authorize(ctx context.Context) (*AuthContext, error) { return nil, trace.AccessDenied("missing authentication context") } userI := ctx.Value(ContextUser) + userWithIdentity, ok := userI.(IdentityGetter) + if !ok { + return nil, trace.AccessDenied("unsupported context type %T", userI) + } + identity := userWithIdentity.GetIdentity() + authContext, err := a.fromUser(userI) + if err != nil { + return nil, trace.Wrap(err) + } + authContext.Identity = identity + return authContext, nil +} + +func (a *authorizer) fromUser(userI interface{}) (*AuthContext, error) { switch user := userI.(type) { case LocalUser: return a.authorizeLocalUser(user) @@ -461,10 +478,23 @@ func contextForLocalUser(username string, identity services.UserGetter, access s // ContextUser is a user set in the context of the request const ContextUser = "teleport-user" -// LocalUsername is a local username +// LocalUser is a local user type LocalUser struct { // Username is local username Username string + // Identity is x509-derived identity used to build this user + Identity tlsca.Identity +} + +// GetIdentity returns client identity +func (l LocalUser) GetIdentity() tlsca.Identity { + return l.Identity +} + +// IdentityGetter returns client identity +type IdentityGetter interface { + // GetIdentity returns x509-derived identity of the user + GetIdentity() tlsca.Identity } // BuiltinRole is the role of the Teleport service. @@ -480,6 +510,14 @@ type BuiltinRole struct { // ClusterName is the name of the local cluster ClusterName string + + // Identity is source x509 used to build this role + Identity tlsca.Identity +} + +// GetIdentity returns client identity +func (r BuiltinRole) GetIdentity() tlsca.Identity { + return r.Identity } // BuiltinRoleSet wraps a services.RoleSet. The type is used to determine if @@ -488,7 +526,7 @@ type BuiltinRoleSet struct { services.RoleSet } -// BuiltinRoleSet wraps a services.RoleSet. The type is used to determine if +// RemoteBuiltinRoleSet wraps a services.RoleSet. The type is used to determine if // the role is a remote builtin or not. type RemoteBuiltinRoleSet struct { services.RoleSet @@ -505,6 +543,14 @@ type RemoteBuiltinRole struct { // ClusterName is the name of the remote cluster. ClusterName string + + // Identity is source x509 used to build this role + Identity tlsca.Identity +} + +// GetIdentity returns client identity +func (r RemoteBuiltinRole) GetIdentity() tlsca.Identity { + return r.Identity } // RemoteUser defines encoded remote user. @@ -524,6 +570,14 @@ type RemoteUser struct { // KubernetesGroups is a list of Kubernetes groups KubernetesGroups []string `json:"kubernetes_groups"` + + // Identity is source x509 used to build this role + Identity tlsca.Identity +} + +// GetIdentity returns client identity +func (r RemoteUser) GetIdentity() tlsca.Identity { + return r.Identity } // GetClusterConfigFunc returns a cached services.ClusterConfig. diff --git a/lib/auth/proto/auth.pb.go b/lib/auth/proto/auth.pb.go index e326f58ea48da..5603551d05ffd 100644 --- a/lib/auth/proto/auth.pb.go +++ b/lib/auth/proto/auth.pb.go @@ -7,8 +7,8 @@ import proto "github.com/golang/protobuf/proto" import fmt "fmt" import math "math" import _ "github.com/gogo/protobuf/gogoproto" -import _ "github.com/golang/protobuf/ptypes/duration" import empty "github.com/golang/protobuf/ptypes/empty" +import _ "github.com/golang/protobuf/ptypes/timestamp" import services "github.com/gravitational/teleport/lib/services" import time "time" @@ -60,7 +60,7 @@ func (x Operation) String() string { return proto.EnumName(Operation_name, int32(x)) } func (Operation) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_auth_593b62452c2941b4, []int{0} + return fileDescriptor_auth_edb58835f7ffeb60, []int{0} } // Event returns cluster event @@ -92,7 +92,7 @@ func (m *Event) Reset() { *m = Event{} } func (m *Event) String() string { return proto.CompactTextString(m) } func (*Event) ProtoMessage() {} func (*Event) Descriptor() ([]byte, []int) { - return fileDescriptor_auth_593b62452c2941b4, []int{0} + return fileDescriptor_auth_edb58835f7ffeb60, []int{0} } func (m *Event) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -184,6 +184,13 @@ func (m *Event) GetResource() isEvent_Resource { return nil } +func (m *Event) GetType() Operation { + if m != nil { + return m.Type + } + return Operation_INIT +} + func (m *Event) GetResourceHeader() *services.ResourceHeader { if x, ok := m.GetResource().(*Event_ResourceHeader); ok { return x.ResourceHeader @@ -545,7 +552,7 @@ func (m *Watch) Reset() { *m = Watch{} } func (m *Watch) String() string { return proto.CompactTextString(m) } func (*Watch) ProtoMessage() {} func (*Watch) Descriptor() ([]byte, []int) { - return fileDescriptor_auth_593b62452c2941b4, []int{1} + return fileDescriptor_auth_edb58835f7ffeb60, []int{1} } func (m *Watch) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -574,6 +581,13 @@ func (m *Watch) XXX_DiscardUnknown() { var xxx_messageInfo_Watch proto.InternalMessageInfo +func (m *Watch) GetKinds() []WatchKind { + if m != nil { + return m.Kinds + } + return nil +} + // WatchKind specifies resource kind to watch type WatchKind struct { // Kind is a resource kind to watch @@ -593,7 +607,7 @@ func (m *WatchKind) Reset() { *m = WatchKind{} } func (m *WatchKind) String() string { return proto.CompactTextString(m) } func (*WatchKind) ProtoMessage() {} func (*WatchKind) Descriptor() ([]byte, []int) { - return fileDescriptor_auth_593b62452c2941b4, []int{2} + return fileDescriptor_auth_edb58835f7ffeb60, []int{2} } func (m *WatchKind) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -622,11 +636,32 @@ func (m *WatchKind) XXX_DiscardUnknown() { var xxx_messageInfo_WatchKind proto.InternalMessageInfo +func (m *WatchKind) GetKind() string { + if m != nil { + return m.Kind + } + return "" +} + +func (m *WatchKind) GetLoadSecrets() bool { + if m != nil { + return m.LoadSecrets + } + return false +} + +func (m *WatchKind) GetName() string { + if m != nil { + return m.Name + } + return "" +} + // Set of certificates corresponding to a single public key. type Certs struct { - // SSH X509 cert (pem encoded). + // SSH X509 cert (PEM-encoded). SSH []byte `protobuf:"bytes,1,opt,name=SSH,proto3" json:"ssh,omitempty"` - // TLS X509 cert (pem encoded). + // TLS X509 cert (PEM-encoded). TLS []byte `protobuf:"bytes,2,opt,name=TLS,proto3" json:"tls,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -637,7 +672,7 @@ func (m *Certs) Reset() { *m = Certs{} } func (m *Certs) String() string { return proto.CompactTextString(m) } func (*Certs) ProtoMessage() {} func (*Certs) Descriptor() ([]byte, []int) { - return fileDescriptor_auth_593b62452c2941b4, []int{3} + return fileDescriptor_auth_edb58835f7ffeb60, []int{3} } func (m *Certs) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -666,16 +701,38 @@ func (m *Certs) XXX_DiscardUnknown() { var xxx_messageInfo_Certs proto.InternalMessageInfo -// User certificate signing request, used by `tsh sign`. +func (m *Certs) GetSSH() []byte { + if m != nil { + return m.SSH + } + return nil +} + +func (m *Certs) GetTLS() []byte { + if m != nil { + return m.TLS + } + return nil +} + +// UserCertRequest specifies certificate-generation parameters +// for a user. type UserCertsRequest struct { - // Pulic key to be signed. - Key []byte `protobuf:"bytes,1,opt,name=Key,proto3" json:"key"` + // PublicKey is a public key to be signed. + PublicKey []byte `protobuf:"bytes,1,opt,name=PublicKey,proto3" json:"public_key"` // Username of key owner. Username string `protobuf:"bytes,2,opt,name=Username,proto3" json:"username"` - // Desired certificate TTL. - Ttl time.Duration `protobuf:"bytes,3,opt,name=Ttl,stdduration" json:"ttl"` - // Certificate format flag. - Compatibility string `protobuf:"bytes,4,opt,name=Compatibility,proto3" json:"compatability,omitempty"` + // Expires is a desired time of the expiry of the certificate, could + // be adjusted based on the permissions + Expires time.Time `protobuf:"bytes,3,opt,name=Expires,stdtime" json:"expires,omitempty"` + // Format encodes the desired SSH Certificate format (either old ssh compatibility + // format to remove some metadata causing trouble with old SSH servers) + // or standard SSH cert format with custom extensions + Format string `protobuf:"bytes,4,opt,name=Format,proto3" json:"format,omitempty"` + // RouteToCluster is an optional cluster name to add to the certificate, + // so that requests originating with this certificate will be redirected + // to this cluster + RouteToCluster string `protobuf:"bytes,5,opt,name=RouteToCluster,proto3" json:"route_to_cluster,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -685,7 +742,7 @@ func (m *UserCertsRequest) Reset() { *m = UserCertsRequest{} } func (m *UserCertsRequest) String() string { return proto.CompactTextString(m) } func (*UserCertsRequest) ProtoMessage() {} func (*UserCertsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_auth_593b62452c2941b4, []int{4} + return fileDescriptor_auth_edb58835f7ffeb60, []int{4} } func (m *UserCertsRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -714,6 +771,41 @@ func (m *UserCertsRequest) XXX_DiscardUnknown() { var xxx_messageInfo_UserCertsRequest proto.InternalMessageInfo +func (m *UserCertsRequest) GetPublicKey() []byte { + if m != nil { + return m.PublicKey + } + return nil +} + +func (m *UserCertsRequest) GetUsername() string { + if m != nil { + return m.Username + } + return "" +} + +func (m *UserCertsRequest) GetExpires() time.Time { + if m != nil { + return m.Expires + } + return time.Time{} +} + +func (m *UserCertsRequest) GetFormat() string { + if m != nil { + return m.Format + } + return "" +} + +func (m *UserCertsRequest) GetRouteToCluster() string { + if m != nil { + return m.RouteToCluster + } + return "" +} + func init() { proto.RegisterType((*Event)(nil), "proto.Event") proto.RegisterType((*Watch)(nil), "proto.Watch") @@ -740,7 +832,7 @@ type AuthServiceClient interface { WatchEvents(ctx context.Context, in *Watch, opts ...grpc.CallOption) (AuthService_WatchEventsClient, error) // UpsertNode upserts node UpsertNode(ctx context.Context, in *services.ServerV2, opts ...grpc.CallOption) (*services.KeepAlive, error) - // GenerateUserCerts generates a set of user certificates for use by `tctl sign`. + // GenerateUserCerts generates a set of user certificates for use by `tctl auth sign`. GenerateUserCerts(ctx context.Context, in *UserCertsRequest, opts ...grpc.CallOption) (*Certs, error) } @@ -845,7 +937,7 @@ type AuthServiceServer interface { WatchEvents(*Watch, AuthService_WatchEventsServer) error // UpsertNode upserts node UpsertNode(context.Context, *services.ServerV2) (*services.KeepAlive, error) - // GenerateUserCerts generates a set of user certificates for use by `tctl sign`. + // GenerateUserCerts generates a set of user certificates for use by `tctl auth sign`. GenerateUserCerts(context.Context, *UserCertsRequest) (*Certs, error) } @@ -1289,11 +1381,11 @@ func (m *UserCertsRequest) MarshalTo(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.Key) > 0 { + if len(m.PublicKey) > 0 { dAtA[i] = 0xa i++ - i = encodeVarintAuth(dAtA, i, uint64(len(m.Key))) - i += copy(dAtA[i:], m.Key) + i = encodeVarintAuth(dAtA, i, uint64(len(m.PublicKey))) + i += copy(dAtA[i:], m.PublicKey) } if len(m.Username) > 0 { dAtA[i] = 0x12 @@ -1303,17 +1395,23 @@ func (m *UserCertsRequest) MarshalTo(dAtA []byte) (int, error) { } dAtA[i] = 0x1a i++ - i = encodeVarintAuth(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdDuration(m.Ttl))) - n14, err := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.Ttl, dAtA[i:]) + i = encodeVarintAuth(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.Expires))) + n14, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Expires, dAtA[i:]) if err != nil { return 0, err } i += n14 - if len(m.Compatibility) > 0 { + if len(m.Format) > 0 { dAtA[i] = 0x22 i++ - i = encodeVarintAuth(dAtA, i, uint64(len(m.Compatibility))) - i += copy(dAtA[i:], m.Compatibility) + i = encodeVarintAuth(dAtA, i, uint64(len(m.Format))) + i += copy(dAtA[i:], m.Format) + } + if len(m.RouteToCluster) > 0 { + dAtA[i] = 0x2a + i++ + i = encodeVarintAuth(dAtA, i, uint64(len(m.RouteToCluster))) + i += copy(dAtA[i:], m.RouteToCluster) } if m.XXX_unrecognized != nil { i += copy(dAtA[i:], m.XXX_unrecognized) @@ -1508,7 +1606,7 @@ func (m *Certs) Size() (n int) { func (m *UserCertsRequest) Size() (n int) { var l int _ = l - l = len(m.Key) + l = len(m.PublicKey) if l > 0 { n += 1 + l + sovAuth(uint64(l)) } @@ -1516,9 +1614,13 @@ func (m *UserCertsRequest) Size() (n int) { if l > 0 { n += 1 + l + sovAuth(uint64(l)) } - l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.Ttl) + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Expires) n += 1 + l + sovAuth(uint64(l)) - l = len(m.Compatibility) + l = len(m.Format) + if l > 0 { + n += 1 + l + sovAuth(uint64(l)) + } + l = len(m.RouteToCluster) if l > 0 { n += 1 + l + sovAuth(uint64(l)) } @@ -2350,7 +2452,7 @@ func (m *UserCertsRequest) Unmarshal(dAtA []byte) error { switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field PublicKey", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { @@ -2374,9 +2476,9 @@ func (m *UserCertsRequest) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...) - if m.Key == nil { - m.Key = []byte{} + m.PublicKey = append(m.PublicKey[:0], dAtA[iNdEx:postIndex]...) + if m.PublicKey == nil { + m.PublicKey = []byte{} } iNdEx = postIndex case 2: @@ -2410,7 +2512,7 @@ func (m *UserCertsRequest) Unmarshal(dAtA []byte) error { iNdEx = postIndex case 3: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Ttl", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Expires", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -2434,13 +2536,42 @@ func (m *UserCertsRequest) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.Ttl, dAtA[iNdEx:postIndex]); err != nil { + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Expires, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 4: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Compatibility", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Format", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuth + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthAuth + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Format = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RouteToCluster", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -2465,7 +2596,7 @@ func (m *UserCertsRequest) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Compatibility = string(dAtA[iNdEx:postIndex]) + m.RouteToCluster = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex @@ -2594,71 +2725,74 @@ var ( ErrIntOverflowAuth = fmt.Errorf("proto: integer overflow") ) -func init() { proto.RegisterFile("auth.proto", fileDescriptor_auth_593b62452c2941b4) } - -var fileDescriptor_auth_593b62452c2941b4 = []byte{ - // 1000 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x55, 0x51, 0x4f, 0xe3, 0x46, - 0x17, 0xc5, 0x9b, 0x04, 0x92, 0x49, 0x60, 0xb3, 0xb3, 0x08, 0x4c, 0xe0, 0x8b, 0x11, 0xdf, 0x0b, - 0xda, 0x56, 0x49, 0x65, 0x54, 0xa9, 0x42, 0x95, 0x56, 0x98, 0x45, 0x65, 0x05, 0xa2, 0x5b, 0x27, - 0x64, 0xa5, 0xf6, 0x21, 0x32, 0xce, 0xdd, 0x60, 0xe1, 0x78, 0xdc, 0x99, 0x71, 0xa4, 0xa8, 0x7f, - 0x64, 0x7f, 0x12, 0x8f, 0x7d, 0xec, 0x93, 0xdb, 0xd2, 0x37, 0x3f, 0xf4, 0x37, 0x54, 0x73, 0xed, - 0x24, 0x76, 0xd8, 0x27, 0x7b, 0xce, 0xb9, 0xe7, 0xdc, 0x99, 0x3b, 0x33, 0x77, 0x08, 0x71, 0x22, - 0x79, 0xdf, 0x09, 0x39, 0x93, 0x8c, 0x56, 0xf0, 0xd3, 0xda, 0x1e, 0xb3, 0x31, 0xc3, 0xdf, 0xae, - 0xfa, 0x4b, 0xc9, 0xd6, 0xfe, 0x98, 0xb1, 0xb1, 0x0f, 0x5d, 0x1c, 0xdd, 0x45, 0x9f, 0xba, 0x30, - 0x09, 0xe5, 0x2c, 0x23, 0xdb, 0xab, 0xe4, 0x28, 0xe2, 0x8e, 0xf4, 0x58, 0x90, 0xf1, 0xa7, 0x63, - 0x4f, 0xde, 0x47, 0x77, 0x1d, 0x97, 0x4d, 0xba, 0x63, 0xee, 0x4c, 0x3d, 0x89, 0xac, 0xe3, 0x77, - 0x25, 0xf8, 0x10, 0x32, 0x2e, 0xbb, 0xbe, 0x77, 0xd7, 0x15, 0xc0, 0xa7, 0x9e, 0x0b, 0xa2, 0x2b, - 0x67, 0x21, 0x88, 0x54, 0x7b, 0x14, 0x57, 0x49, 0xe5, 0x62, 0x0a, 0x81, 0xa4, 0xdf, 0x91, 0x72, - 0x7f, 0x16, 0x82, 0xae, 0x1d, 0x6a, 0xc7, 0x5b, 0x66, 0x33, 0xe5, 0x3b, 0x3f, 0x86, 0x90, 0xe6, - 0xb2, 0x68, 0x12, 0x1b, 0x5b, 0x4a, 0xfa, 0x35, 0x9b, 0x78, 0x12, 0xe7, 0x67, 0xa3, 0x82, 0xfe, - 0x4c, 0xb6, 0x6c, 0x10, 0x2c, 0xe2, 0x2e, 0x5c, 0x82, 0x33, 0x02, 0xae, 0xbf, 0x38, 0xd4, 0x8e, - 0xeb, 0xa6, 0xde, 0x99, 0xa7, 0xec, 0x14, 0x79, 0x6b, 0x27, 0x89, 0x0d, 0xca, 0x33, 0x6c, 0xe9, - 0x77, 0xb9, 0x66, 0xaf, 0x38, 0xd1, 0x21, 0xd9, 0x3c, 0x07, 0x2e, 0xcf, 0x22, 0x79, 0xcf, 0xb8, - 0x27, 0x67, 0x7a, 0x09, 0xad, 0xf7, 0x96, 0xd6, 0x05, 0x7a, 0x60, 0x5a, 0x07, 0x49, 0x6c, 0xe8, - 0x2e, 0x70, 0x39, 0x74, 0xe6, 0x68, 0x21, 0x43, 0xd1, 0x8f, 0xfe, 0x42, 0x1a, 0x3d, 0x55, 0x2f, - 0xb7, 0xcf, 0x1e, 0x20, 0x10, 0x7a, 0x79, 0x75, 0xea, 0x79, 0x76, 0x60, 0x5a, 0xfb, 0x49, 0x6c, - 0xec, 0x0a, 0xc4, 0x86, 0x12, 0xc1, 0x82, 0x7b, 0xc1, 0x8c, 0xba, 0x64, 0xeb, 0x03, 0x67, 0x53, - 0x4f, 0x78, 0x2c, 0x40, 0x48, 0xaf, 0xa0, 0x7d, 0x6b, 0x69, 0x5f, 0xe4, 0x07, 0xa6, 0xf5, 0xbf, - 0x24, 0x36, 0xf6, 0xc2, 0x39, 0x9a, 0xe6, 0x28, 0x96, 0xa8, 0x28, 0xa1, 0x1f, 0x49, 0xfd, 0xdc, - 0x8f, 0x84, 0x04, 0x7e, 0xe3, 0x4c, 0x40, 0x5f, 0xc7, 0x0c, 0xbb, 0xb9, 0x02, 0x2d, 0xc9, 0x81, - 0x69, 0xb5, 0x92, 0xd8, 0xd8, 0x71, 0x53, 0x68, 0x18, 0x38, 0x93, 0x62, 0xf9, 0xf3, 0x4e, 0x58, - 0xfb, 0x74, 0x78, 0xce, 0x82, 0x4f, 0xde, 0x58, 0xdf, 0x78, 0x56, 0xfb, 0x3c, 0x3d, 0x38, 0xc9, - 0x6a, 0x9f, 0x99, 0xbb, 0x88, 0xae, 0xd4, 0x3e, 0x2f, 0xa0, 0xa7, 0xa4, 0x7c, 0x2b, 0x80, 0xeb, - 0x55, 0xf4, 0x6d, 0x2e, 0x7d, 0x15, 0x3a, 0x30, 0xd3, 0x23, 0x17, 0x09, 0xe0, 0x05, 0x13, 0xd4, - 0x28, 0xad, 0xcd, 0x7c, 0xd0, 0x6b, 0xab, 0x5a, 0x85, 0x0e, 0x4e, 0x52, 0x2d, 0x67, 0x7e, 0x71, - 0x7d, 0xa8, 0xa1, 0xd7, 0xa4, 0xa6, 0x16, 0x28, 0x42, 0xc7, 0x05, 0x9d, 0xa0, 0xc1, 0xeb, 0xa5, - 0xc1, 0x82, 0xb2, 0x76, 0x93, 0xd8, 0x78, 0x1d, 0xcc, 0x87, 0x05, 0xa3, 0xa5, 0x01, 0xb5, 0xc8, - 0x7a, 0x0f, 0xf8, 0x14, 0xb8, 0x5e, 0x47, 0x2b, 0x9a, 0x3b, 0x3b, 0x88, 0x0f, 0x4c, 0x6b, 0x3b, - 0x89, 0x8d, 0xa6, 0xc0, 0x51, 0xc1, 0x26, 0x53, 0xaa, 0x52, 0xdb, 0x30, 0x05, 0x2e, 0xa0, 0x1f, - 0x05, 0x01, 0xf8, 0x7a, 0x63, 0xb5, 0xd4, 0x05, 0x7a, 0x7e, 0xcc, 0x79, 0x0a, 0x0e, 0x25, 0xa2, - 0xc5, 0x52, 0x17, 0x04, 0xf4, 0x81, 0x34, 0xd3, 0xbf, 0x73, 0x16, 0x04, 0xe0, 0xaa, 0x1b, 0xad, - 0x6f, 0x62, 0x8e, 0x83, 0x65, 0x8e, 0xd5, 0x88, 0x81, 0x69, 0x19, 0x49, 0x6c, 0xec, 0xa7, 0xf6, - 0x6a, 0x43, 0x33, 0xa2, 0x90, 0xe9, 0x99, 0xb1, 0x45, 0x48, 0x75, 0x7e, 0x8d, 0x8f, 0x2e, 0x49, - 0xe5, 0xa3, 0x23, 0xdd, 0x7b, 0xfa, 0x96, 0x54, 0xae, 0xbc, 0x60, 0x24, 0x74, 0xed, 0xb0, 0x84, - 0x3b, 0x96, 0x36, 0x18, 0x24, 0x15, 0x61, 0xed, 0x3e, 0xc6, 0xc6, 0x5a, 0x12, 0x1b, 0x2f, 0x1f, - 0x54, 0x58, 0xae, 0xcb, 0xa4, 0xba, 0xa3, 0xdf, 0x48, 0x6d, 0x11, 0x4c, 0x0f, 0x48, 0x59, 0x7d, - 0xb1, 0x5b, 0xd5, 0xac, 0x6a, 0x12, 0x1b, 0x65, 0x25, 0xb3, 0x11, 0xa5, 0x26, 0xa9, 0x5f, 0x33, - 0x67, 0xd4, 0x03, 0x97, 0x83, 0x14, 0xd8, 0x8e, 0xaa, 0x56, 0x33, 0x89, 0x8d, 0x86, 0xcf, 0x9c, - 0xd1, 0x50, 0xa4, 0xb8, 0x9d, 0x0f, 0x52, 0x8e, 0x78, 0x7f, 0x4a, 0x4b, 0x47, 0xb5, 0xf5, 0x36, - 0xa2, 0x47, 0x3f, 0x91, 0x8a, 0xea, 0x1b, 0x82, 0xfe, 0x9f, 0x94, 0x7a, 0xbd, 0x4b, 0xcc, 0xdb, - 0xb0, 0x5e, 0x25, 0xb1, 0xb1, 0x29, 0xc4, 0x7d, 0x6e, 0xb2, 0x8a, 0x55, 0x41, 0xfd, 0xeb, 0x1e, - 0xe6, 0xcd, 0x82, 0xa4, 0x9f, 0x5f, 0x91, 0x62, 0x8f, 0xfe, 0xd0, 0x48, 0x53, 0x1d, 0x65, 0xf4, - 0xb5, 0xe1, 0xd7, 0x08, 0x84, 0xa4, 0x7b, 0xa4, 0x74, 0x05, 0xb3, 0xcc, 0x7e, 0x23, 0x89, 0x8d, - 0xd2, 0x03, 0xcc, 0x6c, 0x85, 0xd1, 0x63, 0x52, 0x55, 0xe1, 0x6a, 0x52, 0xe8, 0x5c, 0xb3, 0x1a, - 0x49, 0x6c, 0x54, 0xa3, 0x0c, 0xb3, 0x17, 0x2c, 0xfd, 0x9e, 0x94, 0xfa, 0xd2, 0x5f, 0xb4, 0xca, - 0xf4, 0xf9, 0xe8, 0xcc, 0x9f, 0x8f, 0xce, 0xbb, 0xec, 0xf9, 0xb0, 0x5e, 0x66, 0x15, 0x2f, 0x49, - 0xe9, 0x7f, 0xfe, 0xd3, 0xd0, 0x6c, 0x25, 0xa3, 0x67, 0x64, 0xf3, 0x9c, 0x4d, 0x42, 0x47, 0x7a, - 0x77, 0x9e, 0xaf, 0x5a, 0x6e, 0x19, 0x93, 0x61, 0xe3, 0x73, 0x91, 0x70, 0x52, 0x22, 0xb7, 0xa0, - 0xa2, 0xe2, 0xcd, 0x1b, 0x52, 0x5b, 0x3c, 0x1c, 0xb4, 0x4a, 0xca, 0xef, 0x6f, 0xde, 0xf7, 0x9b, - 0x6b, 0x74, 0x83, 0x94, 0x3e, 0xdc, 0xf6, 0x9b, 0x1a, 0x25, 0x64, 0xfd, 0xdd, 0xc5, 0xf5, 0x45, - 0xff, 0xa2, 0xf9, 0xc2, 0xfc, 0x57, 0x23, 0x75, 0xd5, 0x8e, 0x7b, 0xe9, 0x29, 0xa4, 0x6f, 0xc9, - 0x56, 0x0f, 0x82, 0xd1, 0x15, 0x40, 0x78, 0xe6, 0x7b, 0x53, 0x10, 0x34, 0x77, 0x37, 0x17, 0x68, - 0x6b, 0xe7, 0xd9, 0xb2, 0x2e, 0xd4, 0x4c, 0x8e, 0x35, 0xfa, 0x15, 0xa9, 0xe3, 0x39, 0xc1, 0x67, - 0x4d, 0xd0, 0x46, 0xfe, 0xa0, 0xb5, 0xe6, 0x23, 0x24, 0xbf, 0xd1, 0xe8, 0xb7, 0x84, 0xdc, 0x86, - 0x02, 0xb8, 0xbc, 0x61, 0x23, 0xa0, 0x5f, 0xb8, 0xba, 0xad, 0x2f, 0x65, 0xa7, 0xa7, 0xe4, 0xd5, - 0x0f, 0x10, 0xa8, 0x15, 0xc2, 0x62, 0x0b, 0xe9, 0x6e, 0xe6, 0xbd, 0xba, 0xa9, 0x8b, 0xa4, 0x08, - 0x5a, 0xdb, 0x8f, 0x7f, 0xb7, 0xd7, 0x1e, 0x9f, 0xda, 0xda, 0xef, 0x4f, 0x6d, 0xed, 0xaf, 0xa7, - 0xb6, 0xf6, 0xf9, 0x9f, 0xf6, 0xda, 0xdd, 0x3a, 0x86, 0x9c, 0xfc, 0x17, 0x00, 0x00, 0xff, 0xff, - 0xcc, 0xc1, 0x45, 0x61, 0x33, 0x08, 0x00, 0x00, +func init() { proto.RegisterFile("auth.proto", fileDescriptor_auth_edb58835f7ffeb60) } + +var fileDescriptor_auth_edb58835f7ffeb60 = []byte{ + // 1044 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x55, 0xd1, 0x4e, 0xe3, 0x46, + 0x17, 0xc6, 0x4b, 0xc2, 0x26, 0x93, 0x90, 0x3f, 0xcc, 0x22, 0xf0, 0x06, 0x36, 0x46, 0xfc, 0x37, + 0x68, 0x8b, 0x92, 0x2a, 0xa8, 0x52, 0xc5, 0xcd, 0x6a, 0x4d, 0xb3, 0x65, 0x05, 0xa2, 0xd4, 0x09, + 0x59, 0xa9, 0xbd, 0x88, 0x8c, 0x73, 0x08, 0x16, 0x8e, 0xc7, 0x9d, 0x19, 0x47, 0x8d, 0xfa, 0x12, + 0xbd, 0xec, 0x8b, 0xf4, 0x1d, 0xb8, 0xec, 0x13, 0xb8, 0x2d, 0xbd, 0xf3, 0x45, 0x9f, 0xa1, 0x9a, + 0x63, 0x27, 0xb1, 0xc3, 0x5e, 0xd9, 0xf3, 0x7d, 0xe7, 0xfb, 0xce, 0xcc, 0xf1, 0xf1, 0x19, 0x42, + 0xec, 0x50, 0xde, 0xb7, 0x02, 0xce, 0x24, 0xa3, 0x45, 0x7c, 0x34, 0xb6, 0xc7, 0x6c, 0xcc, 0xf0, + 0xb5, 0xad, 0xde, 0x12, 0xb2, 0xb1, 0x37, 0x66, 0x6c, 0xec, 0x41, 0x1b, 0x57, 0xb7, 0xe1, 0x5d, + 0x1b, 0x26, 0x81, 0x9c, 0xa5, 0xa4, 0xb1, 0x4a, 0x4a, 0x77, 0x02, 0x42, 0xda, 0x93, 0x20, 0x0d, + 0x38, 0x1d, 0xbb, 0xf2, 0x3e, 0xbc, 0x6d, 0x39, 0x6c, 0xd2, 0x1e, 0x73, 0x7b, 0xea, 0x4a, 0x5b, + 0xba, 0xcc, 0xb7, 0xbd, 0xb6, 0x04, 0x0f, 0x02, 0xc6, 0x65, 0xdb, 0x73, 0x6f, 0xdb, 0x02, 0xf8, + 0xd4, 0x75, 0x40, 0xb4, 0xe5, 0x2c, 0x00, 0x91, 0x68, 0x0f, 0xa3, 0x12, 0x29, 0x76, 0xa7, 0xe0, + 0x4b, 0xfa, 0x35, 0x29, 0xf4, 0x67, 0x01, 0xe8, 0xda, 0x81, 0x76, 0x54, 0xeb, 0xd4, 0x13, 0xbe, + 0xf5, 0x5d, 0x00, 0x1c, 0xdd, 0x4c, 0x1a, 0x47, 0x46, 0x4d, 0x49, 0x8f, 0xd9, 0xc4, 0x95, 0xb8, + 0x41, 0x0b, 0x15, 0xf4, 0x07, 0x52, 0xb3, 0x40, 0xb0, 0x90, 0x3b, 0x70, 0x0e, 0xf6, 0x08, 0xb8, + 0xfe, 0xe2, 0x40, 0x3b, 0xaa, 0x74, 0xf4, 0xd6, 0x3c, 0x65, 0x2b, 0xcf, 0x9b, 0x3b, 0x71, 0x64, + 0x50, 0x9e, 0x62, 0x4b, 0xbf, 0xf3, 0x35, 0x6b, 0xc5, 0x89, 0x0e, 0xc9, 0xe6, 0x19, 0x70, 0xf9, + 0x3e, 0x94, 0xf7, 0x8c, 0xbb, 0x72, 0xa6, 0xaf, 0xa3, 0xf5, 0xeb, 0xa5, 0x75, 0x8e, 0x1e, 0x74, + 0xcc, 0xfd, 0x38, 0x32, 0x74, 0x07, 0xb8, 0x1c, 0xda, 0x73, 0x34, 0x97, 0x21, 0xef, 0x47, 0x7f, + 0x24, 0xd5, 0x9e, 0xaa, 0x97, 0xd3, 0x67, 0x0f, 0xe0, 0x0b, 0xbd, 0xb0, 0xba, 0xf5, 0x2c, 0x3b, + 0xe8, 0x98, 0x7b, 0x71, 0x64, 0xec, 0x0a, 0xc4, 0x86, 0x12, 0xc1, 0x9c, 0x7b, 0xce, 0x8c, 0x3a, + 0xa4, 0x76, 0xcd, 0xd9, 0xd4, 0x15, 0x2e, 0xf3, 0x11, 0xd2, 0x8b, 0x68, 0xdf, 0x58, 0xda, 0xe7, + 0xf9, 0x41, 0xc7, 0x7c, 0x13, 0x47, 0xc6, 0xeb, 0x60, 0x8e, 0x26, 0x39, 0xf2, 0x25, 0xca, 0x4b, + 0xe8, 0x27, 0x52, 0x39, 0xf3, 0x42, 0x21, 0x81, 0x5f, 0xd9, 0x13, 0xd0, 0x37, 0x30, 0xc3, 0x6e, + 0xa6, 0x40, 0x4b, 0x72, 0xd0, 0x31, 0x1b, 0x71, 0x64, 0xec, 0x38, 0x09, 0x34, 0xf4, 0xed, 0x49, + 0xbe, 0xfc, 0x59, 0x27, 0xac, 0x7d, 0xb2, 0x3c, 0x63, 0xfe, 0x9d, 0x3b, 0xd6, 0x5f, 0x3e, 0xab, + 0x7d, 0x96, 0x1e, 0x9c, 0xa4, 0xb5, 0x4f, 0xcd, 0x1d, 0x44, 0x57, 0x6a, 0x9f, 0x15, 0xd0, 0x53, + 0x52, 0xb8, 0x11, 0xc0, 0xf5, 0x12, 0xfa, 0xd6, 0x97, 0xbe, 0x0a, 0x1d, 0x74, 0x92, 0x96, 0x0b, + 0x05, 0xf0, 0x9c, 0x09, 0x6a, 0x94, 0xd6, 0x62, 0x1e, 0xe8, 0xe5, 0x55, 0xad, 0x42, 0x07, 0x27, + 0x89, 0x96, 0x33, 0x2f, 0x7f, 0x3e, 0xd4, 0xd0, 0x4b, 0x52, 0x56, 0x07, 0x14, 0x81, 0xed, 0x80, + 0x4e, 0xd0, 0xe0, 0xd5, 0xd2, 0x60, 0x41, 0x99, 0xbb, 0x71, 0x64, 0xbc, 0xf2, 0xe7, 0xcb, 0x9c, + 0xd1, 0xd2, 0x80, 0x9a, 0x64, 0xa3, 0x07, 0x7c, 0x0a, 0x5c, 0xaf, 0xa0, 0x15, 0xcd, 0xf4, 0x0e, + 0xe2, 0x83, 0x8e, 0xb9, 0x1d, 0x47, 0x46, 0x5d, 0xe0, 0x2a, 0x67, 0x93, 0x2a, 0x55, 0xa9, 0x2d, + 0x98, 0x02, 0x17, 0xd0, 0x0f, 0x7d, 0x1f, 0x3c, 0xbd, 0xba, 0x5a, 0xea, 0x1c, 0x3d, 0x6f, 0x73, + 0x9e, 0x80, 0x43, 0x89, 0x68, 0xbe, 0xd4, 0x39, 0x01, 0x7d, 0x20, 0xf5, 0xe4, 0xed, 0x8c, 0xf9, + 0x3e, 0x38, 0xea, 0x8f, 0xd6, 0x37, 0x31, 0xc7, 0xfe, 0x32, 0xc7, 0x6a, 0xc4, 0xa0, 0x63, 0x1a, + 0x71, 0x64, 0xec, 0x25, 0xf6, 0xea, 0x83, 0xa6, 0x44, 0x2e, 0xd3, 0x33, 0x63, 0x93, 0x90, 0xd2, + 0xfc, 0x37, 0x3e, 0x3c, 0x27, 0xc5, 0x4f, 0xb6, 0x74, 0xee, 0xe9, 0x3b, 0x52, 0xbc, 0x70, 0xfd, + 0x91, 0xd0, 0xb5, 0x83, 0x75, 0xfc, 0x62, 0xc9, 0x80, 0x41, 0x52, 0x11, 0xe6, 0xee, 0x63, 0x64, + 0xac, 0xc5, 0x91, 0xf1, 0xbf, 0x07, 0x15, 0x96, 0x99, 0x32, 0x89, 0xee, 0xf0, 0x17, 0x52, 0x5e, + 0x04, 0xd3, 0x7d, 0x52, 0x50, 0x4f, 0x9c, 0x56, 0x65, 0xb3, 0x14, 0x47, 0x46, 0x41, 0xc9, 0x2c, + 0x44, 0x69, 0x87, 0x54, 0x2e, 0x99, 0x3d, 0xea, 0x81, 0xc3, 0x41, 0x0a, 0x1c, 0x47, 0x25, 0xb3, + 0x1e, 0x47, 0x46, 0xd5, 0x63, 0xf6, 0x68, 0x28, 0x12, 0xdc, 0xca, 0x06, 0x29, 0x47, 0xfc, 0x7f, + 0xd6, 0x97, 0x8e, 0xea, 0xd3, 0x5b, 0x88, 0x1e, 0x7e, 0x4f, 0x8a, 0x6a, 0x6e, 0x08, 0xfa, 0x7f, + 0xb2, 0xde, 0xeb, 0x9d, 0x63, 0xde, 0xaa, 0xb9, 0x15, 0x47, 0xc6, 0xa6, 0x10, 0xf7, 0x99, 0xcd, + 0x2a, 0x56, 0x05, 0xf5, 0x2f, 0x7b, 0x98, 0x37, 0x0d, 0x92, 0x5e, 0xf6, 0x44, 0x8a, 0x3d, 0xfc, + 0xfd, 0x05, 0xa9, 0xab, 0x56, 0x46, 0x5f, 0x0b, 0x7e, 0x0a, 0x41, 0x48, 0x7a, 0x4c, 0xca, 0xd7, + 0xe1, 0xad, 0xe7, 0x3a, 0x17, 0x30, 0x4b, 0x93, 0xd4, 0xe2, 0xc8, 0x20, 0x01, 0x82, 0xc3, 0x07, + 0x98, 0x59, 0xcb, 0x00, 0x7a, 0x44, 0x4a, 0xca, 0x41, 0xed, 0x13, 0x93, 0x95, 0xcd, 0x6a, 0x1c, + 0x19, 0xa5, 0x30, 0xc5, 0xac, 0x05, 0x4b, 0x7b, 0xe4, 0x65, 0xf7, 0xe7, 0xc0, 0xe5, 0x20, 0xd2, + 0x09, 0xda, 0x68, 0x25, 0xd7, 0x4a, 0x6b, 0x7e, 0xad, 0xb4, 0xfa, 0xf3, 0x6b, 0xc5, 0x7c, 0x93, + 0x7e, 0x89, 0x2d, 0x48, 0x24, 0xcb, 0x9d, 0xff, 0xfa, 0xa7, 0xa1, 0x59, 0x73, 0x27, 0x7a, 0x4c, + 0x36, 0x3e, 0x30, 0x3e, 0xb1, 0x25, 0x4e, 0xcd, 0x72, 0xd2, 0xe5, 0x77, 0x88, 0x64, 0x0e, 0x9b, + 0xc6, 0xd0, 0x0f, 0xa4, 0x66, 0xb1, 0x50, 0x42, 0x9f, 0xa5, 0x53, 0x00, 0x87, 0x61, 0xd9, 0x6c, + 0xc6, 0x91, 0xd1, 0xe0, 0x8a, 0x19, 0x4a, 0x36, 0x4c, 0xa7, 0x47, 0x46, 0xbf, 0xa2, 0x7a, 0xfb, + 0x96, 0x94, 0x17, 0xb7, 0x12, 0x2d, 0x91, 0xc2, 0xc7, 0xab, 0x8f, 0xfd, 0xfa, 0x1a, 0x7d, 0x49, + 0xd6, 0xaf, 0x6f, 0xfa, 0x75, 0x8d, 0x12, 0xb2, 0xf1, 0x4d, 0xf7, 0xb2, 0xdb, 0xef, 0xd6, 0x5f, + 0x74, 0xfe, 0xd5, 0x48, 0x45, 0xcd, 0xfa, 0x5e, 0xd2, 0xe2, 0xf4, 0x1d, 0xa9, 0xf5, 0xc0, 0x1f, + 0x5d, 0x00, 0x04, 0xef, 0x3d, 0x77, 0x0a, 0x82, 0x66, 0x7e, 0xfc, 0x05, 0xda, 0xd8, 0x79, 0x56, + 0x9c, 0xae, 0xda, 0xca, 0x91, 0x46, 0xbf, 0x20, 0x15, 0x6c, 0x42, 0xbc, 0x33, 0x05, 0xad, 0x66, + 0xbb, 0xb8, 0x31, 0x5f, 0x21, 0xf9, 0xa5, 0x46, 0xbf, 0x22, 0xe4, 0x26, 0x10, 0xc0, 0xe5, 0x15, + 0x1b, 0x01, 0xfd, 0xcc, 0x5c, 0x68, 0x7c, 0x2e, 0x3b, 0x3d, 0x25, 0x5b, 0xdf, 0x82, 0xaf, 0x4e, + 0x08, 0x8b, 0xfe, 0xa0, 0xbb, 0xa9, 0xf7, 0x6a, 0xc7, 0x2c, 0x92, 0x22, 0x68, 0x6e, 0x3f, 0xfe, + 0xdd, 0xd4, 0x1e, 0x9f, 0x9a, 0xda, 0x1f, 0x4f, 0x4d, 0xed, 0xaf, 0xa7, 0xa6, 0xf6, 0xdb, 0x3f, + 0xcd, 0xb5, 0xdb, 0x0d, 0x0c, 0x39, 0xf9, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x89, 0xec, 0xe3, 0x97, + 0x91, 0x08, 0x00, 0x00, } diff --git a/lib/auth/proto/auth.proto b/lib/auth/proto/auth.proto index b7508eeb9779c..af5c6be400d79 100644 --- a/lib/auth/proto/auth.proto +++ b/lib/auth/proto/auth.proto @@ -3,12 +3,12 @@ package proto; import "gogoproto/gogo.proto"; import "google/protobuf/empty.proto"; -import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; import "github.com/gravitational/teleport/lib/services/types.proto"; option (gogoproto.marshaler_all) = true; option (gogoproto.unmarshaler_all) = true; -option (gogoproto.goproto_getters_all) = false; +option (gogoproto.goproto_getters_all) = true; // Operation identifies type of operation enum Operation { @@ -83,16 +83,23 @@ message Certs { } // UserCertRequest specifies certificate-generation parameters -// for a user. Used by `tctl auth sign`. +// for a user. message UserCertsRequest { - // Key is a public key to be signed. - bytes Key = 1 [(gogoproto.jsontag) = "key"]; + // PublicKey is a public key to be signed. + bytes PublicKey = 1 [(gogoproto.jsontag) = "public_key"]; // Username of key owner. string Username = 2 [(gogoproto.jsontag) = "username"]; - // TTL to be applied to the certificates. - google.protobuf.Duration TTL = 3 [(gogoproto.stdduration) = true, (gogoproto.nullable) = false, (gogoproto.jsontag) = "ttl"]; - // Compatibility encodes the desired certificate format. - string Compatibility = 4 [(gogoproto.jsontag) = "compatability,omitempty"]; + // Expires is a desired time of the expiry of the certificate, could + // be adjusted based on the permissions + google.protobuf.Timestamp Expires = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false, (gogoproto.jsontag) = "expires,omitempty"]; + // Format encodes the desired SSH Certificate format (either old ssh compatibility + // format to remove some metadata causing trouble with old SSH servers) + // or standard SSH cert format with custom extensions + string Format = 4 [(gogoproto.jsontag) = "format,omitempty"]; + // RouteToCluster is an optional cluster name to add to the certificate, + // so that requests originating with this certificate will be redirected + // to this cluster + string RouteToCluster = 5 [(gogoproto.jsontag) = "route_to_cluster,omitempty"]; } // AuthService is authentication/authorization service implementation diff --git a/lib/auth/tls_test.go b/lib/auth/tls_test.go index 32052a9d735af..f0aff2d0d0a85 100644 --- a/lib/auth/tls_test.go +++ b/lib/auth/tls_test.go @@ -32,6 +32,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/auth/proto" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/events" @@ -1274,32 +1275,74 @@ func (s *TLSSuite) TestGenerateCerts(c *check.C) { nopClient, err := s.server.NewClient(TestNop()) c.Assert(err, check.IsNil) - _, err = nopClient.GenerateUserCerts(context.TODO(), pub, user1.GetName(), time.Hour, teleport.CertificateFormatStandard) + _, err = nopClient.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{ + PublicKey: pub, + Username: user1.GetName(), + Expires: time.Now().Add(time.Hour).UTC(), + Format: teleport.CertificateFormatStandard, + }) c.Assert(err, check.NotNil) fixtures.ExpectAccessDenied(c, err) c.Assert(err, check.ErrorMatches, "this request can be only executed by an admin") - // Users don't match - userClient2, err := s.server.NewClient(TestUser(user2.GetName())) + // User can't generate certificates for another user + testUser2 := TestUser(user2.GetName()) + testUser2.TTL = time.Hour + userClient2, err := s.server.NewClient(testUser2) c.Assert(err, check.IsNil) - _, err = userClient2.GenerateUserCerts(context.TODO(), pub, user1.GetName(), time.Hour, teleport.CertificateFormatStandard) + _, err = userClient2.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{ + PublicKey: pub, + Username: user1.GetName(), + Expires: time.Now().Add(time.Hour).UTC(), + Format: teleport.CertificateFormatStandard, + }) c.Assert(err, check.NotNil) fixtures.ExpectAccessDenied(c, err) c.Assert(err, check.ErrorMatches, "this request can be only executed by an admin") + // User can renew their certificates, however the TTL will be limited + // to the TTL of their session for both SSH and x509 certs and + // that route to cluster will be encoded in the cert metadata + userCerts, err := userClient2.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{ + PublicKey: pub, + Username: user2.GetName(), + Expires: time.Now().Add(100 * time.Hour).UTC(), + Format: teleport.CertificateFormatStandard, + RouteToCluster: "cluster1", + }) + c.Assert(err, check.IsNil) + + parseCert := func(sshCert []byte) (*ssh.Certificate, time.Duration) { + parsedKey, _, _, _, err := ssh.ParseAuthorizedKey(sshCert) + c.Assert(err, check.IsNil) + parsedCert, _ := parsedKey.(*ssh.Certificate) + validBefore := time.Unix(int64(parsedCert.ValidBefore), 0) + return parsedCert, validBefore.Sub(time.Now()) + } + _, diff := parseCert(userCerts.SSH) + c.Assert(diff < testUser2.TTL, check.Equals, true, check.Commentf("expected %v < %v", diff, testUser2.TTL)) + + tlsCert, err := tlsca.ParseCertificatePEM(userCerts.TLS) + c.Assert(err, check.IsNil) + identity, err := tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter) + c.Assert(err, check.IsNil) + c.Assert(identity.Expires.Before(time.Now().Add(testUser2.TTL)), check.Equals, true, check.Commentf("%v vs %v", identity.Expires, time.Now().UTC())) + c.Assert(identity.RouteToCluster, check.Equals, "cluster1") + // Admin should be allowed to generate certs with TTL longer than max. adminClient, err := s.server.NewClient(TestAdmin()) c.Assert(err, check.IsNil) - userCerts, err := adminClient.GenerateUserCerts(context.TODO(), pub, user1.GetName(), 40*time.Hour, teleport.CertificateFormatStandard) + userCerts, err = adminClient.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{ + PublicKey: pub, + Username: user1.GetName(), + Expires: time.Now().Add(40 * time.Hour).UTC(), + Format: teleport.CertificateFormatStandard, + }) c.Assert(err, check.IsNil) - parsedKey, _, _, _, err := ssh.ParseAuthorizedKey(userCerts.SSH) - c.Assert(err, check.IsNil) - parsedCert, _ := parsedKey.(*ssh.Certificate) - validBefore := time.Unix(int64(parsedCert.ValidBefore), 0) - diff := validBefore.Sub(time.Now()) + parsedCert, diff := parseCert(userCerts.SSH) c.Assert(diff > defaults.MaxCertDuration, check.Equals, true, check.Commentf("expected %v > %v", diff, defaults.CertDuration)) // user should have agent forwarding (default setting) @@ -1313,18 +1356,25 @@ func (s *TLSSuite) TestGenerateCerts(c *check.C) { err = s.server.Auth().UpsertRole(userRole) c.Assert(err, check.IsNil) - userCerts, err = adminClient.GenerateUserCerts(context.TODO(), pub, user1.GetName(), 1*time.Hour, teleport.CertificateFormatStandard) - c.Assert(err, check.IsNil) - parsedKey, _, _, _, err = ssh.ParseAuthorizedKey(userCerts.SSH) - c.Assert(err, check.IsNil) - parsedCert, _ = parsedKey.(*ssh.Certificate) + userCerts, err = adminClient.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{ + PublicKey: pub, + Username: user1.GetName(), + Expires: time.Now().Add(1 * time.Hour).UTC(), + Format: teleport.CertificateFormatStandard, + }) + parsedCert, _ = parseCert(userCerts.SSH) // user should get agent forwarding _, exists = parsedCert.Extensions[teleport.CertExtensionPermitAgentForwarding] c.Assert(exists, check.Equals, true) // apply HTTP Auth to generate user cert: - userCerts, err = adminClient.GenerateUserCerts(context.TODO(), pub, user1.GetName(), time.Hour, teleport.CertificateFormatStandard) + userCerts, err = adminClient.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{ + PublicKey: pub, + Username: user1.GetName(), + Expires: time.Now().Add(time.Hour).UTC(), + Format: teleport.CertificateFormatStandard, + }) c.Assert(err, check.IsNil) _, _, _, _, err = ssh.ParseAuthorizedKey(userCerts.SSH) diff --git a/lib/client/api.go b/lib/client/api.go index 7b18b1e113a9a..16baf71f9be50 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -813,6 +813,16 @@ func (tc *TeleportClient) getTargetNodes(ctx context.Context, proxy *ProxyClient return retval, nil } +// GenerateCertsForCluster generates certificates for the user +// that have a metadata instructing server to route the requests to the cluster +func (tc *TeleportClient) GenerateCertsForCluster(ctx context.Context, routeToCluster string) error { + proxyClient, err := tc.ConnectToProxy(ctx) + if err != nil { + return trace.Wrap(err) + } + return proxyClient.GenerateCertsForCluster(ctx, routeToCluster) +} + // SSH connects to a node and, if 'command' is specified, executes the command on it, // otherwise runs interactive shell // diff --git a/lib/client/client.go b/lib/client/client.go index 344a50f87b29d..05c3616bccd1f 100644 --- a/lib/client/client.go +++ b/lib/client/client.go @@ -33,11 +33,13 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/auth" + "github.com/gravitational/teleport/lib/auth/proto" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/sshutils/scp" + "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/socks" @@ -108,6 +110,52 @@ func (proxy *ProxyClient) GetSites() ([]services.Site, error) { return sites, nil } +// GenerateCertsForCluster generates certificates for the user +// that have a metadata instructing server to route the requests to the cluster +func (proxy *ProxyClient) GenerateCertsForCluster(ctx context.Context, routeToCluster string) error { + localAgent := proxy.teleportClient.LocalAgent() + key, err := localAgent.GetKey() + if err != nil { + return trace.Wrap(err) + } + cert, err := key.SSHCert() + if err != nil { + return trace.Wrap(err) + } + tlsCert, err := key.TLSCertificate() + if err != nil { + return trace.Wrap(err) + } + clusterName, err := tlsca.ClusterName(tlsCert.Issuer) + if err != nil { + return trace.Wrap(err) + } + clt, err := proxy.ConnectToCluster(ctx, clusterName, true) + if err != nil { + return trace.Wrap(err) + } + req := proto.UserCertsRequest{ + Username: cert.KeyId, + PublicKey: key.Pub, + Expires: time.Unix(int64(cert.ValidBefore), 0), + RouteToCluster: routeToCluster, + } + if _, ok := cert.Permissions.Extensions[teleport.CertExtensionTeleportRoles]; !ok { + req.Format = teleport.CertificateFormatOldSSH + } + + certs, err := clt.GenerateUserCerts(ctx, req) + if err != nil { + return trace.Wrap(err) + } + key.Cert = certs.SSH + key.TLSCert = certs.TLS + + // save the cert to the local storage (~/.tsh usually): + _, err = localAgent.AddKey(key) + return trace.Wrap(err) +} + // FindServersByLabels returns list of the nodes which have labels exactly matching // the given label set. // diff --git a/lib/client/interfaces.go b/lib/client/interfaces.go index b4853d836fddc..a5d416466421f 100644 --- a/lib/client/interfaces.go +++ b/lib/client/interfaces.go @@ -180,6 +180,11 @@ func (k *Key) EqualsTo(other *Key) bool { bytes.Equal(k.TLSCert, other.TLSCert) } +// TLSCertificate returns x509 certificate +func (k *Key) TLSCertificate() (*x509.Certificate, error) { + return tlsca.ParseCertificatePEM(k.TLSCert) +} + // TLSCertValidBefore returns the time of the TLS cert expiration func (k *Key) TLSCertValidBefore() (t time.Time, err error) { cert, err := tlsca.ParseCertificatePEM(k.TLSCert) @@ -220,21 +225,26 @@ func (k *Key) AsAuthMethod() (ssh.AuthMethod, error) { return NewAuthMethodForCert(signer), nil } -// CheckCert makes sure the SSH certificate is valid. -func (k *Key) CheckCert() error { +// SSHCert returns parsed SSH certificate +func (k *Key) SSHCert() (*ssh.Certificate, error) { key, _, _, _, err := ssh.ParseAuthorizedKey(k.Cert) if err != nil { - return trace.Wrap(err) + return nil, trace.Wrap(err) } cert, ok := key.(*ssh.Certificate) if !ok { - return trace.BadParameter("found key, not certificate") - } - if len(cert.ValidPrincipals) == 0 { - return trace.BadParameter("principals are required") + return nil, trace.BadParameter("found key, not certificate") } + return cert, nil +} +// CheckCert makes sure the SSH certificate is valid. +func (k *Key) CheckCert() error { + cert, err := k.SSHCert() + if err != nil { + return trace.Wrap(err) + } // A valid principal is always passed in because the principals are not being // checked here, but rather the validity period, signature, and algorithms. certChecker := utils.CertChecker{} diff --git a/lib/events/api.go b/lib/events/api.go index 25d340d8444ad..35eb773bbe7f1 100644 --- a/lib/events/api.go +++ b/lib/events/api.go @@ -112,6 +112,8 @@ const ( LoginMethod = "method" // LoginMethodLocal represents login with username/password LoginMethodLocal = "local" + // LoginMethodClientCert represents login with client certificate + LoginMethodClientCert = "client.cert" // LoginMethodOIDC represents login with OIDC LoginMethodOIDC = "oidc" // LoginMethodSAML represents login with SAML diff --git a/lib/kube/client/kubeclient.go b/lib/kube/client/kubeclient.go index 9376e8000300f..261551a61a057 100644 --- a/lib/kube/client/kubeclient.go +++ b/lib/kube/client/kubeclient.go @@ -8,7 +8,6 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/client" - kubeutils "github.com/gravitational/teleport/lib/kube/utils" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/trace" @@ -30,14 +29,9 @@ func UpdateKubeconfig(tc *client.TeleportClient) error { } clusterName, proxyPort := tc.KubeProxyHostPort() - var clusterAddr string + clusterAddr := fmt.Sprintf("https://%v:%v", clusterName, proxyPort) if tc.SiteName != "" { - // In case of a remote cluster, use SNI subdomain to "point" to a remote cluster name - clusterAddr = fmt.Sprintf("https://%v.%v:%v", - kubeutils.EncodeClusterName(tc.SiteName), clusterName, proxyPort) clusterName = tc.SiteName - } else { - clusterAddr = fmt.Sprintf("https://%v:%v", clusterName, proxyPort) } creds, err := tc.LocalAgent().GetKey() @@ -54,7 +48,7 @@ func UpdateKubeconfig(tc *client.TeleportClient) error { ClientKeyData: creds.Priv, } config.Clusters[clusterName] = &clientcmdapi.Cluster{ - Server: clusterAddr, + Server: clusterAddr, CertificateAuthorityData: certAuthorities, } diff --git a/lib/kube/proxy/forwarder.go b/lib/kube/proxy/forwarder.go index 1a2153490044c..0c60b189c3b1c 100644 --- a/lib/kube/proxy/forwarder.go +++ b/lib/kube/proxy/forwarder.go @@ -373,20 +373,31 @@ func (f *Forwarder) setupContext(ctx auth.AuthContext, req *http.Request, isRemo if err != nil { return nil, trace.Wrap(err) } - for _, remoteCluster := range f.Tunnel.GetSites() { - encodedName := kubeutils.EncodeClusterName(remoteCluster.GetName()) - if strings.HasPrefix(req.Host, remoteCluster.GetName()+".") || strings.HasPrefix(req.Host, encodedName+".") { - f.Debugf("Going to proxy to cluster: %v based on matching host prefix %v.", remoteCluster.GetName(), req.Host) - targetCluster = remoteCluster - isRemoteCluster = remoteCluster.GetName() != f.ClusterName - break + if ctx.Identity.RouteToCluster != "" { + f.Debugf("Client certificate of %v has requested routing to a cluster: %v.", ctx.User.GetName(), ctx.Identity.RouteToCluster) + targetCluster, err = f.Tunnel.GetSite(ctx.Identity.RouteToCluster) + if err != nil { + return nil, trace.Wrap(err) } - if f.ClusterOverride != "" && f.ClusterOverride == remoteCluster.GetName() { - f.Debugf("Going to proxy to cluster: %v based on override %v.", remoteCluster.GetName(), f.ClusterOverride) - targetCluster = remoteCluster - isRemoteCluster = remoteCluster.GetName() != f.ClusterName - f.Debugf("Override isRemoteCluster: %v %v %v", isRemoteCluster, remoteCluster.GetName(), f.ClusterName) - break + } else { + // DELETE IN(4.3.0) + // This logic is deprecated and after the second upgrade, will not be used + // by the newer post 4.2.0 clients, so will be safe to remove + for _, remoteCluster := range f.Tunnel.GetSites() { + encodedName := kubeutils.EncodeClusterName(remoteCluster.GetName()) + if strings.HasPrefix(req.Host, remoteCluster.GetName()+".") || strings.HasPrefix(req.Host, encodedName+".") { + f.Debugf("Going to proxy to cluster: %v based on matching host prefix %v.", remoteCluster.GetName(), req.Host) + targetCluster = remoteCluster + isRemoteCluster = remoteCluster.GetName() != f.ClusterName + break + } + if f.ClusterOverride != "" && f.ClusterOverride == remoteCluster.GetName() { + f.Debugf("Going to proxy to cluster: %v based on override %v.", remoteCluster.GetName(), f.ClusterOverride) + targetCluster = remoteCluster + isRemoteCluster = remoteCluster.GetName() != f.ClusterName + f.Debugf("Override isRemoteCluster: %v %v %v", isRemoteCluster, remoteCluster.GetName(), f.ClusterName) + break + } } } if targetCluster.GetName() != f.ClusterName && isRemoteUser { diff --git a/lib/tlsca/ca.go b/lib/tlsca/ca.go index 81b07e69d5144..82f3dca4d84f4 100644 --- a/lib/tlsca/ca.go +++ b/lib/tlsca/ca.go @@ -76,6 +76,11 @@ type Identity struct { Principals []string // KubernetesGroups is a list of Kubernetes groups allowed KubernetesGroups []string + // Expires specifies whenever the session will expire + Expires time.Time + // RouteToCluster specifies the target cluster + // if present in the session + RouteToCluster string } // CheckAndSetDefaults checks and sets default values @@ -98,17 +103,22 @@ func (id *Identity) Subject() pkix.Name { subject.OrganizationalUnit = append([]string{}, id.Usage...) subject.Locality = append([]string{}, id.Principals...) subject.Province = append([]string{}, id.KubernetesGroups...) + subject.StreetAddress = []string{id.RouteToCluster} return subject } // FromSubject returns identity from subject name -func FromSubject(subject pkix.Name) (*Identity, error) { +func FromSubject(subject pkix.Name, expires time.Time) (*Identity, error) { i := &Identity{ Username: subject.CommonName, Groups: subject.Organization, Usage: subject.OrganizationalUnit, Principals: subject.Locality, KubernetesGroups: subject.Province, + Expires: expires, + } + if len(subject.StreetAddress) > 0 { + i.RouteToCluster = subject.StreetAddress[0] } if err := i.CheckAndSetDefaults(); err != nil { return nil, trace.Wrap(err) @@ -179,7 +189,7 @@ func (ca *CertAuthority) GenerateCertificate(req CertificateRequest) ([]byte, er ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, // BasicConstraintsValid is true to not allow any intermediate certs. BasicConstraintsValid: true, - IsCA: false, + IsCA: false, } // sort out principals into DNS names and IP addresses diff --git a/tool/tctl/common/auth_command.go b/tool/tctl/common/auth_command.go index 90352caec4629..ed97b36583676 100644 --- a/tool/tctl/common/auth_command.go +++ b/tool/tctl/common/auth_command.go @@ -11,6 +11,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/native" + "github.com/gravitational/teleport/lib/auth/proto" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service" @@ -334,7 +335,12 @@ func (a *AuthCommand) generateUserKeys(clusterApi auth.ClientI) error { } // Request signed certs from `auth` server. - certs, err := clusterApi.GenerateUserCerts(context.TODO(), key.Pub, a.genUser, a.genTTL, certificateFormat) + certs, err := clusterApi.GenerateUserCerts(context.TODO(), proto.UserCertsRequest{ + PublicKey: key.Pub, + Username: a.genUser, + Expires: time.Now().UTC().Add(a.genTTL), + Format: certificateFormat, + }) if err != nil { return trace.Wrap(err) } diff --git a/tool/tsh/tsh.go b/tool/tsh/tsh.go index 95ad64c774951..b793849b541c9 100644 --- a/tool/tsh/tsh.go +++ b/tool/tsh/tsh.go @@ -407,6 +407,9 @@ func onLogin(cf *CLIConf) { // but cluster is specified, treat this as selecting a new cluster // for the same proxy case (cf.Proxy == "" || host(cf.Proxy) == host(profile.ProxyURL.Host)) && cf.SiteName != "": + if err := tc.GenerateCertsForCluster(cf.Context, cf.SiteName); err != nil { + utils.FatalError(err) + } tc.SaveProfile("", "") if err := kubeclient.UpdateKubeconfig(tc); err != nil { utils.FatalError(err)