Skip to content

Commit

Permalink
Implement multiple roles per token (#161)
Browse files Browse the repository at this point in the history
* add app roles

* update tests

* fix tests

* useCombinedTokens addition

* CHANGELOG

* update for roles set on configmap

* update everything for roles flag

* go mod tidy

* clear DOCKERFILE + fix deployment

* values schema json update

* updates for comments

* CHANGELOG UPDATE
  • Loading branch information
ssyno authored Oct 11, 2024
1 parent f7646b5 commit 4ee9cf1
Show file tree
Hide file tree
Showing 15 changed files with 234 additions and 132 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Implemented option to generate combined tokens with multiple roles
- Change ownership to Team Shield

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ require (
github.com/onsi/ginkgo/v2 v2.20.0
github.com/onsi/gomega v1.34.1
github.com/pkg/errors v0.9.1
go.uber.org/zap v1.27.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.25.0
k8s.io/apimachinery v0.25.0
Expand Down Expand Up @@ -92,6 +91,7 @@ require (
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.28.0 // indirect
Expand Down
5 changes: 3 additions & 2 deletions helm/teleport-operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ spec:
labels:
{{- include "labels.selector" . | nindent 8 }}
spec:
serviceAccountName: {{ include "resource.default.name" . }}
serviceAccountName: {{ include "resource.default.name" . }}
securityContext:
runAsUser: {{ .Values.pod.user.id }}
runAsGroup: {{ .Values.pod.group.id }}
Expand All @@ -35,7 +35,8 @@ spec:
value: "{{ .Values.teleport.proxyAddr }}=yes"
image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Chart.Version }}"
args:
- "--namespace={{ include "resource.default.namespace" . }}"
- "--namespace={{ include "resource.default.namespace" . }}"
- "--token-roles={{ .Values.teleportOperator.roles | join "," }}"
{{- if .Values.tbot.enabled }}
- "--tbot"
{{- end }}
Expand Down
13 changes: 13 additions & 0 deletions helm/teleport-operator/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@
}
}
},
"teleportOperator": {
"type": "object",
"properties": {
"roles": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of roles for the Teleport operator. Example: ['kube', 'app']"
}
},
"required": ["roles"]
},
"pod": {
"type": "object",
"properties": {
Expand Down
7 changes: 6 additions & 1 deletion helm/teleport-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ teleport:
managementClusterName: ""
proxyAddr: test.teleport.giantswarm.io:443
teleportClusterName: test.teleport.giantswarm.io
teleportVersion: 15.1.7
teleportVersion: 16.1.7

teleportOperator:
roles:
- kube
- app

pod:
user:
Expand Down
33 changes: 17 additions & 16 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
TokenRoles []string
}

//+kubebuilder:rbac:groups=cluster.x-k8s.io.giantswarm.io,resources=clusters,verbs=get;list;watch;create;update;patch;delete
Expand Down Expand Up @@ -157,7 +158,7 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
return ctrl.Result{}, microerror.Mask(err)
}
if secret == nil {
token, err := r.Teleport.GenerateToken(ctx, registerName, "node")
token, err := r.Teleport.GenerateToken(ctx, registerName, []string{key.RoleNode})
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
Expand All @@ -169,12 +170,12 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, "node")
tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, key.RoleNode)
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
if !tokenValid {
token, err := r.Teleport.GenerateToken(ctx, registerName, "node")
token, err := r.Teleport.GenerateToken(ctx, registerName, []string{key.RoleNode})
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
Expand All @@ -186,41 +187,41 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
}

// Check if the confimap exists in the cluster, if not, generate teleport token and create the config map
// Check if the configmap exists in the cluster, if not, generate teleport token and create the config map
// if it is, check teleport token validity, and update the configmap if teleport token has expired
configMap, err := r.Teleport.GetConfigMap(ctx, log, r.Client, cluster.Name, cluster.Namespace)
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}

