package kubernetes import ( "context" "encoding/json" "os" "forge.cadoles.com/cadoles/bouncer/internal/auth/jwt" "forge.cadoles.com/cadoles/bouncer/internal/integration" "forge.cadoles.com/cadoles/bouncer/internal/jwk" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" v1 "k8s.io/api/core/v1" k8serr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) const ( namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" writerTokenSubject = "bouncer-admin-kubernetes-writer" readerTokenSubject = "bouncer-admin-kubernetes-reader" ) type Integration struct { Options *Options } // OnKeyLoad implements integration.OnKeyLoad. func (i *Integration) OnKeyLoad(ctx context.Context) (jwk.Key, error) { locker := i.Options.Locker timeout := i.Options.LockTimeout var key jwk.Key err := locker.WithLock(ctx, "bouncer-kubernetes-onkeyload", timeout, func(ctx context.Context) error { client, err := i.getClient() if err != nil { return errors.WithStack(err) } if i.Options.PrivateKeySecret != "" { sharedPrivateKey, err := i.getSharedPrivateKey(ctx, client, i.Options.PrivateKeySecretNamespace, i.Options.PrivateKeySecret) if err != nil { return errors.WithStack(err) } if sharedPrivateKey != nil { key = sharedPrivateKey } } return nil }) if err != nil { return nil, errors.WithStack(err) } return key, nil } // Integration implements integration.OnStartup. func (i *Integration) Integration() {} // OnStartup implements integration.OnStartup. func (i *Integration) OnStartup(ctx context.Context) error { locker := i.Options.Locker timeout := i.Options.LockTimeout err := locker.WithLock(ctx, "bouncer-kubernetes-onstartup", timeout, func(ctx context.Context) error { client, err := i.getClient() if err != nil { return errors.WithStack(err) } if i.Options.WriterTokenSecret != "" { if err := i.upsertTokenSecret(ctx, client, i.Options.WriterTokenSecretNamespace, i.Options.WriterTokenSecret, writerTokenSubject, jwt.RoleWriter); err != nil { return errors.Wrap(err, "could not upsert writer token secret") } } if i.Options.ReaderTokenSecret != "" { if err := i.upsertTokenSecret(ctx, client, i.Options.ReaderTokenSecretNamespace, i.Options.ReaderTokenSecret, readerTokenSubject, jwt.RoleReader); err != nil { return errors.Wrap(err, "could not upsert reader token secret") } } return nil }) if err != nil { return errors.WithStack(err) } return nil } func (i *Integration) getClient() (*kubernetes.Clientset, error) { config, err := rest.InClusterConfig() if err != nil { return nil, errors.WithStack(err) } client, err := kubernetes.NewForConfig(config) if err != nil { return nil, errors.WithStack(err) } return client, nil } const ( annotationPublicKey = "bouncer.cadoles.com/public-key" ) func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.Clientset, namespace string, name string, subject string, role jwt.Role) error { if namespace == "" { defaultNamespace, err := i.getCurrentNamespace() if err != nil { return errors.WithStack(err) } namespace = defaultNamespace } ctx = logger.With(ctx, logger.F("secretNamespace", namespace), logger.F("secretName", name), logger.F("tokenRole", role), logger.F("tokenSubject", subject), ) logger.Debug(ctx, "generating new token") alreadyExists := true secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { if k8serr.IsNotFound(err) { alreadyExists = false } else { return errors.WithStack(err) } } privateKey, err := integration.CtxPrivateKey(ctx) if err != nil { return errors.WithStack(err) } keySet, err := integration.CtxPublicKeySet(ctx) if err != nil { return errors.WithStack(err) } publicKeyThumbprint, err := getKeySetThumbprint(keySet) if err != nil { return errors.WithStack(err) } if !alreadyExists { token, err := jwt.GenerateToken(ctx, privateKey, i.Options.Issuer, subject, role) if err != nil { return errors.WithStack(err) } secret := &v1.Secret{ Type: v1.SecretTypeOpaque, ObjectMeta: metav1.ObjectMeta{ Name: name, Annotations: map[string]string{ annotationPublicKey: publicKeyThumbprint, }, }, StringData: map[string]string{ "token": token, }, } logger.Info(ctx, "creating token secret") if _, err := client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil { return errors.WithStack(err) } } else { existingPublicKeyHash, exists := secret.Annotations[annotationPublicKey] if !exists || publicKeyThumbprint != existingPublicKeyHash { token, err := jwt.GenerateToken(ctx, privateKey, i.Options.Issuer, subject, role) if err != nil { return errors.WithStack(err) } secret.StringData = map[string]string{ "token": token, } if secret.Annotations == nil { secret.Annotations = make(map[string]string) } secret.Annotations[annotationPublicKey] = publicKeyThumbprint logger.Info(ctx, "updating token secret") if _, err := client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil { return errors.WithStack(err) } } else { logger.Info(ctx, "key did not changed, doing nothing") } } return nil } func (i *Integration) getSharedPrivateKey(ctx context.Context, client *kubernetes.Clientset, namespace string, name string) (jwk.Key, error) { if namespace == "" { defaultNamespace, err := i.getCurrentNamespace() if err != nil { return nil, errors.WithStack(err) } namespace = defaultNamespace } ctx = logger.With(ctx, logger.F("secretNamespace", namespace), logger.F("secretName", name), ) logger.Debug(ctx, "searching shared private key from secret") secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil && !k8serr.IsNotFound(err) { return nil, errors.WithStack(err) } rawPrivateKey, exists := secret.Data["key"] if exists && len(rawPrivateKey) != 0 { key, err := jwk.ParseKey(rawPrivateKey) if err != nil { return nil, errors.WithStack(err) } return key, nil } localKey, err := integration.CtxPrivateKey(ctx) if err != nil { return nil, errors.WithStack(err) } rawLocalKey, err := json.Marshal(localKey) if err != nil { return nil, errors.WithStack(err) } secret = &v1.Secret{ Type: v1.SecretTypeOpaque, ObjectMeta: metav1.ObjectMeta{ Name: name, }, Data: map[string][]byte{ "key": rawLocalKey, }, } if _, err := client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil { return nil, errors.WithStack(err) } return localKey, nil } func (i *Integration) getCurrentNamespace() (string, error) { namespace, err := os.ReadFile(namespaceFile) if err != nil { return "", errors.Wrap(err, "could not retrieve current namespace") } return string(namespace), nil } func NewIntegration(funcs ...OptionFunc) *Integration { opts := NewOptions(funcs...) return &Integration{ Options: opts, } } var ( _ integration.OnStartup = &Integration{} _ integration.OnKeyLoad = &Integration{} )