195 lines
4.8 KiB
Go
195 lines
4.8 KiB
Go
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{}
|
|
)
|