Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for extra principals, fixes #1174 #1562

Merged
merged 1 commit into from
Jan 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 4 additions & 22 deletions lib/auth/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
}
Expand Down
112 changes: 86 additions & 26 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -720,47 +750,77 @@ 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).
//
// 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
}

Expand Down
56 changes: 48 additions & 8 deletions lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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`)
Expand All @@ -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
Expand All @@ -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)
Expand Down
16 changes: 8 additions & 8 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Loading