Skip to content
Closed
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
61 changes: 61 additions & 0 deletions api/client/joinservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
)

// JoinServiceClient is a client for the JoinService, which runs on both the
Expand Down Expand Up @@ -125,3 +126,63 @@ func (c *JoinServiceClient) RegisterUsingAzureMethod(ctx context.Context, challe
}
return certsResp.Certs, nil
}

// KubernetesRemoteChallengeSolver
type KubernetesRemoteChallengeSolver func(audience string) (jwt string, err error)

// RegisterUsingKubernetesRemoteMethod
func (c *JoinServiceClient) RegisterUsingKubernetesRemoteMethod(ctx context.Context, tokenReq *types.RegisterUsingTokenRequest, solve KubernetesRemoteChallengeSolver) (*proto.Certs, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()

joinClient, err := c.grpcClient.RegisterUsingKubernetesRemoteMethod(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

// 1. Send initial request to server describing which token we wish to use.
if err := joinClient.Send(&proto.RegisterUsingKubernetesRemoteMethodRequest{
Payload: &proto.RegisterUsingKubernetesRemoteMethodRequest_RegisterUsingTokenRequest{
RegisterUsingTokenRequest: tokenReq,
},
}); err != nil {
return nil, trace.Wrap(err)
}

// 2. Receive the challenge audience from the server.
resp, err := joinClient.Recv()
if err != nil {
return nil, trace.Wrap(err)
}
challenge := resp.GetChallengeAudience()
if challenge == "" {
return nil, trace.BadParameter("received empty challenge audience from server")
}

// 3. Solve the challenge using the provided callback func
jwt, err := solve(challenge)
if err != nil {
return nil, trace.Wrap(err)
}

// 4. Send the solution to the server.
if err := joinClient.Send(&proto.RegisterUsingKubernetesRemoteMethodRequest{
Payload: &proto.RegisterUsingKubernetesRemoteMethodRequest_ChallengeSolutionJwt{
ChallengeSolutionJwt: jwt,
},
}); err != nil {
return nil, trace.Wrap(err)
}

// 5/
resp, err = joinClient.Recv()
if err != nil {
return nil, trace.Wrap(err)
}
certs := resp.GetCerts()
if certs == nil {
return nil, trace.BadParameter("did not receive expected certs from server")
}

return certs, nil
}
811 changes: 770 additions & 41 deletions api/client/proto/joinservice.pb.go

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions api/proto/teleport/legacy/client/proto/joinservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ message RegisterUsingAzureMethodResponse {
Certs certs = 2;
}

// RegisterUsingKubernetesRemoteMethodRequest is the request for registration
// via the `kubernetes_remote` join method. It is a stream of potential payload
// values as the registration is a multi step process.
message RegisterUsingKubernetesRemoteMethodRequest {
oneof payload {
// RegisterUsingTokenRequest holds registration parameters common to all
// join methods.
types.RegisterUsingTokenRequest register_using_token_request = 1;
// ChallengeSolutionJWT holds the JWT generated by the client using
// the Kubernetes API TokenRequest and including the audience requested by
// the server in ChallengeAudience.
string challenge_solution_jwt = 2;
}
}

// RegisterUsingKubernetesRemoteMethodResponse is the response for registration
// via the `kubernetes_remote` join method. It is a stream of potential payload
// values as the registration is a multi step process.
message RegisterUsingKubernetesRemoteMethodResponse {
oneof payload {
// ChallengeAudience is the audience that the client should use when
// calling the Kubernetes API TokenRequest method.
string challenge_audience = 1;
// Certs is the returned signed certs.
Certs certs = 2;
}
}

// JoinService provides methods which allow Teleport nodes, proxies, and other
// services to join the Teleport cluster by fetching signed cluster
// certificates. It is implemented on both the Auth and Proxy servers to serve
Expand All @@ -79,4 +107,7 @@ service JoinService {
// RegisterUsingAzureMethod is used to register a new node to the cluster
// using the Azure join method.
rpc RegisterUsingAzureMethod(stream RegisterUsingAzureMethodRequest) returns (stream RegisterUsingAzureMethodResponse);
// RegisterUsingKubernetesRemoteMethod is used to register a new node to the
// cluster using the `kubernetes-remote` join method.
rpc RegisterUsingKubernetesRemoteMethod(stream RegisterUsingKubernetesRemoteMethodRequest) returns (stream RegisterUsingKubernetesRemoteMethodResponse);
}
43 changes: 43 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,8 @@ message ProvisionTokenSpecV2 {
ProvisionTokenSpecV2GitLab GitLab = 12 [(gogoproto.jsontag) = "gitlab,omitempty"];
// GCP allows the configuration of options specific to the "gcp" join method.
ProvisionTokenSpecV2GCP GCP = 13 [(gogoproto.jsontag) = "gcp,omitempty"];
// KubernetesRemote allows the configuration of options specific to the "kubernetes_remote" join method.
ProvisionTokenSpecV2KubernetesRemote KubernetesRemote = 14 [(gogoproto.jsontag) = "kubernetes_remote,omitempty"];
}

// ProvisionTokenSpecV2Github contains the GitHub-specific part of the
Expand Down Expand Up @@ -1369,6 +1371,47 @@ message ProvisionTokenSpecV2Kubernetes {
repeated Rule Allow = 1 [(gogoproto.jsontag) = "allow,omitempty"];
}

// ProvisionTokenSpecV2KubernetesRemote contains the Kubernetes-specific part of the
// ProvisionTokenSpecV2
message ProvisionTokenSpecV2KubernetesRemote {
// Rule is a set of properties the Kubernetes-issued token might have to be
// allowed to use this ProvisionToken.
message Rule {
// ServiceAccount is the namespaced name of the Kubernetes service account.
// The format is "namespace:service-account". This value must be provided
// for each rule.
string ServiceAccount = 1 [(gogoproto.jsontag) = "service_account,omitempty"];
// Clusters is a list of cluster names.
// If empty, the allow rule can be used by any cluster. If specified,
// only tokens from the specified clusters can use this allow rule.
repeated string Clusters = 2 [(gogoproto.jsontag) = "clusters,omitempty"];
}

// Cluster is a set of properties that describe a cluster that an entity may
// attempt to join from.
message Cluster {
// Name is an identifier configured for the cluster. This value is chosen
// by the user, and does not have to match a specific value configured in
// Kubernetes.
string Name = 1 [(gogoproto.jsontag) = "name,omitempty"];
// Source is how the public signing keys for the cluster will be obtained
// to use for validating a provided JWT.
oneof Source {
// StaticJWKS allows the public signing keys for a cluster to be
// specified statically with the JSON body of the JWKS endpoint.
string StaticJWKS = 2 [(gogoproto.jsontag) = "static_jwks,omitempty"];
}
}

// Clusters is a list of Cluster that are accepted sources of JWTs to use
// against this token. A Service Account JWT signed by a cluster not listed
// here will be rejected.
repeated Cluster Clusters = 1 [(gogoproto.jsontag) = "clusters,omitempty"];
// Allow is a list of Rules, nodes using this token must match one
// allow rule to use this token.
repeated Rule Allow = 2 [(gogoproto.jsontag) = "allow,omitempty"];
}

// ProvisionTokenSpecV2Azure contains the Azure-specific part of the
// ProvisionTokenSpecV2.
message ProvisionTokenSpecV2Azure {
Expand Down
58 changes: 58 additions & 0 deletions api/types/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ const (
// JoinMethodGCP indicates that the node will join with the GCP join method.
// Documentation regarding implementation of this can be found in lib/gcp.
JoinMethodGCP JoinMethod = "gcp"
// JoinMethodKubernetesRemote indicates that the node will join with the
// remote Kubernetes join method. Documentation regarding implementation
// can be found in lib/kubernetestoken
JoinMethodKubernetesRemote JoinMethod = "kubernetes_remote"
)

var JoinMethods = []JoinMethod{
Expand All @@ -74,6 +78,7 @@ var JoinMethods = []JoinMethod{
JoinMethodAzure,
JoinMethodGitLab,
JoinMethodGCP,
JoinMethodKubernetesRemote,
}

func ValidateJoinMethod(method JoinMethod) error {
Expand Down Expand Up @@ -300,6 +305,17 @@ func (p *ProvisionTokenV2) CheckAndSetDefaults() error {
if err := providerCfg.checkAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
case JoinMethodKubernetesRemote:
providerCfg := p.Spec.KubernetesRemote
if providerCfg == nil {
return trace.BadParameter(
`"kubernetes_remote" configuration must be provided for the join method %q`,
JoinMethodKubernetesRemote,
)
}
if err := providerCfg.checkAndSetDefaults(); err != nil {
return trace.Wrap(err, "validating spec.%s", JoinMethodKubernetesRemote)
}
default:
return trace.BadParameter("unknown join method %q", p.Spec.JoinMethod)
}
Expand Down Expand Up @@ -660,3 +676,45 @@ func (a *ProvisionTokenSpecV2GCP) checkAndSetDefaults() error {
}
return nil
}

func (a *ProvisionTokenSpecV2KubernetesRemote) checkAndSetDefaults() error {
if len(a.Allow) == 0 {
return trace.BadParameter("allow: must be non-empty")
}
for i, allowRule := range a.Allow {
if allowRule.ServiceAccount == "" {
return trace.BadParameter("allow[%d].service_account: must be non-empty", i)
}
if len(strings.Split(allowRule.ServiceAccount, ":")) != 2 {
return trace.BadParameter(
`allow[%d].service_account: must be in format "namespace:service_account", got %q instead`,
i,
allowRule.ServiceAccount,
)
}
// Ensure any cluster referenced within the allow rules has been
// specified under clusters.
for _, clusterName := range allowRule.Clusters {
for _, cluster := range a.Clusters {
if clusterName == cluster.Name {
break
}
return trace.BadParameter("allow[%d].cluster: specifies %q but this cluster was not defined in clusters", i, clusterName)
}
}
}

if len(a.Clusters) == 0 {
return trace.BadParameter("clusters: must be non-empty")
}
for i, cluster := range a.Clusters {
if cluster.Name == "" {
return trace.BadParameter("clusters[%d].name: must be non-empty", i)
}
if cluster.GetStaticJWKS() == "" {
return trace.BadParameter("cluster[%d].static_jwks: must be non-empty", i)
}
// TODO: Validate JWKS ??
}
return nil
}
Loading