From c1153734b0a73dd518e489443953dbba5525a038 Mon Sep 17 00:00:00 2001 From: Sasha Klizhentas Date: Mon, 8 Jan 2018 20:28:53 -0800 Subject: [PATCH] Add support for extra principals, fixes #1174 Add support for extra principals for proxy. Proxy section already supports public_addr property that is used during tctl users add output. Use the value from this property to update host SSH certificate for proxy service. proxy_service: public_addr: example.com:3024 With the configuration above, proxy host certificate will contain example.com principal in the SSH principals list. --- lib/auth/apiserver.go | 26 +----- lib/auth/auth.go | 112 ++++++++++++++++++------ lib/auth/auth_test.go | 56 ++++++++++-- lib/auth/auth_with_roles.go | 16 ++-- lib/auth/clt.go | 30 +++---- lib/auth/helpers.go | 12 ++- lib/auth/init.go | 17 +++- lib/auth/register.go | 29 ++++-- lib/auth/testauthority/testauthority.go | 1 + lib/auth/tls_test.go | 25 ++++-- lib/service/service.go | 40 +++++++-- lib/srv/regular/sshserver_test.go | 6 +- lib/utils/utils.go | 25 ++++++ lib/web/apiserver_test.go | 6 +- 14 files changed, 293 insertions(+), 108 deletions(-) diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index 6e4a774d943..895d8647ee5 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -892,24 +892,15 @@ func (s *APIServer) generateToken(auth ClientI, w http.ResponseWriter, r *http.R return string(token), nil } -type registerUsingTokenReq struct { - HostID string `json:"hostID"` - NodeName string `json:"node_name"` - Role teleport.Role `json:"role"` - Token string `json:"token"` -} - func (s *APIServer) registerUsingToken(auth ClientI, w http.ResponseWriter, r *http.Request, _ httprouter.Params, version string) (interface{}, error) { - var req *registerUsingTokenReq + var req RegisterUsingTokenRequest if err := httplib.ReadJSON(r, &req); err != nil { return nil, trace.Wrap(err) } - - keys, err := auth.RegisterUsingToken(req.Token, req.HostID, req.NodeName, req.Role) + keys, err := auth.RegisterUsingToken(req) if err != nil { return nil, trace.Wrap(err) } - return keys, nil } @@ -929,22 +920,13 @@ func (s *APIServer) registerNewAuthServer(auth ClientI, w http.ResponseWriter, r return message("ok"), nil } -type generateServerKeysReq struct { - // HostID is unique ID of the host - HostID string `json:"host_id"` - // NodeName is user friendly host name - NodeName string `json:"node_name"` - // Roles is a list of roles assigned to node - Roles teleport.Roles `json:"roles"` -} - func (s *APIServer) generateServerKeys(auth ClientI, w http.ResponseWriter, r *http.Request, _ httprouter.Params, version string) (interface{}, error) { - var req *generateServerKeysReq + var req GenerateServerKeysRequest if err := httplib.ReadJSON(r, &req); err != nil { return nil, trace.Wrap(err) } - keys, err := auth.GenerateServerKeys(req.HostID, req.NodeName, req.Roles) + keys, err := auth.GenerateServerKeys(req) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 87fd16098a8..e6e1165b1b1 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -597,9 +597,36 @@ func HostFQDN(hostUUID, clusterName string) string { return fmt.Sprintf("%v.%v", hostUUID, clusterName) } +// GenerateServerKeysRequest is a request to generate server keys +type GenerateServerKeysRequest struct { + // HostID is a unique ID of the host + HostID string `json:"host_id"` + // NodeName is a user friendly host name + NodeName string `json:"node_name"` + // Roles is a list of roles assigned to node + Roles teleport.Roles `json:"roles"` + // AdditionalPrincipals is a list of additional principals + // to include in OpenSSH and X509 certificates + AdditionalPrincipals []string `json:"additional_principals"` +} + +// CheckAndSetDefaults checks and sets default values +func (req *GenerateServerKeysRequest) CheckAndSetDefaults() error { + if req.HostID == "" { + return trace.BadParameter("missing parameter HostID") + } + if len(req.Roles) != 1 { + return trace.BadParameter("expected only one system role, got %v", len(req.Roles)) + } + return nil +} + // GenerateServerKeys generates new host private keys and certificates (signed // by the host certificate authority) for a node. -func (s *AuthServer) GenerateServerKeys(hostID string, nodeName string, roles teleport.Roles) (*PackedKeys, error) { +func (s *AuthServer) GenerateServerKeys(req GenerateServerKeysRequest) (*PackedKeys, error) { + if err := req.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } clusterName, err := s.GetDomainName() if err != nil { return nil, trace.Wrap(err) @@ -636,33 +663,36 @@ func (s *AuthServer) GenerateServerKeys(hostID string, nodeName string, roles te if err != nil { return nil, trace.Wrap(err) } - // generate hostSSH certificate hostSSHCert, err := s.Authority.GenerateHostCert(services.HostCertParams{ PrivateCASigningKey: caPrivateKey, PublicHostKey: pubSSHKey, - HostID: hostID, - NodeName: nodeName, + HostID: req.HostID, + NodeName: req.NodeName, ClusterName: clusterName, - Roles: roles, + Roles: req.Roles, + Principals: append([]string{}, req.AdditionalPrincipals...), }) - + if err != nil { + return nil, trace.Wrap(err) + } // generate host TLS certificate identity := tlsca.Identity{ - Username: HostFQDN(hostID, clusterName), - Groups: roles.StringSlice(), + Username: HostFQDN(req.HostID, clusterName), + Groups: req.Roles.StringSlice(), } certRequest := tlsca.CertificateRequest{ Clock: s.clock, PublicKey: cryptoPubKey, Subject: identity.Subject(), NotAfter: s.clock.Now().UTC().Add(defaults.CATTL), + DNSNames: append([]string{}, req.AdditionalPrincipals...), } // HTTPS requests need to specify DNS name that should be present in the // certificate as one of the DNS Names. It is not known in advance, // that is why there is a default one for all certificates - if roles.Include(teleport.RoleAuth) || roles.Include(teleport.RoleAdmin) { - certRequest.DNSNames = []string{teleport.APIDomain} + if req.Roles.Include(teleport.RoleAuth) || req.Roles.Include(teleport.RoleAdmin) { + certRequest.DNSNames = append(certRequest.DNSNames, teleport.APIDomain) } hostTLSCert, err := tlsAuthority.GenerateCertificate(certRequest) if err != nil { @@ -720,6 +750,35 @@ func (s *AuthServer) checkTokenTTL(token string) bool { return true } +// RegisterUsingTokenRequest is a request to register with +// auth server using authentication token +type RegisterUsingTokenRequest struct { + // HostID is a unique host ID, usually a UUID + HostID string `json:"hostID"` + // NodeName is a node name + NodeName string `json:"node_name"` + // Role is a system role, e.g. Proxy + Role teleport.Role `json:"role"` + // Token is an authentication token + Token string `json:"token"` + // AdditionalPrincipals is a list of additional principals + AdditionalPrincipals []string `json:"additional_principals"` +} + +// CheckAndSetDefaults checks for errors and sets defaults +func (r *RegisterUsingTokenRequest) CheckAndSetDefaults() error { + if r.HostID == "" { + return trace.BadParameter("missing parameter HostID") + } + if r.Token == "" { + return trace.BadParameter("missing parameter Token") + } + if err := r.Role.Check(); err != nil { + return trace.Wrap(err) + } + return nil +} + // RegisterUsingToken adds a new node to the Teleport cluster using previously issued token. // A node must also request a specific role (and the role must match one of the roles // the token was generated for). @@ -727,40 +786,41 @@ func (s *AuthServer) checkTokenTTL(token string) bool { // If a token was generated with a TTL, it gets enforced (can't register new nodes after TTL expires) // If a token was generated with a TTL=0, it means it's a single-use token and it gets destroyed // after a successful registration. -func (s *AuthServer) RegisterUsingToken(token, hostID string, nodeName string, role teleport.Role) (*PackedKeys, error) { - log.Infof("Node %q [%v] is trying to join with role: %v.", nodeName, hostID, role) - if hostID == "" { - return nil, trace.BadParameter("HostID cannot be empty") - } - - if err := role.Check(); err != nil { +func (s *AuthServer) RegisterUsingToken(req RegisterUsingTokenRequest) (*PackedKeys, error) { + log.Infof("Node %q [%v] is trying to join with role: %v.", req.NodeName, req.HostID, req.Role) + if err := req.CheckAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } // make sure the token is valid - roles, err := s.ValidateToken(token) + roles, err := s.ValidateToken(req.Token) if err != nil { - msg := fmt.Sprintf("%q [%v] can not join the cluster with role %s, token error: %v", nodeName, hostID, role, err) + msg := fmt.Sprintf("%q [%v] can not join the cluster with role %s, token error: %v", req.NodeName, req.HostID, req.Role, err) log.Warn(msg) return nil, trace.AccessDenied(msg) } - // make sure the caller is requested wthe role allowed by the token - if !roles.Include(role) { - msg := fmt.Sprintf("node %q [%v] can not join the cluster, the token does not allow %q role", nodeName, hostID, role) + // make sure the caller is requested the role allowed by the token + if !roles.Include(req.Role) { + msg := fmt.Sprintf("node %q [%v] can not join the cluster, the token does not allow %q role", req.NodeName, req.HostID, req.Role) log.Warn(msg) return nil, trace.BadParameter(msg) } - if !s.checkTokenTTL(token) { - return nil, trace.AccessDenied("node %q [%v] can not join the cluster, token has expired", nodeName, hostID) + if !s.checkTokenTTL(req.Token) { + return nil, trace.AccessDenied("node %q [%v] can not join the cluster, token has expired", req.NodeName, req.HostID) } // generate and return host certificate and keys - keys, err := s.GenerateServerKeys(hostID, nodeName, teleport.Roles{role}) + keys, err := s.GenerateServerKeys(GenerateServerKeysRequest{ + HostID: req.HostID, + NodeName: req.NodeName, + Roles: teleport.Roles{req.Role}, + AdditionalPrincipals: req.AdditionalPrincipals, + }) if err != nil { return nil, trace.Wrap(err) } - log.Infof("Node %q [%v] has joined the cluster.", nodeName, hostID) + log.Infof("Node %q [%v] has joined the cluster.", req.NodeName, req.HostID) return keys, nil } diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index 2d2832be0f0..2605e7e4eb1 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -22,6 +22,8 @@ import ( "testing" "time" + "golang.org/x/crypto/ssh" + "github.com/gravitational/teleport" authority "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/backend" @@ -31,10 +33,9 @@ import ( "github.com/gravitational/teleport/lib/services/suite" "github.com/gravitational/teleport/lib/utils" - "github.com/gravitational/trace" - "github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/oidc" + "github.com/gravitational/trace" "github.com/jonboulle/clockwork" . "gopkg.in/check.v1" ) @@ -183,7 +184,12 @@ func (s *AuthSuite) TestTokensCRUD(c *C) { c.Assert(roles.Include(teleport.RoleProxy), Equals, false) // unsuccessful registration (wrong role) - keys, err := s.a.RegisterUsingToken(tok, "bad-host-id", "bad-node-name", teleport.RoleProxy) + keys, err := s.a.RegisterUsingToken(RegisterUsingTokenRequest{ + Token: tok, + HostID: "bad-host-id", + NodeName: "bad-node-name", + Role: teleport.RoleProxy, + }) c.Assert(keys, IsNil) c.Assert(err, NotNil) c.Assert(err, ErrorMatches, `node "bad-node-name" \[bad-host-id\] can not join the cluster, the token does not allow "Proxy" role`) @@ -198,14 +204,38 @@ func (s *AuthSuite) TestTokensCRUD(c *C) { c.Assert(err, IsNil) // use it twice: - _, err = s.a.RegisterUsingToken(multiUseToken, "once", "node-name", teleport.RoleProxy) + keys, err = s.a.RegisterUsingToken(RegisterUsingTokenRequest{ + Token: multiUseToken, + HostID: "once", + NodeName: "node-name", + Role: teleport.RoleProxy, + AdditionalPrincipals: []string{"example.com"}, + }) c.Assert(err, IsNil) - _, err = s.a.RegisterUsingToken(multiUseToken, "twice", "node-name", teleport.RoleProxy) + + // along the way, make sure that additional principals work + key, _, _, _, err := ssh.ParseAuthorizedKey(keys.Cert) + c.Assert(err, IsNil) + hostCert := key.(*ssh.Certificate) + comment := Commentf("can't find example.com in %v", hostCert.ValidPrincipals) + c.Assert(utils.SliceContainsStr(hostCert.ValidPrincipals, "example.com"), Equals, true, comment) + + _, err = s.a.RegisterUsingToken(RegisterUsingTokenRequest{ + Token: multiUseToken, + HostID: "twice", + NodeName: "node-name", + Role: teleport.RoleProxy, + }) c.Assert(err, IsNil) // try to use after TTL: s.a.clock = clockwork.NewFakeClockAt(time.Now().UTC().Add(time.Hour + 1)) - _, err = s.a.RegisterUsingToken(multiUseToken, "late.bird", "node-name", teleport.RoleProxy) + _, err = s.a.RegisterUsingToken(RegisterUsingTokenRequest{ + Token: multiUseToken, + HostID: "late.bird", + NodeName: "node-name", + Role: teleport.RoleProxy, + }) c.Assert(err, ErrorMatches, `node "node-name" \[late.bird\] can not join the cluster, token has expired`) // expired token should be gone now @@ -220,9 +250,19 @@ func (s *AuthSuite) TestTokensCRUD(c *C) { c.Assert(err, IsNil) err = s.a.SetStaticTokens(st) c.Assert(err, IsNil) - _, err = s.a.RegisterUsingToken("static-token-value", "static.host", "node-name", teleport.RoleProxy) + _, err = s.a.RegisterUsingToken(RegisterUsingTokenRequest{ + Token: "static-token-value", + HostID: "static.host", + NodeName: "node-name", + Role: teleport.RoleProxy, + }) c.Assert(err, IsNil) - _, err = s.a.RegisterUsingToken("static-token-value", "wrong.role", "node-name", teleport.RoleAuth) + _, err = s.a.RegisterUsingToken(RegisterUsingTokenRequest{ + Token: "static-token-value", + HostID: "wrong.role", + NodeName: "node-name", + Role: teleport.RoleAuth, + }) c.Assert(err, NotNil) r, err := s.a.ValidateToken("static-token-value") c.Assert(err, IsNil) diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index ebe09f4f59d..2c0739e19bd 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -214,9 +214,9 @@ func (a *AuthWithRoles) GenerateToken(roles teleport.Roles, ttl time.Duration) ( return a.authServer.GenerateToken(roles, ttl) } -func (a *AuthWithRoles) RegisterUsingToken(token, hostID string, nodeName string, role teleport.Role) (*PackedKeys, error) { +func (a *AuthWithRoles) RegisterUsingToken(req RegisterUsingTokenRequest) (*PackedKeys, error) { // tokens have authz mechanism on their own, no need to check - return a.authServer.RegisterUsingToken(token, hostID, nodeName, role) + return a.authServer.RegisterUsingToken(req) } func (a *AuthWithRoles) RegisterNewAuthServer(token string) error { @@ -226,24 +226,24 @@ func (a *AuthWithRoles) RegisterNewAuthServer(token string) error { // GenerateServerKeys generates new host private keys and certificates (signed // by the host certificate authority) for a node. -func (a *AuthWithRoles) GenerateServerKeys(hostID string, nodeName string, roles teleport.Roles) (*PackedKeys, error) { +func (a *AuthWithRoles) GenerateServerKeys(req GenerateServerKeysRequest) (*PackedKeys, error) { clusterName, err := a.authServer.GetDomainName() if err != nil { return nil, trace.Wrap(err) } // username is hostID + cluster name, so make sure server requests new keys for itself - if a.user.GetName() != HostFQDN(hostID, clusterName) { - return nil, trace.AccessDenied("username mismatch %q and %q", a.user.GetName(), HostFQDN(hostID, clusterName)) + if a.user.GetName() != HostFQDN(req.HostID, clusterName) { + return nil, trace.AccessDenied("username mismatch %q and %q", a.user.GetName(), HostFQDN(req.HostID, clusterName)) } existingRoles, err := teleport.NewRoles(a.user.GetRoles()) if err != nil { return nil, trace.Wrap(err) } // prohibit privilege escalations through role changes - if !existingRoles.Equals(roles) { - return nil, trace.AccessDenied("roles do not match: %v and %v", existingRoles, roles) + if !existingRoles.Equals(req.Roles) { + return nil, trace.AccessDenied("roles do not match: %v and %v", existingRoles, req.Roles) } - return a.authServer.GenerateServerKeys(hostID, nodeName, roles) + return a.authServer.GenerateServerKeys(req) } func (a *AuthWithRoles) UpsertNode(s services.Server) error { diff --git a/lib/auth/clt.go b/lib/auth/clt.go index cf7f5187949..ab607b0f515 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -424,34 +424,28 @@ func (c *Client) GenerateToken(roles teleport.Roles, ttl time.Duration) (string, // RegisterUsingToken calls the auth service API to register a new node using a registration token // which was previously issued via GenerateToken. -func (c *Client) RegisterUsingToken(token, hostID string, nodeName string, role teleport.Role) (*PackedKeys, error) { - out, err := c.PostJSON(c.Endpoint("tokens", "register"), - registerUsingTokenReq{ - HostID: hostID, - NodeName: nodeName, - Token: token, - Role: role, - }) +func (c *Client) RegisterUsingToken(req RegisterUsingTokenRequest) (*PackedKeys, error) { + if err := req.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + out, err := c.PostJSON(c.Endpoint("tokens", "register"), req) if err != nil { return nil, trace.Wrap(err) } - var keys PackedKeys if err := json.Unmarshal(out.Bytes(), &keys); err != nil { return nil, trace.Wrap(err) } - return &keys, nil } // RenewCredentials returns a new set of credentials associated // with the server with the same privileges -func (c *Client) GenerateServerKeys(hostID string, nodeName string, roles teleport.Roles) (*PackedKeys, error) { - out, err := c.PostJSON(c.Endpoint("server", "credentials"), generateServerKeysReq{ - HostID: hostID, - NodeName: nodeName, - Roles: roles, - }) +func (c *Client) GenerateServerKeys(req GenerateServerKeysRequest) (*PackedKeys, error) { + if err := req.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + out, err := c.PostJSON(c.Endpoint("server", "credentials"), req) if err != nil { return nil, trace.Wrap(err) } @@ -2243,7 +2237,7 @@ type ProvisioningService interface { // RegisterUsingToken calls the auth service API to register a new node via registration token // which has been previously issued via GenerateToken - RegisterUsingToken(token, hostID string, nodeName string, role teleport.Role) (*PackedKeys, error) + RegisterUsingToken(req RegisterUsingTokenRequest) (*PackedKeys, error) // RegisterNewAuthServer is used to register new auth server with token RegisterNewAuthServer(token string) error @@ -2265,7 +2259,7 @@ type ClientI interface { GetDomainName() (string, error) // GenerateServerKeys generates new host private keys and certificates (signed // by the host certificate authority) for a node - GenerateServerKeys(hostID string, nodeName string, roles teleport.Roles) (*PackedKeys, error) + GenerateServerKeys(GenerateServerKeysRequest) (*PackedKeys, error) // DELETE IN: 2.6.0 // AccessPointDialer is no longer used for communication with auth server // GetDialer returns dialer that will connect to auth server API diff --git a/lib/auth/helpers.go b/lib/auth/helpers.go index 91df6cf5c9d..4e085c1b24f 100644 --- a/lib/auth/helpers.go +++ b/lib/auth/helpers.go @@ -216,7 +216,11 @@ func (a *TestAuthServer) NewCertificate(identity TestIdentity) (*tls.Certificate } return &cert, nil case BuiltinRole: - keys, err := a.AuthServer.GenerateServerKeys(id.Username, id.Username, teleport.Roles{id.Role}) + keys, err := a.AuthServer.GenerateServerKeys(GenerateServerKeysRequest{ + HostID: id.Username, + NodeName: id.Username, + Roles: teleport.Roles{id.Role}, + }) if err != nil { return nil, trace.Wrap(err) } @@ -518,7 +522,11 @@ func (t *TestTLSServer) Stop() error { // NewServerIdentity generates new server identity, used in tests func NewServerIdentity(clt *AuthServer, hostID string, role teleport.Role) (*Identity, error) { - keys, err := clt.GenerateServerKeys(hostID, hostID, teleport.Roles{teleport.RoleAuth}) + keys, err := clt.GenerateServerKeys(GenerateServerKeysRequest{ + HostID: hostID, + NodeName: hostID, + Roles: teleport.Roles{teleport.RoleAuth}, + }) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/init.go b/lib/auth/init.go index ced48ee775b..2908df77697 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -543,7 +543,11 @@ func initKeys(a *AuthServer, dataDir string, id IdentityID) (*Identity, error) { } if !keyExists || !sshCertExists || !tlsCertExists { - packedKeys, err := a.GenerateServerKeys(id.HostUUID, id.NodeName, teleport.Roles{id.Role}) + packedKeys, err := a.GenerateServerKeys(GenerateServerKeysRequest{ + HostID: id.HostUUID, + NodeName: id.NodeName, + Roles: teleport.Roles{id.Role}, + }) if err != nil { return nil, trace.Wrap(err) } @@ -610,6 +614,17 @@ func (i *Identity) HasTLSConfig() bool { return len(i.TLSCACertBytes) != 0 && len(i.TLSCertBytes) != 0 && len(i.TLSCACertBytes) != 0 } +// HasPrincipals returns whether identity has principals +func (i *Identity) HasPrincipals(additionalPrincipals []string) bool { + set := utils.StringsSet(additionalPrincipals) + for _, principal := range i.Cert.ValidPrincipals { + if _, ok := set[principal]; !ok { + return false + } + } + return true +} + // TLSConfig returns TLS config for mutual TLS authentication // can return NotFound error if there are no TLS credentials setup for identity func (i *Identity) TLSConfig() (*tls.Config, error) { diff --git a/lib/auth/register.go b/lib/auth/register.go index ceb5eb0f979..0fd12a33dde 100644 --- a/lib/auth/register.go +++ b/lib/auth/register.go @@ -33,8 +33,13 @@ import ( // LocalRegister is used to generate host keys when a node or proxy is running within the same process // as the auth server. This method does not need to use provisioning tokens. -func LocalRegister(dataDir string, id IdentityID, authServer *AuthServer) error { - keys, err := authServer.GenerateServerKeys(id.HostUUID, id.NodeName, teleport.Roles{id.Role}) +func LocalRegister(dataDir string, id IdentityID, authServer *AuthServer, additionalPrincipals []string) error { + keys, err := authServer.GenerateServerKeys(GenerateServerKeysRequest{ + HostID: id.HostUUID, + NodeName: id.NodeName, + Roles: teleport.Roles{id.Role}, + AdditionalPrincipals: additionalPrincipals, + }) if err != nil { return trace.Wrap(err) } @@ -44,7 +49,7 @@ func LocalRegister(dataDir string, id IdentityID, authServer *AuthServer) error // Register is used to generate host keys when a node or proxy are running on different hosts // than the auth server. This method requires provisioning tokens to prove a valid auth server // was used to issue the joining request. -func Register(dataDir, token string, id IdentityID, servers []utils.NetAddr) error { +func Register(dataDir, token string, id IdentityID, servers []utils.NetAddr, additionalPrincipals []string) error { tok, err := readToken(token) if err != nil { return trace.Wrap(err) @@ -78,7 +83,13 @@ func Register(dataDir, token string, id IdentityID, servers []utils.NetAddr) err defer client.Close() // get the host certificate and keys - keys, err := client.RegisterUsingToken(tok, id.HostUUID, id.NodeName, id.Role) + keys, err := client.RegisterUsingToken(RegisterUsingTokenRequest{ + Token: tok, + HostID: id.HostUUID, + NodeName: id.NodeName, + Role: id.Role, + AdditionalPrincipals: additionalPrincipals, + }) if err != nil { return trace.Wrap(err) } @@ -88,13 +99,17 @@ func Register(dataDir, token string, id IdentityID, servers []utils.NetAddr) err // ReRegister renews the certificates and private keys based on the existing // identity ID -func ReRegister(dataDir string, clt ClientI, id IdentityID) error { +func ReRegister(dataDir string, clt ClientI, id IdentityID, additionalPrincipals []string) error { hostID, err := id.HostID() if err != nil { return trace.Wrap(err) } - keys, err := clt.GenerateServerKeys( - hostID, id.NodeName, teleport.Roles{id.Role}) + keys, err := clt.GenerateServerKeys(GenerateServerKeysRequest{ + HostID: hostID, + NodeName: id.NodeName, + Roles: teleport.Roles{id.Role}, + AdditionalPrincipals: additionalPrincipals, + }) if err != nil { return trace.Wrap(err) } diff --git a/lib/auth/testauthority/testauthority.go b/lib/auth/testauthority/testauthority.go index 543c7c4e615..fc71f9f916c 100644 --- a/lib/auth/testauthority/testauthority.go +++ b/lib/auth/testauthority/testauthority.go @@ -56,6 +56,7 @@ func (n *Keygen) GenerateHostCert(c services.HostCertParams) ([]byte, error) { validBefore = uint64(b.Unix()) } principals := native.BuildPrincipals(c.HostID, c.NodeName, c.ClusterName, c.Roles) + principals = append(principals, c.Principals...) cert := &ssh.Certificate{ ValidPrincipals: principals, Key: pubKey, diff --git a/lib/auth/tls_test.go b/lib/auth/tls_test.go index 93227c5bcef..a220650cdb4 100644 --- a/lib/auth/tls_test.go +++ b/lib/auth/tls_test.go @@ -521,20 +521,35 @@ func (s *TLSSuite) TestGenerateCerts(c *check.C) { c.Assert(err, check.IsNil) certs, err := hostClient.GenerateServerKeys( - hostID, s.server.AuthServer.ClusterName, teleport.Roles{teleport.RoleNode}) + GenerateServerKeysRequest{ + HostID: hostID, + NodeName: s.server.AuthServer.ClusterName, + Roles: teleport.Roles{teleport.RoleNode}, + AdditionalPrincipals: []string{"example.com"}, + }) c.Assert(err, check.IsNil) - _, _, _, _, err = ssh.ParseAuthorizedKey(certs.Cert) + key, _, _, _, err := ssh.ParseAuthorizedKey(certs.Cert) c.Assert(err, check.IsNil) + hostCert := key.(*ssh.Certificate) + comment := check.Commentf("can't find example.com in %v", hostCert.ValidPrincipals) + c.Assert(utils.SliceContainsStr(hostCert.ValidPrincipals, "example.com"), check.Equals, true, comment) // attempt to elevate privileges by getting admin role in the certificate _, err = hostClient.GenerateServerKeys( - hostID, s.server.AuthServer.ClusterName, teleport.Roles{teleport.RoleAdmin}) + GenerateServerKeysRequest{ + HostID: hostID, + NodeName: s.server.AuthServer.ClusterName, + Roles: teleport.Roles{teleport.RoleAdmin}, + }) fixtures.ExpectAccessDenied(c, err) // attempt to get certificate for different host id - _, err = hostClient.GenerateServerKeys( - "some-other-host-id", s.server.AuthServer.ClusterName, teleport.Roles{teleport.RoleNode}) + _, err = hostClient.GenerateServerKeys(GenerateServerKeysRequest{ + HostID: "some-other-host-id", + NodeName: s.server.AuthServer.ClusterName, + Roles: teleport.Roles{teleport.RoleNode}, + }) fixtures.ExpectAccessDenied(c, err) user1, userRole, err := CreateUserAndRole(clt, "user1", []string{"user1"}) diff --git a/lib/service/service.go b/lib/service/service.go index abd2f14d50d..5da4ed60c4a 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -195,7 +195,7 @@ func (process *TeleportProcess) GetIdentity(role teleport.Role) (i *auth.Identit // connectToAuthService attempts to login into the auth servers specified in the // configuration. Returns 'true' if successful -func (process *TeleportProcess) connectToAuthService(role teleport.Role) (*Connector, error) { +func (process *TeleportProcess) connectToAuthService(role teleport.Role, additionalPrincipals []string) (*Connector, error) { identity, err := process.GetIdentity(role) if err != nil { return nil, trace.Wrap(err) @@ -223,7 +223,7 @@ func (process *TeleportProcess) connectToAuthService(role teleport.Role) (*Conne return nil, trace.Wrap(err) } defer authClient.Close() - if err := auth.ReRegister(process.Config.DataDir, authClient, identity.ID); err != nil { + if err := auth.ReRegister(process.Config.DataDir, authClient, identity.ID, additionalPrincipals); err != nil { return nil, trace.Wrap(err) } if identity, err = process.readIdentity(role); err != nil { @@ -240,6 +240,19 @@ func (process *TeleportProcess) connectToAuthService(role teleport.Role) (*Conne if err != nil { return nil, trace.Wrap(err) } + if len(additionalPrincipals) != 0 && !identity.HasPrincipals(additionalPrincipals) { + log.Infof("Identity %v needs principals %v, going to re-register.", identity.ID, additionalPrincipals) + if err := auth.ReRegister(process.Config.DataDir, client, identity.ID, additionalPrincipals); err != nil { + return nil, trace.Wrap(err) + } + if identity, err = process.readIdentity(role); err != nil { + return nil, trace.Wrap(err) + } + tlsConfig, err = identity.TLSConfig() + if err != nil { + return nil, trace.Wrap(err) + } + } // success ? we're logged in! return &Connector{Client: client, Identity: identity}, nil } @@ -535,7 +548,7 @@ func (process *TeleportProcess) initAuthService(authority sshca.Authority) error process.RegisterFunc("auth.heartbeat.broadcast", func() error { // Heart beat auth server presence, this is not the best place for this // logic, consolidate it into auth package later - connector, err := process.connectToAuthService(teleport.RoleAdmin) + connector, err := process.connectToAuthService(teleport.RoleAdmin, nil) if err != nil { return trace.Wrap(err) } @@ -642,7 +655,7 @@ func (process *TeleportProcess) newLocalCache(clt auth.ClientI, cacheName []stri // initSSH initializes the "node" role, i.e. a simple SSH server connected to the auth server. func (process *TeleportProcess) initSSH() error { process.RegisterWithAuthServer( - process.Config.Token, teleport.RoleNode, SSHIdentityEvent) + process.Config.Token, teleport.RoleNode, SSHIdentityEvent, nil) eventsC := make(chan Event) process.WaitForEvent(SSHIdentityEvent, eventsC, make(chan struct{})) @@ -722,7 +735,7 @@ func (process *TeleportProcess) initSSH() error { // RegisterWithAuthServer uses one time provisioning token obtained earlier // from the server to get a pair of SSH keys signed by Auth server host // certificate authority -func (process *TeleportProcess) RegisterWithAuthServer(token string, role teleport.Role, eventName string) { +func (process *TeleportProcess) RegisterWithAuthServer(token string, role teleport.Role, eventName string, additionalPrincipals []string) { cfg := process.Config identityID := auth.IdentityID{Role: role, HostUUID: cfg.HostUUID, NodeName: cfg.Hostname} @@ -733,7 +746,7 @@ func (process *TeleportProcess) RegisterWithAuthServer(token string, role telepo process.RegisterFunc(fmt.Sprintf("register.%v", strings.ToLower(role.String())), func() error { retryTime := defaults.ServerHeartbeatTTL / 3 for { - connector, err := process.connectToAuthService(role) + connector, err := process.connectToAuthService(role, additionalPrincipals) if err == nil { process.BroadcastEvent(Event{Name: eventName, Payload: connector}) authClient = connector.Client @@ -752,14 +765,14 @@ func (process *TeleportProcess) RegisterWithAuthServer(token string, role telepo // Auth service is on the same host, no need to go though the invitation // procedure log.Debugf("This server has local Auth server started, using it to add role to the cluster.") - err = auth.LocalRegister(cfg.DataDir, identityID, process.getLocalAuth()) + err = auth.LocalRegister(cfg.DataDir, identityID, process.getLocalAuth(), additionalPrincipals) } else { // Auth server is remote, so we need a provisioning token if token == "" { return trace.BadParameter("%v must join a cluster and needs a provisioning token", role) } log.Infof("Joining the cluster with a token %v.", token) - err = auth.Register(cfg.DataDir, token, identityID, cfg.AuthServers) + err = auth.Register(cfg.DataDir, token, identityID, cfg.AuthServers, additionalPrincipals) } if err != nil { log.Errorf("Failed to join the cluster: %v.", err) @@ -792,7 +805,16 @@ func (process *TeleportProcess) initProxy() error { } } - process.RegisterWithAuthServer(process.Config.Token, teleport.RoleProxy, ProxyIdentityEvent) + var additionalPrincipals []string + if process.Config.Proxy.PublicAddr.Addr != "" { + host, err := utils.Host(process.Config.Proxy.PublicAddr.Addr) + if err != nil { + return trace.Wrap(err) + } + additionalPrincipals = []string{host} + } + + process.RegisterWithAuthServer(process.Config.Token, teleport.RoleProxy, ProxyIdentityEvent, additionalPrincipals) process.RegisterFunc("proxy.init", func() error { eventsC := make(chan Event) process.WaitForEvent(ProxyIdentityEvent, eventsC, make(chan struct{})) diff --git a/lib/srv/regular/sshserver_test.go b/lib/srv/regular/sshserver_test.go index 2f7f4bf9872..c50c4ea5c6d 100644 --- a/lib/srv/regular/sshserver_test.go +++ b/lib/srv/regular/sshserver_test.go @@ -111,7 +111,11 @@ func (s *SrvSuite) SetUpTest(c *C) { c.Assert(err, IsNil) // set up host private key and certificate - certs, err := s.server.Auth().GenerateServerKeys(hostID, s.server.ClusterName(), teleport.Roles{teleport.RoleNode}) + certs, err := s.server.Auth().GenerateServerKeys(auth.GenerateServerKeysRequest{ + HostID: hostID, + NodeName: s.server.ClusterName(), + Roles: teleport.Roles{teleport.RoleNode}, + }) c.Assert(err, IsNil) // set up user CA and set up a user that has access to the server diff --git a/lib/utils/utils.go b/lib/utils/utils.go index 43367448fe8..05de85cab37 100644 --- a/lib/utils/utils.go +++ b/lib/utils/utils.go @@ -33,6 +33,19 @@ import ( "golang.org/x/crypto/ssh" ) +// StringsSet creates set of string (map[string]struct{}) +// from a list of strings +func StringsSet(in []string) map[string]struct{} { + if in == nil { + return nil + } + out := make(map[string]struct{}) + for _, v := range in { + out[v] = struct{}{} + } + return out +} + // ParseOnOff parses whether value is "on" or "off", parameterName is passed for error // reporting purposes, defaultValue is returned when no value is set func ParseOnOff(parameterName, val string, defaultValue bool) (bool, error) { @@ -62,6 +75,18 @@ func IsGroupMember(gid int) (bool, error) { return false, nil } +// Host extracts host from host:port string +func Host(hostname string) (string, error) { + if hostname == "" { + return "", trace.BadParameter("missing parameter hostname") + } + if !strings.Contains(hostname, ":") { + return hostname, nil + } + host, _, err := SplitHostPort(hostname) + return host, err +} + // SplitHostPort splits host and port and checks that host is not empty func SplitHostPort(hostname string) (string, string, error) { host, port, err := net.SplitHostPort(hostname) diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index f3b090d7ac7..ab79832c0d7 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -152,7 +152,11 @@ func (s *WebSuite) SetUpTest(c *C) { nodePort := s.freePorts[len(s.freePorts)-1] s.freePorts = s.freePorts[:len(s.freePorts)-1] - certs, err := s.server.Auth().GenerateServerKeys(hostID, s.server.ClusterName(), teleport.Roles{teleport.RoleNode}) + certs, err := s.server.Auth().GenerateServerKeys(auth.GenerateServerKeysRequest{ + HostID: hostID, + NodeName: s.server.ClusterName(), + Roles: teleport.Roles{teleport.RoleNode}, + }) c.Assert(err, IsNil) signer, err := sshutils.NewSigner(certs.Key, certs.Cert)