Skip to content

Commit

Permalink
Mc namespace (#167)
Browse files Browse the repository at this point in the history
* implement mcNamespace

* CHANGELOG

* fix configmap handling

* test for token validation

* newtoken & istokenvalid rework

* check the same way as secret's token

* istokenvalid update

* 5 minutes reconciliation period

* 720 hours default expiry
  • Loading branch information
ssyno authored Oct 11, 2024
1 parent f339e15 commit 4c40268
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 47 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Fixed

- Implemented `MC-Namespace` flag to avoid enabling roles except `kube` to workload clusters
- Increased `grpc buffer` size

## [0.11.1] - 2024-10-11
Expand Down
1 change: 1 addition & 0 deletions helm/teleport-operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ spec:
args:
- "--namespace={{ include "resource.default.namespace" . }}"
- "--token-roles={{ .Values.teleportOperator.roles | join "," }}"
- "--mc-namespace={{ .Values.teleportOperator.mcNamespace }}"
{{- if .Values.tbot.enabled }}
- "--tbot"
{{- end }}
Expand Down
1 change: 1 addition & 0 deletions helm/teleport-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ teleport:
teleportOperator:
roles:
- kube
mcNamespace: "org-giantswarm"

pod:
user:
Expand Down
27 changes: 17 additions & 10 deletions internal/controller/cluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type ClusterReconciler struct {
Teleport *teleport.Teleport
IsBotEnabled bool
Namespace string
MCNamespace string
TokenRoles []string
}

Expand All @@ -69,9 +70,14 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
if apierrors.IsNotFound(err) {
return ctrl.Result{}, nil
}

return ctrl.Result{}, microerror.Mask(err)
}

roles := []string{key.RoleKube}
if r.MCNamespace != "" && cluster.Namespace == r.MCNamespace {
roles = r.TokenRoles
}

log.Info("Reconciling cluster", "cluster", cluster)

if r.Teleport.Identity != nil {
Expand Down Expand Up @@ -151,8 +157,7 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
}

// Check if the secret exists in the cluster, if not, generate teleport token and create the secret
// if it is, check teleport token validity, and update the secret if teleport token has expired
// Check and update Secret if necessary
secret, err := r.Teleport.GetSecret(ctx, log, r.Client, cluster.Name, cluster.Namespace)
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
Expand Down Expand Up @@ -195,32 +200,34 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}

if configMap == nil {
token, err := r.Teleport.GenerateToken(ctx, registerName, r.TokenRoles)
token, err := r.Teleport.GenerateToken(ctx, registerName, roles)
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
if err := r.Teleport.CreateConfigMap(ctx, log, r.Client, cluster.Name, cluster.Namespace, registerName, token, r.TokenRoles); err != nil {
if err := r.Teleport.CreateConfigMap(ctx, log, r.Client, cluster.Name, cluster.Namespace, registerName, token, roles); err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
log.Info("Created new config map with teleport join token", "configMapName", key.GetConfigmapName(cluster.Name, r.Teleport.Config.AppName), "roles", roles)
} else {
token, err := r.Teleport.GetTokenFromConfigMap(ctx, configMap)
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, key.RolesToString(r.TokenRoles))
tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, key.RolesToString(roles))
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
if !tokenValid {
token, err := r.Teleport.GenerateToken(ctx, registerName, r.TokenRoles)
newToken, err := r.Teleport.GenerateToken(ctx, registerName, roles)
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
if err := r.Teleport.UpdateConfigMap(ctx, log, r.Client, configMap, token, r.TokenRoles); err != nil {
if err := r.Teleport.UpdateConfigMap(ctx, log, r.Client, configMap, newToken, roles); err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
log.Info("Updated config map with new teleport join token", "configMapName", configMap.GetName(), "roles", roles)
} else {
log.Info("ConfigMap has valid teleport kube join token", "configMapName", configMap.GetName())
log.Info("ConfigMap has valid teleport join token", "configMapName", configMap.GetName(), "roles", roles)
}
}

Expand All @@ -242,7 +249,7 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct

// We need to requeue to check the teleport token validity
// and update secret for the cluster, if it expires
return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
}

