~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
168 lines · 5.8 KB Raw History
package clusterkcp

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/clusterprovider"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kcpclient"
)

const serviceAccountAttempts = 3

func TenantNamespaces() []string {
	return []string{kcpclient.TenantNamespace}
}

func psaLabels() map[string]string {
	return map[string]string{
		clusterprovider.PSAEnforceLabel: clusterprovider.PSAEnforceValue,
		clusterprovider.PSAWarnLabel:    clusterprovider.PSAWarnValue,
	}
}

func labelsCarryPSA(labels map[string]string) bool {
	for k, want := range psaLabels() {
		if labels[k] != want {
			return false
		}
	}
	return true
}

func hardenedServiceAccount(namespace string) *corev1.ServiceAccount {
	return &corev1.ServiceAccount{
		ObjectMeta:                   metav1.ObjectMeta{Name: clusterprovider.DefaultServiceAccountName, Namespace: namespace},
		AutomountServiceAccountToken: new(false),
	}
}

func Harden(ctx context.Context, core kubernetes.Interface) error {
	if core == nil {
		return clusterprovider.ErrNoHardeningCredential
	}
	for _, ns := range TenantNamespaces() {
		if err := hardenNamespace(ctx, core, ns); err != nil {
			return err
		}
	}
	return nil
}

func hardenNamespace(ctx context.Context, core kubernetes.Interface, namespace string) error {
	if err := ensureNamespace(ctx, core, namespace); err != nil {
		return err
	}
	if err := ensureDefaultServiceAccount(ctx, core, namespace); err != nil {
		return err
	}
	missing := missingHardening(ctx, core, namespace)
	if len(missing) > 0 {
		return fmt.Errorf("%w: %s", clusterprovider.ErrHardeningNotInForce, strings.Join(missing, "; "))
	}
	return nil
}

func ensureNamespace(ctx context.Context, core kubernetes.Interface, namespace string) error {
	got, err := core.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
	switch {
	case apierrors.IsNotFound(err):
		_, err = core.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{Name: namespace, Labels: psaLabels()},
		}, metav1.CreateOptions{FieldManager: fieldManager})
		if err != nil && !apierrors.IsAlreadyExists(err) {
			return fmt.Errorf("clusterprovider: creating namespace %s: %w", namespace, err)
		}
		return nil
	case err != nil:
		return fmt.Errorf("clusterprovider: reading namespace %s: %w", namespace, err)
	}
	if labelsCarryPSA(got.Labels) {
		return nil
	}
	body, err := json.Marshal(map[string]any{
		"metadata": map[string]any{"name": namespace, "labels": psaLabels()},
	})
	if err != nil {
		return fmt.Errorf("clusterprovider: rendering the PSA labels for %s: %w", namespace, err)
	}
	if _, err := core.CoreV1().Namespaces().Patch(ctx, namespace, types.MergePatchType, body, metav1.PatchOptions{FieldManager: fieldManager}); err != nil {
		return fmt.Errorf("clusterprovider: labelling namespace %s: %w", namespace, err)
	}
	return nil
}

func ensureDefaultServiceAccount(ctx context.Context, core kubernetes.Interface, namespace string) error {
	for range serviceAccountAttempts {
		sa, err := core.CoreV1().ServiceAccounts(namespace).Get(ctx, clusterprovider.DefaultServiceAccountName, metav1.GetOptions{})
		switch {
		case apierrors.IsNotFound(err):
			_, cerr := core.CoreV1().ServiceAccounts(namespace).Create(ctx, hardenedServiceAccount(namespace), metav1.CreateOptions{FieldManager: fieldManager})
			switch {
			case cerr == nil:
				return nil
			case apierrors.IsAlreadyExists(cerr):
				continue
			default:
				return fmt.Errorf("clusterprovider: creating the default ServiceAccount in %s: %w", namespace, cerr)
			}
		case err != nil:
			return fmt.Errorf("clusterprovider: reading the default ServiceAccount in %s: %w", namespace, err)
		}
		if sa.AutomountServiceAccountToken != nil && !*sa.AutomountServiceAccountToken {
			return nil
		}
		sa = sa.DeepCopy()
		sa.AutomountServiceAccountToken = new(false)
		if _, err := core.CoreV1().ServiceAccounts(namespace).Update(ctx, sa, metav1.UpdateOptions{FieldManager: fieldManager}); err != nil {
			return fmt.Errorf("clusterprovider: setting automountServiceAccountToken=false on the default ServiceAccount in %s: %w", namespace, err)
		}
		return nil
	}
	return fmt.Errorf("%w (%s)", clusterprovider.ErrHardeningRaced, namespace)
}

type labelWant struct {
	label string
	value string
}

var psaWants = []labelWant{
	{clusterprovider.PSAEnforceLabel, clusterprovider.PSAEnforceValue},
	{clusterprovider.PSAWarnLabel, clusterprovider.PSAWarnValue},
}

func missingHardening(ctx context.Context, core kubernetes.Interface, namespace string) []string {
	var missing []string

	ns, err := core.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
	switch {
	case err != nil:
		missing = append(missing, fmt.Sprintf("namespace %s: unreadable: %v", namespace, err))
	default:
		for _, w := range psaWants {
			if got := ns.Labels[w.label]; got != w.value {
				missing = append(missing, fmt.Sprintf("namespace %s %s=%q want %q", namespace, w.label, got, w.value))
			}
		}
	}

	sa, err := core.CoreV1().ServiceAccounts(namespace).Get(ctx, clusterprovider.DefaultServiceAccountName, metav1.GetOptions{})
	switch {
	case err != nil:
		missing = append(missing, fmt.Sprintf("namespace %s default ServiceAccount: does not exist -- every pod in it gets an API token it did not ask for", namespace))
	case sa.AutomountServiceAccountToken == nil:
		missing = append(missing, fmt.Sprintf("namespace %s default ServiceAccount has no automountServiceAccountToken field, which means the cluster default (true)", namespace))
	case *sa.AutomountServiceAccountToken:
		missing = append(missing, fmt.Sprintf("namespace %s default ServiceAccount still automounts a token", namespace))
	}
	return missing
}