package kubernetes import ( "context" "crypto" "fmt" "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 } // 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 { config, err := rest.InClusterConfig() if err != nil { return errors.WithStack(err) } client, err := kubernetes.NewForConfig(config) 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 } 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") key, err := jwk.LoadOrGenerate(i.Options.PrivateKey, jwk.DefaultKeySize) if err != nil { return errors.WithStack(err) } publicKey, err := key.PublicKey() if err != nil { return errors.WithStack(err) } publicKeyThumbprint, err := publicKey.Thumbprint(crypto.SHA256) if err != nil { return errors.WithStack(err) } publicKeyHash := fmt.Sprintf("%x", publicKeyThumbprint) 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) } } if !alreadyExists { token, err := jwt.GenerateToken(ctx, key, 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: publicKeyHash, }, }, 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 || publicKeyHash != existingPublicKeyHash { token, err := jwt.GenerateToken(ctx, key, 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] = publicKeyHash 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) 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{} )