// SetupWithManager sets up the controller with the Manager.
Expand Down
46 changes: 27 additions & 19 deletions internal/controller/cluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func Test_ClusterController(t *testing.T) {
expectedSecret *corev1.Secret
expectedConfigMap *corev1.ConfigMap
expectedError error
mcNamespace string
tokenRoles []string
}{
{
name: "case 0: Register cluster and create Secret, ConfigMap and App resources in case they do not exist",
Expand All @@ -49,20 +51,21 @@ func Test_ClusterController(t *testing.T) {
cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}),
tokenRoles: []string{key.RoleKube, key.RoleApp},
},
{
name: "case 1: Register cluster and update Secret, ConfigMap and App resources in case they exist",
name: "case 1: Register cluster in MC namespace and create Secret, ConfigMap with TokenRoles",
namespace: test.NamespaceName,
token: test.TokenName,
config: newConfig(),
identity: newIdentity(test.LastReadValue),
cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube, key.RoleApp}),
mcNamespace: test.NamespaceName,
tokenRoles: []string{key.RoleKube, key.RoleApp},
},
{
name: "case 2: Update Secret and ConfigMap resources in case join token changes",
Expand All @@ -72,20 +75,22 @@ func Test_ClusterController(t *testing.T) {
identity: newIdentity(test.LastReadValue),
cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}),
expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.NewTokenName),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{"kube", "app"}),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{key.RoleKube}),
tokenRoles: []string{key.RoleKube, key.RoleApp},
},
{
name: "case 3: Deregister cluster and delete resources in case the cluster is deleted",
namespace: test.NamespaceName,
token: test.TokenName,
config: newConfig(),
identity: newIdentity(test.LastReadValue),
cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Now()),
secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
name: "case 3: Deregister cluster and delete resources in case the cluster is deleted",
namespace: test.NamespaceName,
token: test.TokenName,
config: newConfig(),
identity: newIdentity(test.LastReadValue),
cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Now()),
secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}),
tokenRoles: []string{key.RoleKube, key.RoleApp},
},
{
name: "case 4: Reconnect to Teleport when credentials are rotated",
Expand All @@ -96,13 +101,14 @@ func Test_ClusterController(t *testing.T) {
cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName),
identitySecret: test.NewIdentitySecret(test.NamespaceName, test.IdentityFileValue),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}),
newTeleportClient: func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) {
return test.NewTeleportClient(test.FakeTeleportClientConfig{Tokens: nil}), nil
},
expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.NewTokenName),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{"kube", "app"}),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{key.RoleKube}),
tokenRoles: []string{key.RoleKube, key.RoleApp},
},
{
name: "case 5: Return an error in case reconnection to Teleport fails after the credentials are rotated",
Expand All @@ -112,11 +118,12 @@ func Test_ClusterController(t *testing.T) {
identity: newIdentity(time.Now().Add(-identityExpirationPeriod - time.Second)),
cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}),
newTeleportClient: func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) {
return nil, errors.New("simulated error")
},
expectedError: errors.New("secrets \"identity-output\" not found"),
tokenRoles: []string{key.RoleKube, key.RoleApp},
},
}

