294 lines
7.1 KiB
Go
294 lines
7.1 KiB
Go
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{}
|
|
)
|