Skip to content

Commit

Permalink
Allow setting a default service account for impersonation
Browse files Browse the repository at this point in the history
Introduce the flag `--default-service-account` for allowing cluster admins to enforce impersonation for resources reconciliation.

Signed-off-by: Stefan Prodan <[email protected]>
  • Loading branch information
stefanprodan committed Jan 27, 2022
1 parent 413717a commit 38de075
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 30 deletions.
65 changes: 37 additions & 28 deletions controllers/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type HelmReleaseReconciler struct {
EventRecorder kuberecorder.EventRecorder
ExternalEventRecorder *events.Recorder
MetricsRecorder *metrics.Recorder
DefaultServiceAccount string
}

func (r *HelmReleaseReconciler) SetupWithManager(mgr ctrl.Manager, opts HelmReleaseReconcilerOptions) error {
Expand Down Expand Up @@ -446,43 +447,51 @@ func (r *HelmReleaseReconciler) checkDependencies(hr v2.HelmRelease) error {
return nil
}

func (r *HelmReleaseReconciler) setImpersonationConfig(restConfig *rest.Config, hr v2.HelmRelease) {
name := r.DefaultServiceAccount
if sa := hr.Spec.ServiceAccountName; sa != "" {
name = sa
}
if name != "" {
username := fmt.Sprintf("system:serviceaccount:%s:%s", hr.GetNamespace(), name)
restConfig.Impersonate = rest.ImpersonationConfig{UserName: username}
}
}

func (r *HelmReleaseReconciler) getRESTClientGetter(ctx context.Context, hr v2.HelmRelease) (genericclioptions.RESTClientGetter, error) {
if hr.Spec.KubeConfig == nil {
// impersonate service account if specified
if hr.Spec.ServiceAccountName != "" {
token, err := r.getServiceAccountToken(ctx, hr)
if err != nil {
return nil, fmt.Errorf("could not impersonate ServiceAccount '%s': %w", hr.Spec.ServiceAccountName, err)
}
config := *r.Config
r.setImpersonationConfig(&config, hr)

config := *r.Config
config.BearerToken = token
return kube.NewInClusterRESTClientGetter(&config, hr.GetReleaseNamespace()), nil
if hr.Spec.KubeConfig != nil {
secretName := types.NamespacedName{
Namespace: hr.GetNamespace(),
Name: hr.Spec.KubeConfig.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Get(ctx, secretName, &secret); err != nil {
return nil, fmt.Errorf("could not find KubeConfig secret '%s': %w", secretName, err)
}

return kube.NewInClusterRESTClientGetter(r.Config, hr.GetReleaseNamespace()), nil
}
secretName := types.NamespacedName{
Namespace: hr.GetNamespace(),
Name: hr.Spec.KubeConfig.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Get(ctx, secretName, &secret); err != nil {
return nil, fmt.Errorf("could not find KubeConfig secret '%s': %w", secretName, err)
}
var kubeConfig []byte
for k, _ := range secret.Data {
if k == "value" || k == "value.yaml" {
kubeConfig = secret.Data[k]
break
}
}

var kubeConfig []byte
for k, _ := range secret.Data {
if k == "value" || k == "value.yaml" {
kubeConfig = secret.Data[k]
break
if len(kubeConfig) == 0 {
return nil, fmt.Errorf("KubeConfig secret '%s' does not contain a 'value' key", secretName)
}
return kube.NewMemoryRESTClientGetter(kubeConfig, hr.GetReleaseNamespace(), &config), nil
}

if len(kubeConfig) == 0 {
return nil, fmt.Errorf("KubeConfig secret '%s' does not contain a 'value' key", secretName)
if r.DefaultServiceAccount != "" || hr.Spec.ServiceAccountName != "" {
return kube.NewInClusterRESTClientGetter(&config, hr.GetReleaseNamespace()), nil
}
return kube.NewMemoryRESTClientGetter(kubeConfig, hr.GetReleaseNamespace()), nil

return kube.NewInClusterRESTClientGetter(r.Config, hr.GetReleaseNamespace()), nil

}

func (r *HelmReleaseReconciler) getServiceAccountToken(ctx context.Context, hr v2.HelmRelease) (string, error) {
Expand Down
12 changes: 12 additions & 0 deletions docs/spec/v2beta1/helmreleases.md
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,15 @@ When the controller reconciles the `podinfo` release, it will impersonate the `w
account. If the chart contains cluster level objects like CRDs, the reconciliation will fail since
the account it runs under has no permissions to alter objects outside of the `webapp` namespace.

### Enforce impersonation

On multi-tenant clusters, platform admins can enforce impersonation with the
`--default-service-account` flag.

When the flag is set, all HelmReleases which don't have `spec.serviceAccountName` specified
will use the service account name provided by `--default-service-account=<SA Name>`
in the namespace of the object.

## Remote Clusters / Cluster-API

If the `spec.kubeConfig` field is set, Helm actions will run against the default cluster specified
Expand Down Expand Up @@ -1122,6 +1131,9 @@ kubectl -n default create secret generic prod-kubeconfig \
> from current Cluster API providers. KubeConfigs with cmd-path in them likely won't work without
> a custom, per-provider installation of helm-controller.

When both `spec.kubeConfig` and `spec.ServiceAccountName` are specified,
the controller will impersonate the service account on the target cluster.

## Post Renderers

HelmRelease resources has a built-in [Kustomize](https://kubectl.docs.kubernetes.io/references/kustomize/)
Expand Down
25 changes: 23 additions & 2 deletions internal/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ func NewInClusterRESTClientGetter(cfg *rest.Config, namespace string) genericcli
flags.BearerToken = &cfg.BearerToken
flags.CAFile = &cfg.CAFile
flags.Namespace = &namespace
if sa := cfg.Impersonate.UserName; sa != "" {
flags.Impersonate = &sa
}

return flags
}

Expand All @@ -40,17 +44,26 @@ func NewInClusterRESTClientGetter(cfg *rest.Config, namespace string) genericcli
type MemoryRESTClientGetter struct {
kubeConfig []byte
namespace string
cfg *rest.Config
}

func NewMemoryRESTClientGetter(kubeConfig []byte, namespace string) genericclioptions.RESTClientGetter {
func NewMemoryRESTClientGetter(kubeConfig []byte, namespace string, cfg *rest.Config) genericclioptions.RESTClientGetter {
return &MemoryRESTClientGetter{
kubeConfig: kubeConfig,
namespace: namespace,
cfg: cfg,
}
}

func (c *MemoryRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
return clientcmd.RESTConfigFromKubeConfig(c.kubeConfig)
cfg, err := clientcmd.RESTConfigFromKubeConfig(c.kubeConfig)
if err != nil {
return nil, err
}
if sa := c.cfg.Impersonate.UserName; sa != "" {
cfg.Impersonate = c.cfg.Impersonate
}
return cfg, nil
}

func (c *MemoryRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
Expand All @@ -59,6 +72,10 @@ func (c *MemoryRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryI
return nil, err
}

if sa := c.cfg.Impersonate.UserName; sa != "" {
config.Impersonate = c.cfg.Impersonate
}

// The more groups you have, the more discovery requests you need to make.
// given 25 groups (our groups + a few custom resources) with one-ish version each, discovery needs to make 50 requests
// double it just so we don't end up here again for a while. This config is only used for discovery.
Expand Down Expand Up @@ -88,5 +105,9 @@ func (c *MemoryRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig
overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
overrides.Context.Namespace = c.namespace

if sa := c.cfg.Impersonate.UserName; sa != "" {
overrides.AuthInfo.Impersonate = c.cfg.Impersonate.UserName
}

return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
}
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func main() {
clientOptions client.Options
logOptions logger.Options
leaderElectionOptions leaderelection.Options
defaultServiceAccount string
)

flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
Expand All @@ -81,6 +82,7 @@ func main() {
flag.BoolVar(&watchAllNamespaces, "watch-all-namespaces", true,
"Watch for custom resources in all namespaces, if set to false it will only watch the runtime namespace.")
flag.IntVar(&httpRetry, "http-retry", 9, "The maximum number of retries when failing to fetch artifacts over HTTP.")
flag.StringVar(&defaultServiceAccount, "default-service-account", "", "Default service account used for impersonation.")
clientOptions.BindFlags(flag.CommandLine)
logOptions.BindFlags(flag.CommandLine)
leaderElectionOptions.BindFlags(flag.CommandLine)
Expand Down Expand Up @@ -139,6 +141,7 @@ func main() {
EventRecorder: mgr.GetEventRecorderFor(controllerName),
ExternalEventRecorder: eventRecorder,
MetricsRecorder: metricsRecorder,
DefaultServiceAccount: defaultServiceAccount,
}).SetupWithManager(mgr, controllers.HelmReleaseReconcilerOptions{
MaxConcurrentReconciles: concurrent,
DependencyRequeueInterval: requeueDependency,
Expand Down

0 comments on commit 38de075

Please sign in to comment.