Utilisation d'une clé privée partagée via un Secret
sur Kubernetes #19
|
@ -7,3 +7,6 @@
|
||||||
/bin
|
/bin
|
||||||
/.bouncer-token
|
/.bouncer-token
|
||||||
/.env
|
/.env
|
||||||
|
/misc/k8s
|
||||||
|
/misc/k6s
|
||||||
|
/misc/grafterm
|
1
go.mod
1
go.mod
|
@ -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
3
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -11,6 +11,10 @@ func NewDefaultIntegrationsConfig() IntegrationsConfig {
|
||||||
Kubernetes: KubernetesConfig{
|
Kubernetes: KubernetesConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
WriterTokenSecret: "",
|
WriterTokenSecret: "",
|
||||||
|
WriterTokenSecretNamespace: "",
|
||||||
|
ReaderTokenSecretNamespace: "",
|
||||||
|
PrivateKeySecret: "",
|
||||||
|
PrivateKeySecretNamespace: "",
|
||||||
ReaderTokenSecret: "",
|
ReaderTokenSecret: "",
|
||||||
LockTimeout: *NewInterpolatedDuration(30 * time.Second),
|
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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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{}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -25,6 +26,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"}
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue