Merge pull request 'Utilisation d'une clé privée partagée via un `Secret` sur Kubernetes' (#19) from kubernetes-private-key into develop
Cadoles/bouncer/pipeline/head This commit looks good Details

Reviewed-on: #19
This commit is contained in:
wpetit 2024-03-28 15:57:17 +01:00
commit a82fe46fa3
19 changed files with 351 additions and 83 deletions

View File

@ -6,4 +6,7 @@
/data /data
/bin /bin
/.bouncer-token /.bouncer-token
/.env /.env
/misc/k8s
/misc/k6s
/misc/grafterm

1
go.mod
View File

@ -14,6 +14,7 @@ require (
github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/chi/v5 v5.0.8
github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/jedib0t/go-pretty/v6 v6.4.6
github.com/mitchellh/mapstructure v1.4.1 github.com/mitchellh/mapstructure v1.4.1
github.com/oklog/ulid/v2 v2.1.0
github.com/ory/dockertest/v3 v3.10.0 github.com/ory/dockertest/v3 v3.10.0
github.com/prometheus/client_golang v1.16.0 github.com/prometheus/client_golang v1.16.0
github.com/qri-io/jsonschema v0.2.1 github.com/qri-io/jsonschema v0.2.1

3
go.sum
View File

@ -410,6 +410,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@ -434,6 +436,7 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@ -3,8 +3,11 @@ package admin
import ( import (
"context" "context"
"forge.cadoles.com/cadoles/bouncer/internal/integration"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"forge.cadoles.com/cadoles/bouncer/internal/setup" "forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
func (s *Server) initRepositories(ctx context.Context) error { func (s *Server) initRepositories(ctx context.Context) error {
@ -52,3 +55,34 @@ func (s *Server) initProxyRepository(ctx context.Context) error {
return nil return nil
} }
func (s *Server) initPrivateKey(ctx context.Context) error {
localKey, err := jwk.LoadOrGenerate(string(s.serverConfig.Auth.PrivateKey), jwk.DefaultKeySize)
if err != nil {
return errors.WithStack(err)
}
ctx = integration.WithPrivateKey(ctx, localKey)
key, err := integration.RunOnKeyLoad(ctx, s.integrations)
if err != nil {
return errors.WithStack(err)
}
if key != nil {
s.privateKey = key
} else {
s.privateKey = localKey
}
logger.Info(ctx, "using private key", logger.F("keyID", s.privateKey.KeyID()))
publicKeys, err := jwk.PublicKeySet(s.privateKey)
if err != nil {
return errors.WithStack(err)
}
s.publicKeys = publicKeys
return nil
}

View File

@ -35,6 +35,9 @@ type Server struct {
bootstrapConfig config.BootstrapConfig bootstrapConfig config.BootstrapConfig
proxyRepository store.ProxyRepository proxyRepository store.ProxyRepository
layerRepository store.LayerRepository layerRepository store.LayerRepository
privateKey jwk.Key
publicKeys jwk.Set
} }
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) { func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
@ -67,6 +70,15 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
return return
} }
if err := s.initPrivateKey(ctx); err != nil {
errs <- errors.WithStack(err)
return
}
ctx = integration.WithPrivateKey(ctx, s.privateKey)
ctx = integration.WithPublicKeySet(ctx, s.publicKeys)
if err := integration.RunOnStartup(ctx, s.integrations); err != nil { if err := integration.RunOnStartup(ctx, s.integrations); err != nil {
errs <- errors.WithStack(err) errs <- errors.WithStack(err)
@ -96,20 +108,6 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
} }
}() }()
key, err := jwk.LoadOrGenerate(string(s.serverConfig.Auth.PrivateKey), jwk.DefaultKeySize)
if err != nil {
errs <- errors.WithStack(err)
return
}
keys, err := jwk.PublicKeySet(key)
if err != nil {
errs <- errors.WithStack(err)
return
}
router := chi.NewRouter() router := chi.NewRouter()
if s.serverConfig.HTTP.UseRealIP { if s.serverConfig.HTTP.UseRealIP {
@ -160,7 +158,7 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
router.Route("/api/v1", func(r chi.Router) { router.Route("/api/v1", func(r chi.Router) {
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(auth.Middleware( r.Use(auth.Middleware(
jwt.NewAuthenticator(keys, string(s.serverConfig.Auth.Issuer), jwt.DefaultAcceptableSkew), jwt.NewAuthenticator(s.publicKeys, string(s.serverConfig.Auth.Issuer), jwt.DefaultAcceptableSkew),
)) ))
r.Route("/proxies", func(r chi.Router) { r.Route("/proxies", func(r chi.Router) {

View File

@ -5,9 +5,7 @@ import (
"strings" "strings"
"forge.cadoles.com/cadoles/bouncer/internal/admin" "forge.cadoles.com/cadoles/bouncer/internal/admin"
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
"forge.cadoles.com/cadoles/bouncer/internal/command/common" "forge.cadoles.com/cadoles/bouncer/internal/command/common"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"forge.cadoles.com/cadoles/bouncer/internal/setup" "forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -21,11 +19,6 @@ const (
func RunCommand() *cli.Command { func RunCommand() *cli.Command {
flags := append( flags := append(
common.Flags(), common.Flags(),
&cli.BoolFlag{
Name: flagPrintDefaultToken,
Usage: "Generate and print a default writer token in console at startup",
Value: true,
},
) )
return &cli.Command{ return &cli.Command{
@ -49,22 +42,6 @@ func RunCommand() *cli.Command {
defer flushSentry() defer flushSentry()
if printDefaultToken := ctx.Bool(flagPrintDefaultToken); printDefaultToken {
key, err := jwk.Generate(jwk.DefaultKeySize)
if err != nil {
return errors.Wrap(err, "could not generate default key")
}
token, err := jwt.GenerateToken(ctx.Context, key, string(conf.Admin.Auth.Issuer), "default-admin", jwt.Role(jwt.RoleWriter))
if err != nil {
return errors.WithStack(err)
}
logger.SetLevel(logger.LevelInfo)
logger.Info(ctx.Context, "default writer token", logger.F("token", token))
logger.SetLevel(logger.Level(conf.Logger.Level))
}
integrations, err := setup.SetupIntegrations(ctx.Context, conf) integrations, err := setup.SetupIntegrations(ctx.Context, conf)
if err != nil { if err != nil {
return errors.Wrap(err, "could not setup integrations") return errors.Wrap(err, "could not setup integrations")

View File

@ -9,10 +9,14 @@ type IntegrationsConfig struct {
func NewDefaultIntegrationsConfig() IntegrationsConfig { func NewDefaultIntegrationsConfig() IntegrationsConfig {
return IntegrationsConfig{ return IntegrationsConfig{
Kubernetes: KubernetesConfig{ Kubernetes: KubernetesConfig{
Enabled: false, Enabled: false,
WriterTokenSecret: "", WriterTokenSecret: "",
ReaderTokenSecret: "", WriterTokenSecretNamespace: "",
LockTimeout: *NewInterpolatedDuration(30 * time.Second), ReaderTokenSecretNamespace: "",
PrivateKeySecret: "",
PrivateKeySecretNamespace: "",
ReaderTokenSecret: "",
LockTimeout: *NewInterpolatedDuration(30 * time.Second),
}, },
} }
} }
@ -23,5 +27,7 @@ type KubernetesConfig struct {
WriterTokenSecretNamespace InterpolatedString `yaml:"writerTokenSecretNamespace"` WriterTokenSecretNamespace InterpolatedString `yaml:"writerTokenSecretNamespace"`
ReaderTokenSecret InterpolatedString `yaml:"readerTokenSecret"` ReaderTokenSecret InterpolatedString `yaml:"readerTokenSecret"`
ReaderTokenSecretNamespace InterpolatedString `yaml:"readerTokenSecretNamespace"` ReaderTokenSecretNamespace InterpolatedString `yaml:"readerTokenSecretNamespace"`
PrivateKeySecret InterpolatedString `yaml:"privateKeySecret"`
PrivateKeySecretNamespace InterpolatedString `yaml:"privateKeySecretNamespace"`
LockTimeout InterpolatedDuration `yaml:"lockTimeout"` LockTimeout InterpolatedDuration `yaml:"lockTimeout"`
} }

View File

@ -0,0 +1,49 @@
package integration
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"github.com/pkg/errors"
)
var (
ErrNotFound = errors.New("not found")
)
type contextKey string
const (
ctxPublicKeySet contextKey = "public-key-set"
ctxPrivateKey contextKey = "private-key"
)
func CtxPublicKeySet(ctx context.Context) (jwk.Set, error) {
return ctxValue[jwk.Set](ctx, ctxPublicKeySet)
}
func WithPublicKeySet(ctx context.Context, set jwk.Set) context.Context {
return context.WithValue(ctx, ctxPublicKeySet, set)
}
func CtxPrivateKey(ctx context.Context) (jwk.Key, error) {
return ctxValue[jwk.Key](ctx, ctxPrivateKey)
}
func WithPrivateKey(ctx context.Context, key jwk.Key) context.Context {
return context.WithValue(ctx, ctxPrivateKey, key)
}
func ctxValue[T any](ctx context.Context, key contextKey) (T, error) {
raw := ctx.Value(key)
if raw == nil {
return *new(T), errors.WithStack(ErrNotFound)
}
value, ok := raw.(T)
if !ok {
return *new(T), errors.Errorf("unexpected value type '%T'", raw)
}
return value, nil
}

View File

@ -3,6 +3,7 @@ package integration
import ( import (
"context" "context"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -15,6 +16,11 @@ type OnStartup interface {
OnStartup(ctx context.Context) error OnStartup(ctx context.Context) error
} }
type OnKeyLoad interface {
Integration
OnKeyLoad(ctx context.Context) (jwk.Key, error)
}
func RunOnStartup(ctx context.Context, integrations []Integration) error { func RunOnStartup(ctx context.Context, integrations []Integration) error {
for _, it := range integrations { for _, it := range integrations {
onStartup, ok := it.(OnStartup) onStartup, ok := it.(OnStartup)
@ -29,3 +35,23 @@ func RunOnStartup(ctx context.Context, integrations []Integration) error {
return nil return nil
} }
func RunOnKeyLoad(ctx context.Context, integrations []Integration) (jwk.Key, error) {
for _, it := range integrations {
onKeyLoad, ok := it.(OnKeyLoad)
if !ok {
continue
}
key, err := onKeyLoad.OnKeyLoad(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
if key != nil {
return key, nil
}
}
return nil, nil
}

View File

@ -2,8 +2,7 @@ package kubernetes
import ( import (
"context" "context"
"crypto" "encoding/json"
"fmt"
"os" "os"
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt" "forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
@ -28,6 +27,38 @@ type Integration struct {
Options *Options 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. // Integration implements integration.OnStartup.
func (i *Integration) Integration() {} func (i *Integration) Integration() {}
@ -36,12 +67,7 @@ func (i *Integration) OnStartup(ctx context.Context) error {
locker := i.Options.Locker locker := i.Options.Locker
timeout := i.Options.LockTimeout timeout := i.Options.LockTimeout
err := locker.WithLock(ctx, "bouncer-kubernetes-onstartup", timeout, func(ctx context.Context) error { err := locker.WithLock(ctx, "bouncer-kubernetes-onstartup", timeout, func(ctx context.Context) error {
config, err := rest.InClusterConfig() client, err := i.getClient()
if err != nil {
return errors.WithStack(err)
}
client, err := kubernetes.NewForConfig(config)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -67,6 +93,20 @@ func (i *Integration) OnStartup(ctx context.Context) error {
return nil 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 ( const (
annotationPublicKey = "bouncer.cadoles.com/public-key" annotationPublicKey = "bouncer.cadoles.com/public-key"
) )
@ -90,23 +130,6 @@ func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.
logger.Debug(ctx, "generating new token") 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 alreadyExists := true
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil { if err != nil {
@ -117,8 +140,23 @@ func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.
} }
} }
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 { if !alreadyExists {
token, err := jwt.GenerateToken(ctx, key, i.Options.Issuer, subject, role) token, err := jwt.GenerateToken(ctx, privateKey, i.Options.Issuer, subject, role)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -128,7 +166,7 @@ func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Annotations: map[string]string{ Annotations: map[string]string{
annotationPublicKey: publicKeyHash, annotationPublicKey: publicKeyThumbprint,
}, },
}, },
StringData: map[string]string{ StringData: map[string]string{
@ -143,8 +181,8 @@ func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.
} }
} else { } else {
existingPublicKeyHash, exists := secret.Annotations[annotationPublicKey] existingPublicKeyHash, exists := secret.Annotations[annotationPublicKey]
if !exists || publicKeyHash != existingPublicKeyHash { if !exists || publicKeyThumbprint != existingPublicKeyHash {
token, err := jwt.GenerateToken(ctx, key, i.Options.Issuer, subject, role) token, err := jwt.GenerateToken(ctx, privateKey, i.Options.Issuer, subject, role)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -157,7 +195,7 @@ func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.
secret.Annotations = make(map[string]string) secret.Annotations = make(map[string]string)
} }
secret.Annotations[annotationPublicKey] = publicKeyHash secret.Annotations[annotationPublicKey] = publicKeyThumbprint
logger.Info(ctx, "updating token secret") logger.Info(ctx, "updating token secret")
@ -172,6 +210,66 @@ func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.
return nil 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) { func (i *Integration) getCurrentNamespace() (string, error) {
namespace, err := os.ReadFile(namespaceFile) namespace, err := os.ReadFile(namespaceFile)
if err != nil { if err != nil {
@ -191,4 +289,5 @@ func NewIntegration(funcs ...OptionFunc) *Integration {
var ( var (
_ integration.OnStartup = &Integration{} _ integration.OnStartup = &Integration{}
_ integration.OnKeyLoad = &Integration{}
) )

View File

@ -0,0 +1,41 @@
package kubernetes
import (
"bytes"
"crypto"
"crypto/sha256"
"fmt"
"slices"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"github.com/pkg/errors"
)
func getKeySetThumbprint(set jwk.Set) (string, error) {
data := make([][]byte, 0, set.Len())
for i := 0; i < set.Len(); i++ {
key, exists := set.Key(i)
if !exists {
continue
}
thumbprint, err := key.Thumbprint(crypto.SHA256)
if err != nil {
return "", errors.WithStack(err)
}
data = append(data, thumbprint)
}
slices.SortFunc(data, bytes.Compare)
hash := sha256.New()
for _, d := range data {
if _, err := hash.Write(d); err != nil {
return "", errors.WithStack(err)
}
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}

View File

@ -12,7 +12,8 @@ type Options struct {
WriterTokenSecretNamespace string WriterTokenSecretNamespace string
ReaderTokenSecret string ReaderTokenSecret string
ReaderTokenSecretNamespace string ReaderTokenSecretNamespace string
PrivateKey string PrivateKeySecret string
PrivateKeySecretNamespace string
Issuer string Issuer string
Locker lock.Locker Locker lock.Locker
LockTimeout time.Duration LockTimeout time.Duration
@ -26,7 +27,8 @@ func NewOptions(funcs ...OptionFunc) *Options {
WriterTokenSecretNamespace: "", WriterTokenSecretNamespace: "",
ReaderTokenSecret: "", ReaderTokenSecret: "",
ReaderTokenSecretNamespace: "", ReaderTokenSecretNamespace: "",
PrivateKey: "", PrivateKeySecret: "",
PrivateKeySecretNamespace: "",
Issuer: "", Issuer: "",
Locker: memory.NewLocker(), Locker: memory.NewLocker(),
LockTimeout: 30 * time.Second, LockTimeout: 30 * time.Second,
@ -62,9 +64,15 @@ func WithReaderTokenSecretNamespace(namespace string) OptionFunc {
} }
} }
func WithPrivateKey(privateKeyFile string) OptionFunc { func WithPrivateKeySecret(secretName string) OptionFunc {
return func(opts *Options) { return func(opts *Options) {
opts.PrivateKey = privateKeyFile opts.PrivateKeySecret = secretName
}
}
func WithPrivateKeySecretNamespace(namespace string) OptionFunc {
return func(opts *Options) {
opts.PrivateKeySecretNamespace = namespace
} }
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jws"
"github.com/oklog/ulid/v2"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -23,8 +24,9 @@ type (
) )
var ( var (
FromRaw = jwk.FromRaw FromRaw = jwk.FromRaw
NewSet = jwk.NewSet NewSet = jwk.NewSet
ParseKey = jwk.ParseKey
) )
const AlgorithmKey = jwk.AlgorithmKey const AlgorithmKey = jwk.AlgorithmKey
@ -95,6 +97,12 @@ func Generate(size int) (jwk.Key, error) {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
keyID := ulid.Make().String()
if err := key.Set(jwk.KeyIDKey, keyID); err != nil {
return nil, errors.WithStack(err)
}
return key, nil return key, nil
} }

View File

@ -35,8 +35,9 @@ func setupKubernetesIntegration(ctx context.Context, conf *config.Config) (*kube
kubernetes.WithReaderTokenSecretNamespace(string(conf.Integrations.Kubernetes.ReaderTokenSecretNamespace)), kubernetes.WithReaderTokenSecretNamespace(string(conf.Integrations.Kubernetes.ReaderTokenSecretNamespace)),
kubernetes.WithWriterTokenSecret(string(conf.Integrations.Kubernetes.WriterTokenSecret)), kubernetes.WithWriterTokenSecret(string(conf.Integrations.Kubernetes.WriterTokenSecret)),
kubernetes.WithWriterTokenSecretNamespace(string(conf.Integrations.Kubernetes.WriterTokenSecretNamespace)), kubernetes.WithWriterTokenSecretNamespace(string(conf.Integrations.Kubernetes.WriterTokenSecretNamespace)),
kubernetes.WithPrivateKeySecret(string(conf.Integrations.Kubernetes.PrivateKeySecret)),
kubernetes.WithPrivateKeySecretNamespace(string(conf.Integrations.Kubernetes.PrivateKeySecretNamespace)),
kubernetes.WithIssuer(string(conf.Admin.Auth.Issuer)), kubernetes.WithIssuer(string(conf.Admin.Auth.Issuer)),
kubernetes.WithPrivateKey(string(conf.Admin.Auth.PrivateKey)),
kubernetes.WithLocker(locker), kubernetes.WithLocker(locker),
kubernetes.WithLockTimeout(time.Duration(conf.Integrations.Kubernetes.LockTimeout)), kubernetes.WithLockTimeout(time.Duration(conf.Integrations.Kubernetes.LockTimeout)),
) )

View File

@ -1 +0,0 @@
{"d":"JuBw5OsGv3rPgVczxUgtJ6iUQ41LQu4Xpu-t8IKI_z8r-BZBlbndxidPmRlGZASLGL3rhY4qw6_ScFxakrMpCreO1RMU0kqtz--N48BXFnW5tEgr1voyyKP__bPssQNn6PgkoyAd11es7MEKlBff_DtGrcSkVRgU0zDZB-vIU0aNEIZPNw0icbYqc1u_QQNPpBU9cw6P33WHhzvfCVAkZKRszwznhiPM08n1vjpiA7e1kQ8a6OC4IFZBvohkmpmyOq1g1OLRABQ83YPCjGjCAejO-jEWkbLksp6rAl_YYpCvfBAjFV76JuZq4eh5IU82LsSfi3PGYBkhxWuLY779XQ","dp":"gljHOQowGK7fVn2DJizWtgRIDJuKpKnoX2PWNJUbm2WZwcEPZalAkxn7Y-w_reLVJZuRpfKEUMS-Tn3-CwI1ZjCHPqMPTXcoG0Pe2E-Z88jOs9lW4XSOASiiM980VIvkV1xCxDJkN3NsDFQ9j9kRGnKuMnsucCW3AKaU917hXNU","dq":"mqY19JcEBDnzS70_XkAsOKqPzemOScax66b-4N6zrsgeLVlRjHffY9uCAgBWzlxOidRdQN8q23ZJB4fqsKB2w00Iw7Jxx94IoAKGjKDT5iB48Y_kdKLAwSHRTXsqA9GG3po_H_JpP_EqX4TDBYtqQZuBD_tACP9HbLYMi_V2YU8","e":"AQAB","kty":"RSA","n":"sam0X0BGcuFwX8z3Wde8cv2o_zl6A9ghpkT0tCjw8qH3GNWrbAqzncSWdHBzoChBgAbuTOVs-ixYC0KeUhwFdc8Ul-jmKJWFaS8kIr3y4EH62-vLgMuIKfaxbsyUG6KMkJfnftge1jPO4ccddNej9msxcqTxu37dcgstutwtd6QkS9p5RrNbDBc8-Z7SQ4TuxJfP8msXRnCPJ-I44yszGdQF1Np2DXakJHVn8PBrDh3iSFwORw8jxNS4oS0OlBl5aSc0t5XkkaNcSU2a50SKts290w54fl6MPJ1sLnnznLy4uu37-nrfEUvqRLDZL9B1F82RM1dtLIIiN4gnSrMlpQ","p":"wOmFPhAT_wXWzMuwtEdYIer3-CiOWxFKpFL09eEJkJ29MIUchEaoiJaUAghqPxM48llfOVaUaLbFVxmo5U3fyjNMaP-nHMUBwojutykMK-gC2R3J4bQgFWfKbGSL7M7UsextAvpq9iiOuR0LNE-xTfCgPIxHVdPZskO3yx0DkjM","q":"68OGRb0tLRjb_PpkGctcSjEz_vvcyjzxGL-fn4_h4GCw98Xrj6Y4rZ4lfWWRSeDohSvdd-ICSlxvxkQOIOcA0H7jyJcBC0KDs4hX5BRGJNDri3QX0ry4_F1ptAdbfiFgQGqCfMRCr7L60Tfd_6tLczvny7eEBKQNGdj6dLfhgMc","qi":"DFwixyxUDf0REPLLa8hOKieRL95_AH9rbYWzStBOdSjKWra5l0reD6a4bbvAYvl0e8qCcRI6S8Nzpz0BYm4sJL7poVOnjxqvBY3Q9Ppf4Mq8lW39pOCJcqOHIvvYHsMjTC5uwp7Yg2p0GvxuUibbyNL1PXf6WZ_szVP_oSMrCXA"}

View File

@ -20,7 +20,7 @@ admin:
debug: false debug: false
auth: auth:
issuer: http://127.0.0.1:8081 issuer: http://127.0.0.1:8081
privateKey: /etc/bouncer/admin-key.json privateKey: /var/lib/bouncer/admin-key.json
metrics: metrics:
enabled: true enabled: true
endpoint: /.bouncer/metrics endpoint: /.bouncer/metrics
@ -44,3 +44,4 @@ integrations:
enabled: true enabled: true
writerTokenSecret: ${BOUNCER_WRITER_TOKEN_SECRET} writerTokenSecret: ${BOUNCER_WRITER_TOKEN_SECRET}
readerTokenSecret: ${BOUNCER_READER_TOKEN_SECRET} readerTokenSecret: ${BOUNCER_READER_TOKEN_SECRET}
privateKeySecret: ${BOUNCER_PRIVATE_KEY_SECRET}

View File

@ -10,10 +10,10 @@ configMapGenerator:
- name: bouncer-admin-config - name: bouncer-admin-config
files: files:
- ./files/config.yml - ./files/config.yml
- ./files/admin-key.json
- name: bouncer-admin-bootstrap - name: bouncer-admin-bootstrap
- name: bouncer-admin-env - name: bouncer-admin-env
literals: literals:
- BOUNCER_LOG_LEVEL=2 - BOUNCER_LOG_LEVEL=2
- BOUNCER_WRITER_TOKEN_SECRET=bouncer-admin-writer-token - BOUNCER_WRITER_TOKEN_SECRET=bouncer-admin-writer-token
- BOUNCER_READER_TOKEN_SECRET=bouncer-admin-reader-token - BOUNCER_READER_TOKEN_SECRET=bouncer-admin-reader-token
- BOUNCER_PRIVATE_KEY_SECRET=bouncer-admin-private-key

View File

@ -23,6 +23,10 @@ spec:
containers: containers:
- name: bouncer-admin - name: bouncer-admin
image: bouncer image: bouncer
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
command: command:
[ [
"bouncer", "bouncer",
@ -46,6 +50,8 @@ spec:
name: bouncer-admin-config name: bouncer-admin-config
- mountPath: /etc/bouncer/bootstrap.d - mountPath: /etc/bouncer/bootstrap.d
name: bouncer-admin-bootstrap name: bouncer-admin-bootstrap
- mountPath: /var/lib/bouncer
name: bouncer-admin-var
volumes: volumes:
- name: bouncer-admin-config - name: bouncer-admin-config
configMap: configMap:
@ -53,3 +59,7 @@ spec:
- name: bouncer-admin-bootstrap - name: bouncer-admin-bootstrap
configMap: configMap:
name: bouncer-admin-bootstrap name: bouncer-admin-bootstrap
- name: bouncer-admin-var
emptyDir:
sizeLimit: 10Mi
medium: Memory

View File

@ -21,6 +21,10 @@ spec:
containers: containers:
- name: bouncer-server - name: bouncer-server
image: bouncer image: bouncer
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
command: command:
[ [
"bouncer", "bouncer",