Skip to content

Commit

Permalink
Add support for extra principals, fixes #1174
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
klizhentas committed Jan 9, 2018
1 parent bbc843c commit c115373
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 108 deletions.
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

0 comments on commit c115373

Please sign in to comment.