Expand Down Expand Up @@ -159,7 +166,8 @@ func Test_ClusterController(t *testing.T) {
Namespace: tc.namespace,
Teleport: teleport.New(tc.namespace, tc.config, test.NewMockTokenGenerator(tc.token)),
IsBotEnabled: false,
TokenRoles: []string{"kube", "app"},
TokenRoles: tc.tokenRoles,
MCNamespace: tc.mcNamespace,
}
controller.Teleport.TeleportClient = test.NewTeleportClient(test.FakeTeleportClientConfig{
Tokens: tc.tokens,
Expand Down
38 changes: 34 additions & 4 deletions internal/pkg/teleport/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package teleport

import (
"context"
"strings"
"time"

"github.com/go-logr/logr"
Expand All @@ -12,22 +13,51 @@ import (
"github.com/giantswarm/teleport-operator/internal/pkg/key"
)

func (t *Teleport) IsTokenValid(ctx context.Context, registerName string, oldToken string, tokenType string) (bool, error) {
func (t *Teleport) IsTokenValid(ctx context.Context, registerName string, token string, tokenType string) (bool, error) {
tokens, err := t.TeleportClient.GetTokens(ctx)
if err != nil {
return false, microerror.Mask(err)
}
for _, token := range tokens {
if token.GetMetadata().Labels["cluster"] == registerName && token.GetMetadata().Labels["type"] == tokenType {
if token.GetName() == oldToken {

expectedRoles, err := key.ParseRoles(tokenType)
if err != nil {
return false, microerror.Mask(err)
}

for _, t := range tokens {
if t.GetName() == token &&
t.GetMetadata().Labels["cluster"] == registerName {
// Check if the token has expired
if !t.Expiry().IsZero() && t.Expiry().After(time.Now()) {
// Check if the token has all the expected roles
tokenRoles := t.GetRoles()
if len(tokenRoles) != len(expectedRoles) {
return false, nil
}
for _, role := range expectedRoles {
if !containsRole(tokenRoles, role) {
return false, nil
}
}
return true, nil
}
return false, nil
}
}

// If we didn't find a matching token, it's not valid
return false, nil
}

func containsRole(roles []types.SystemRole, role string) bool {
for _, r := range roles {
if strings.ToLower(r.String()) == role {
return true
}
}
return false
}

func (t *Teleport) GenerateToken(ctx context.Context, registerName string, roles []string) (string, error) {
tokenValidity := time.Now().Add(key.TeleportKubeTokenValidity)
tokenRoles := key.RolesToSystemRoles(roles)
Expand Down
26 changes: 21 additions & 5 deletions internal/pkg/teleport/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,37 +118,53 @@ func Test_IsTokenValid(t *testing.T) {
expectedResult bool
}{
{
name: "case 0: Service should return true in case the token exists",
name: "case 0: Service should return true for a valid, non-expired token",
registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName),
tokenName: test.TokenName,
tokenType: test.TokenTypeKube,
tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"})},
tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"}, time.Now().Add(1*time.Hour))},
expectedResult: true,
},
{
name: "case 1: Service should return false in case the token does not exist",
name: "case 1: Service should return false for a non-existent token",
registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName),
tokenName: test.TokenName,
tokenType: test.TokenTypeKube,
tokens: nil,
expectedResult: false,
},
{
name: "case 2: Service should return false in case the token types do not match",
name: "case 2: Service should return false for a token with mismatched type",
registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName),
tokenName: test.TokenName,
tokenType: test.TokenTypeNode,
tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"})},
expectedResult: false,
},
{
name: "case 3: Service should fail in case the token cannot be retrieved",
name: "case 3: Service should fail when token list cannot be retrieved",
registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName),
tokenName: test.TokenName,
tokenType: test.TokenTypeKube,
failsList: true,
expectError: true,
},
{
name: "case 4: Service should return false for an expired token",
registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName),
tokenName: test.TokenName,
tokenType: test.TokenTypeKube,
tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"}, time.Now().Add(-1*time.Hour))},
expectedResult: false,
},
{
name: "case 5: Service should return false for a token with mismatched cluster name",
registerName: key.GetRegisterName(test.ManagementClusterName, "wrong-cluster"),
tokenName: test.TokenName,
tokenType: test.TokenTypeKube,
tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"}, time.Now().Add(1*time.Hour))},
expectedResult: false,
},
}

for _, tc := range testCases {
Expand Down
25 changes: 17 additions & 8 deletions internal/pkg/test/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

appv1alpha1 "github.com/giantswarm/apiextensions-application/api/v1alpha1"
"github.com/gravitational/teleport/api/types"
teleportTypes "github.com/gravitational/teleport/api/types"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -95,27 +96,35 @@ func NewConfigMap(clusterName, appName, namespaceName, tokenName string, roles [
}
}

func NewToken(tokenName, clusterName string, roles []string) teleportTypes.ProvisionToken {
newToken := &teleportTypes.ProvisionTokenV2{
Metadata: teleportTypes.Metadata{
func NewToken(tokenName, clusterName string, roles []string, expiry ...time.Time) types.ProvisionToken {
var expiryTime time.Time
if len(expiry) > 0 {
expiryTime = expiry[0]
} else {
expiryTime = time.Now().Add(720 * time.Hour)
}

newToken := &types.ProvisionTokenV2{
Metadata: types.Metadata{
Name: tokenName,
Labels: map[string]string{
ClusterKey: key.GetRegisterName(ManagementClusterName, clusterName),
TokenTypeKey: strings.Join(roles, ","),
},
Expires: &expiryTime,
},
Spec: teleportTypes.ProvisionTokenSpecV2{
Roles: []teleportTypes.SystemRole{},
Spec: types.ProvisionTokenSpecV2{
Roles: []types.SystemRole{},
},
}
for _, role := range roles {
switch role {
case key.RoleKube:
newToken.Spec.Roles = append(newToken.Spec.Roles, teleportTypes.RoleKube)
newToken.Spec.Roles = append(newToken.Spec.Roles, types.RoleKube)
case key.RoleApp:
newToken.Spec.Roles = append(newToken.Spec.Roles, teleportTypes.RoleApp)
newToken.Spec.Roles = append(newToken.Spec.Roles, types.RoleApp)
case key.RoleNode:
newToken.Spec.Roles = append(newToken.Spec.Roles, teleportTypes.RoleNode)
newToken.Spec.Roles = append(newToken.Spec.Roles, types.RoleNode)
}
}
return newToken
Expand Down
Loading

0 comments on commit 4c40268

Please sign in to comment.