if configMap != nil {
if configMap == nil {
token, err := r.Teleport.GenerateToken(ctx, registerName, r.TokenRoles)
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 {
return ctrl.Result{}, microerror.Mask(err)
}
} 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, "kube")
tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, key.RolesToString(r.TokenRoles))
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
if !tokenValid {
token, err := r.Teleport.GenerateToken(ctx, registerName, "kube")
token, err := r.Teleport.GenerateToken(ctx, registerName, r.TokenRoles)
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
if err := r.Teleport.UpdateConfigMap(ctx, log, r.Client, configMap, token); err != nil {
if err := r.Teleport.UpdateConfigMap(ctx, log, r.Client, configMap, token, r.TokenRoles); err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
} else {
log.Info("ConfigMap has valid teleport kube join token", "configMapName", configMap.GetName())
}
} else {
token, err := r.Teleport.GenerateToken(ctx, registerName, "kube")
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
if err := r.Teleport.CreateConfigMap(ctx, log, r.Client, cluster.Name, cluster.Namespace, registerName, token); err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
}

if r.IsBotEnabled {
Expand Down
30 changes: 16 additions & 14 deletions internal/controller/cluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ 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),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
},
{
name: "case 1: Register cluster and update Secret, ConfigMap and App resources in case they exist",
Expand All @@ -59,10 +59,10 @@ 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),
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),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
},
{
name: "case 2: Update Secret and ConfigMap resources in case join token changes",
Expand All @@ -72,10 +72,10 @@ 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),
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.NewTokenName),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{"kube", "app"}),
},
{
name: "case 3: Deregister cluster and delete resources in case the cluster is deleted",
Expand All @@ -85,7 +85,7 @@ func Test_ClusterController(t *testing.T) {
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),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
},
{
name: "case 4: Reconnect to Teleport when credentials are rotated",
Expand All @@ -96,13 +96,13 @@ 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),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
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),
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{"kube", "app"}),
},
{
name: "case 5: Return an error in case reconnection to Teleport fails after the credentials are rotated",
Expand All @@ -112,7 +112,7 @@ 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),
configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}),
newTeleportClient: func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) {
return nil, errors.New("simulated error")
},
Expand Down Expand Up @@ -153,11 +153,13 @@ func Test_ClusterController(t *testing.T) {
log := ctrl.Log.WithName("test")

controller := &ClusterReconciler{
Client: ctrlClient,
Log: log,
Scheme: scheme.Scheme,
Namespace: tc.namespace,
Teleport: teleport.New(tc.namespace, tc.config, test.NewMockTokenGenerator(tc.token)),
Client: ctrlClient,
Log: log,
Scheme: scheme.Scheme,
Namespace: tc.namespace,
Teleport: teleport.New(tc.namespace, tc.config, test.NewMockTokenGenerator(tc.token)),
IsBotEnabled: false,
TokenRoles: []string{"kube", "app"},
}
controller.Teleport.TeleportClient = test.NewTeleportClient(test.FakeTeleportClientConfig{
Tokens: tc.tokens,
Expand Down
47 changes: 44 additions & 3 deletions internal/pkg/key/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package key

import (
"fmt"
"strings"
"time"

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

const (
Expand All @@ -15,6 +18,7 @@ const (
TeleportBotSecretName = "identity-output"
TeleportBotNamespace = "giantswarm"
TeleportBotAppName = "teleport-tbot"
TeleportAppTokenValidity = 720 * time.Hour
TeleportKubeTokenValidity = 720 * time.Hour
TeleportNodeTokenValidity = 720 * time.Hour

Expand All @@ -26,8 +30,45 @@ const (
ManagementClusterName = "managementClusterName"
ProxyAddr = "proxyAddr"
TeleportVersion = "teleportVersion"
RoleKube = "kube"
RoleApp = "app"
RoleNode = "node"
)

func ParseRoles(s string) ([]string, error) {
parts := strings.Split(s, ",")
roles := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
switch part {
case RoleKube, RoleApp, RoleNode:
roles = append(roles, part)
default:
return nil, fmt.Errorf("invalid role: %s", part)
}
}
return roles, nil
}

func RolesToString(roles []string) string {
return strings.Join(roles, ",")
}

func RolesToSystemRoles(roles []string) []types.SystemRole {
systemRoles := make([]types.SystemRole, 0, len(roles))
for _, role := range roles {
switch role {
case RoleKube:
systemRoles = append(systemRoles, types.RoleKube)
case RoleApp:
systemRoles = append(systemRoles, types.RoleApp)
case RoleNode:
systemRoles = append(systemRoles, types.RoleNode)
}
}
return systemRoles
}

func GetConfigmapName(clusterName string, appName string) string {
return fmt.Sprintf("%s-%s-config", clusterName, appName)
}
Expand Down Expand Up @@ -56,8 +97,8 @@ func GetAppName(clusterName string, appName string) string {
return fmt.Sprintf("%s-%s", clusterName, appName)
}

func GetConfigmapDataFromTemplate(authToken string, proxyAddr string, kubeClusterName string, teleportVersion string) string {
dataTpl := `roles: "kube"
func GetConfigmapDataFromTemplate(authToken string, proxyAddr string, kubeClusterName string, teleportVersion string, roles []string) string {
dataTpl := `roles: "%s"
authToken: "%s"
proxyAddr: "%s"
kubeClusterName: "%s"
Expand All @@ -67,7 +108,7 @@ kubeClusterName: "%s"
dataTpl = fmt.Sprintf("%steleportVersionOverride: %q", dataTpl, teleportVersion)
}

return fmt.Sprintf(dataTpl, authToken, proxyAddr, kubeClusterName)
return fmt.Sprintf(dataTpl, RolesToString(roles), authToken, proxyAddr, kubeClusterName)
}

func GetTbotConfigmapDataFromTemplate(kubeClusterName string, clusterName string) string {
Expand Down
12 changes: 6 additions & 6 deletions internal/pkg/teleport/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ func (t *Teleport) GetTokenFromConfigMap(ctx context.Context, configMap *corev1.
return token, nil
}

func (t *Teleport) CreateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string, clusterNamespace string, registerName string, token string) error {
func (t *Teleport) CreateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string, clusterNamespace string, registerName string, token string, roles []string) error {
configMapName := key.GetConfigmapName(clusterName, t.Config.AppName)

configMapData := map[string]string{
"values": t.getConfigMapData(registerName, token),
"values": t.getConfigMapData(registerName, token, roles),
}

cm := corev1.ConfigMap{}
Expand Down Expand Up @@ -137,7 +137,7 @@ func (t *Teleport) EnsureTbotConfigMap(ctx context.Context, log logr.Logger, ctr
return nil
}

func (t *Teleport) UpdateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, configMap *corev1.ConfigMap, token string) error {
func (t *Teleport) UpdateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, configMap *corev1.ConfigMap, token string, roles []string) error {
valuesBytes, ok := configMap.Data["values"]
if !ok {
return microerror.Mask(fmt.Errorf("malformed ConfigMap: key `values` not found"))
Expand All @@ -148,8 +148,8 @@ func (t *Teleport) UpdateConfigMap(ctx context.Context, log logr.Logger, ctrlCli
return microerror.Mask(fmt.Errorf("failed to parse YAML: %w", err))
}

// Modify the authToken value
valuesYaml["authToken"] = token
valuesYaml["roles"] = key.RolesToString(roles)

updatedValuesYaml, err := yaml.Marshal(valuesYaml)
if err != nil {
Expand Down Expand Up @@ -205,15 +205,15 @@ func (t *Teleport) DeleteTbotConfigMap(ctx context.Context, log logr.Logger, ctr
return nil
}

func (t *Teleport) getConfigMapData(registerName string, token string) string {
func (t *Teleport) getConfigMapData(registerName string, token string, roles []string) string {
var (
authToken = token
proxyAddr = t.Config.ProxyAddr
kubeClusterName = registerName
teleportVersionOverride = t.Config.TeleportVersion
)

return key.GetConfigmapDataFromTemplate(authToken, proxyAddr, kubeClusterName, teleportVersionOverride)
return key.GetConfigmapDataFromTemplate(authToken, proxyAddr, kubeClusterName, teleportVersionOverride, roles)
}

func (t *Teleport) getTbotConfigMapData(registerName string, clusterName string) string {
Expand Down
Loading

0 comments on commit 4ee9cf1

Please sign in to comment.