mirror of
https://github.com/optim-enterprises-bv/kubernetes.git
synced 2025-11-02 03:08:15 +00:00
Add plugin and key-cache for ExternalJWTSigner integration
This commit is contained in:
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -45,27 +46,27 @@ type CompletedOptions struct {
|
|||||||
|
|
||||||
// Complete set default ServerRunOptions.
|
// Complete set default ServerRunOptions.
|
||||||
// Should be called after kube-apiserver flags parsed.
|
// Should be called after kube-apiserver flags parsed.
|
||||||
func (opts *ServerRunOptions) Complete() (CompletedOptions, error) {
|
func (s *ServerRunOptions) Complete(ctx context.Context) (CompletedOptions, error) {
|
||||||
if opts == nil {
|
if s == nil {
|
||||||
return CompletedOptions{completedOptions: &completedOptions{}}, nil
|
return CompletedOptions{completedOptions: &completedOptions{}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// process opts.ServiceClusterIPRange from list to Primary and Secondary
|
// process s.ServiceClusterIPRange from list to Primary and Secondary
|
||||||
// we process secondary only if provided by user
|
// we process secondary only if provided by user
|
||||||
apiServerServiceIP, primaryServiceIPRange, secondaryServiceIPRange, err := getServiceIPAndRanges(opts.ServiceClusterIPRanges)
|
apiServerServiceIP, primaryServiceIPRange, secondaryServiceIPRange, err := getServiceIPAndRanges(s.ServiceClusterIPRanges)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CompletedOptions{}, err
|
return CompletedOptions{}, err
|
||||||
}
|
}
|
||||||
controlplane, err := opts.Options.Complete([]string{"kubernetes.default.svc", "kubernetes.default", "kubernetes"}, []net.IP{apiServerServiceIP})
|
controlplane, err := s.Options.Complete(ctx, []string{"kubernetes.default.svc", "kubernetes.default", "kubernetes"}, []net.IP{apiServerServiceIP})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CompletedOptions{}, err
|
return CompletedOptions{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
completed := completedOptions{
|
completed := completedOptions{
|
||||||
CompletedOptions: controlplane,
|
CompletedOptions: controlplane,
|
||||||
CloudProvider: opts.CloudProvider,
|
CloudProvider: s.CloudProvider,
|
||||||
|
|
||||||
Extra: opts.Extra,
|
Extra: s.Extra,
|
||||||
}
|
}
|
||||||
|
|
||||||
completed.PrimaryServiceClusterIPRange = primaryServiceIPRange
|
completed.PrimaryServiceClusterIPRange = primaryServiceIPRange
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ func NewAPIServerCommand() *cobra.Command {
|
|||||||
_, featureGate := featuregate.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister(
|
_, featureGate := featuregate.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister(
|
||||||
featuregate.DefaultKubeComponent, utilversion.DefaultBuildEffectiveVersion(), utilfeature.DefaultMutableFeatureGate)
|
featuregate.DefaultKubeComponent, utilversion.DefaultBuildEffectiveVersion(), utilfeature.DefaultMutableFeatureGate)
|
||||||
s := options.NewServerRunOptions()
|
s := options.NewServerRunOptions()
|
||||||
|
ctx := genericapiserver.SetupSignalContext()
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "kube-apiserver",
|
Use: "kube-apiserver",
|
||||||
@@ -97,7 +98,7 @@ cluster's shared state through which all other components interact.`,
|
|||||||
cliflag.PrintFlags(fs)
|
cliflag.PrintFlags(fs)
|
||||||
|
|
||||||
// set default options
|
// set default options
|
||||||
completedOptions, err := s.Complete()
|
completedOptions, err := s.Complete(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ cluster's shared state through which all other components interact.`,
|
|||||||
}
|
}
|
||||||
// add feature enablement metrics
|
// add feature enablement metrics
|
||||||
featureGate.AddMetrics()
|
featureGate.AddMetrics()
|
||||||
return Run(cmd.Context(), completedOptions)
|
return Run(ctx, completedOptions)
|
||||||
},
|
},
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
@@ -119,7 +120,7 @@ cluster's shared state through which all other components interact.`,
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.SetContext(genericapiserver.SetupSignalContext())
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
fs := cmd.Flags()
|
fs := cmd.Flags()
|
||||||
namedFlagSets := s.Flags()
|
namedFlagSets := s.Flags()
|
||||||
|
|||||||
@@ -390,7 +390,7 @@ func StartTestServer(t ktesting.TB, instanceOptions *TestServerInstanceOptions,
|
|||||||
s.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"}
|
s.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"}
|
||||||
s.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()}
|
s.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()}
|
||||||
|
|
||||||
completedOptions, err := s.Complete()
|
completedOptions, err := s.Complete(tCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("failed to set default ServerRunOptions: %v", err)
|
return result, fmt.Errorf("failed to set default ServerRunOptions: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -103,6 +103,7 @@ require (
|
|||||||
k8s.io/csi-translation-lib v0.0.0
|
k8s.io/csi-translation-lib v0.0.0
|
||||||
k8s.io/dynamic-resource-allocation v0.0.0
|
k8s.io/dynamic-resource-allocation v0.0.0
|
||||||
k8s.io/endpointslice v0.0.0
|
k8s.io/endpointslice v0.0.0
|
||||||
|
k8s.io/externaljwt v0.0.0
|
||||||
k8s.io/klog/v2 v2.130.1
|
k8s.io/klog/v2 v2.130.1
|
||||||
k8s.io/kms v0.0.0
|
k8s.io/kms v0.0.0
|
||||||
k8s.io/kube-aggregator v0.0.0
|
k8s.io/kube-aggregator v0.0.0
|
||||||
@@ -239,6 +240,7 @@ replace (
|
|||||||
k8s.io/csi-translation-lib => ./staging/src/k8s.io/csi-translation-lib
|
k8s.io/csi-translation-lib => ./staging/src/k8s.io/csi-translation-lib
|
||||||
k8s.io/dynamic-resource-allocation => ./staging/src/k8s.io/dynamic-resource-allocation
|
k8s.io/dynamic-resource-allocation => ./staging/src/k8s.io/dynamic-resource-allocation
|
||||||
k8s.io/endpointslice => ./staging/src/k8s.io/endpointslice
|
k8s.io/endpointslice => ./staging/src/k8s.io/endpointslice
|
||||||
|
k8s.io/externaljwt => ./staging/src/k8s.io/externaljwt
|
||||||
k8s.io/kms => ./staging/src/k8s.io/kms
|
k8s.io/kms => ./staging/src/k8s.io/kms
|
||||||
k8s.io/kube-aggregator => ./staging/src/k8s.io/kube-aggregator
|
k8s.io/kube-aggregator => ./staging/src/k8s.io/kube-aggregator
|
||||||
k8s.io/kube-controller-manager => ./staging/src/k8s.io/kube-controller-manager
|
k8s.io/kube-controller-manager => ./staging/src/k8s.io/kube-controller-manager
|
||||||
|
|||||||
1
go.work
1
go.work
@@ -23,6 +23,7 @@ use (
|
|||||||
./staging/src/k8s.io/csi-translation-lib
|
./staging/src/k8s.io/csi-translation-lib
|
||||||
./staging/src/k8s.io/dynamic-resource-allocation
|
./staging/src/k8s.io/dynamic-resource-allocation
|
||||||
./staging/src/k8s.io/endpointslice
|
./staging/src/k8s.io/endpointslice
|
||||||
|
./staging/src/k8s.io/externaljwt
|
||||||
./staging/src/k8s.io/kms
|
./staging/src/k8s.io/kms
|
||||||
./staging/src/k8s.io/kube-aggregator
|
./staging/src/k8s.io/kube-aggregator
|
||||||
./staging/src/k8s.io/kube-controller-manager
|
./staging/src/k8s.io/kube-controller-manager
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
"k8s.io/client-go",
|
"k8s.io/client-go",
|
||||||
"k8s.io/code-generator",
|
"k8s.io/code-generator",
|
||||||
"k8s.io/cri-api",
|
"k8s.io/cri-api",
|
||||||
|
"k8s.io/externaljwt",
|
||||||
"k8s.io/kms",
|
"k8s.io/kms",
|
||||||
"k8s.io/kube-aggregator",
|
"k8s.io/kube-aggregator",
|
||||||
"k8s.io/kubelet",
|
"k8s.io/kubelet",
|
||||||
|
|||||||
@@ -785,6 +785,8 @@ function codegen::protobindings() {
|
|||||||
|
|
||||||
"staging/src/k8s.io/kubelet/pkg/apis/pluginregistration"
|
"staging/src/k8s.io/kubelet/pkg/apis/pluginregistration"
|
||||||
"pkg/kubelet/pluginmanager/pluginwatcher/example_plugin_apis"
|
"pkg/kubelet/pluginmanager/pluginwatcher/example_plugin_apis"
|
||||||
|
|
||||||
|
"staging/src/k8s.io/externaljwt/apis"
|
||||||
)
|
)
|
||||||
|
|
||||||
kube::log::status "Generating protobuf bindings for ${#apis[@]} targets"
|
kube::log::status "Generating protobuf bindings for ${#apis[@]} targets"
|
||||||
|
|||||||
@@ -19,19 +19,18 @@ limitations under the License.
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/kubernetes/pkg/apis/authentication"
|
"k8s.io/kubernetes/pkg/apis/authentication"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MinTokenAgeSec = 10 * 60 // 10 minutes
|
||||||
|
|
||||||
// ValidateTokenRequest validates a TokenRequest.
|
// ValidateTokenRequest validates a TokenRequest.
|
||||||
func ValidateTokenRequest(tr *authentication.TokenRequest) field.ErrorList {
|
func ValidateTokenRequest(tr *authentication.TokenRequest) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
specPath := field.NewPath("spec")
|
specPath := field.NewPath("spec")
|
||||||
|
|
||||||
const min = 10 * time.Minute
|
if tr.Spec.ExpirationSeconds < MinTokenAgeSec {
|
||||||
if tr.Spec.ExpirationSeconds < int64(min.Seconds()) {
|
|
||||||
allErrs = append(allErrs, field.Invalid(specPath.Child("expirationSeconds"), tr.Spec.ExpirationSeconds, "may not specify a duration less than 10 minutes"))
|
allErrs = append(allErrs, field.Invalid(specPath.Child("expirationSeconds"), tr.Spec.ExpirationSeconds, "may not specify a duration less than 10 minutes"))
|
||||||
}
|
}
|
||||||
if tr.Spec.ExpirationSeconds > 1<<32 {
|
if tr.Spec.ExpirationSeconds > 1<<32 {
|
||||||
|
|||||||
@@ -409,7 +409,9 @@ func (e *TokensController) generateTokenIfNeeded(logger klog.Logger, serviceAcco
|
|||||||
|
|
||||||
// Generate the token
|
// Generate the token
|
||||||
if needsToken {
|
if needsToken {
|
||||||
token, err := e.token.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *liveSecret))
|
c, pc := serviceaccount.LegacyClaims(*serviceAccount, *liveSecret)
|
||||||
|
// TODO: need to plumb context if using external signer ever becomes a posibility.
|
||||||
|
token, err := e.token.GenerateToken(context.TODO(), c, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package serviceaccount
|
package serviceaccount
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -40,7 +41,7 @@ type testGenerator struct {
|
|||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testGenerator) GenerateToken(sc *jwt.Claims, pc interface{}) (string, error) {
|
func (t *testGenerator) GenerateToken(ctx context.Context, sc *jwt.Claims, pc interface{}) (string, error) {
|
||||||
return t.Token, t.Err
|
return t.Token, t.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ func (c *CompletedConfig) NewCoreGenericConfig() *corerest.GenericConfig {
|
|||||||
LoopbackClientConfig: c.Generic.LoopbackClientConfig,
|
LoopbackClientConfig: c.Generic.LoopbackClientConfig,
|
||||||
ServiceAccountIssuer: c.Extra.ServiceAccountIssuer,
|
ServiceAccountIssuer: c.Extra.ServiceAccountIssuer,
|
||||||
ExtendExpiration: c.Extra.ExtendExpiration,
|
ExtendExpiration: c.Extra.ExtendExpiration,
|
||||||
|
IsTokenSignerExternal: c.Extra.IsTokenSignerExternal,
|
||||||
ServiceAccountMaxExpiration: c.Extra.ServiceAccountMaxExpiration,
|
ServiceAccountMaxExpiration: c.Extra.ServiceAccountMaxExpiration,
|
||||||
APIAudiences: c.Generic.Authentication.APIAudiences,
|
APIAudiences: c.Generic.Authentication.APIAudiences,
|
||||||
Informers: c.Extra.VersionedInformers,
|
Informers: c.Extra.VersionedInformers,
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ type Extra struct {
|
|||||||
ServiceAccountIssuer serviceaccount.TokenGenerator
|
ServiceAccountIssuer serviceaccount.TokenGenerator
|
||||||
ServiceAccountMaxExpiration time.Duration
|
ServiceAccountMaxExpiration time.Duration
|
||||||
ExtendExpiration bool
|
ExtendExpiration bool
|
||||||
|
IsTokenSignerExternal bool
|
||||||
|
|
||||||
// ServiceAccountIssuerDiscovery
|
// ServiceAccountIssuerDiscovery
|
||||||
ServiceAccountIssuerURL string
|
ServiceAccountIssuerURL string
|
||||||
@@ -300,6 +301,7 @@ func CreateConfig(
|
|||||||
ServiceAccountIssuer: opts.ServiceAccountIssuer,
|
ServiceAccountIssuer: opts.ServiceAccountIssuer,
|
||||||
ServiceAccountMaxExpiration: opts.ServiceAccountTokenMaxExpiration,
|
ServiceAccountMaxExpiration: opts.ServiceAccountTokenMaxExpiration,
|
||||||
ExtendExpiration: opts.Authentication.ServiceAccounts.ExtendExpiration,
|
ExtendExpiration: opts.Authentication.ServiceAccounts.ExtendExpiration,
|
||||||
|
IsTokenSignerExternal: opts.Authentication.ServiceAccounts.IsTokenSignerExternal,
|
||||||
|
|
||||||
VersionedInformers: versionedInformers,
|
VersionedInformers: versionedInformers,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ func TestBuildGenericConfig(t *testing.T) {
|
|||||||
s.BindPort = ln.Addr().(*net.TCPAddr).Port
|
s.BindPort = ln.Addr().(*net.TCPAddr).Port
|
||||||
opts.SecureServing = s
|
opts.SecureServing = s
|
||||||
|
|
||||||
completedOptions, err := opts.Complete(nil, nil)
|
completedOptions, err := opts.Complete(context.TODO(), nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to complete apiserver options: %v", err)
|
t.Fatalf("Failed to complete apiserver options: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ limitations under the License.
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -36,9 +37,11 @@ import (
|
|||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
netutil "k8s.io/utils/net"
|
netutil "k8s.io/utils/net"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/apis/authentication/validation"
|
||||||
_ "k8s.io/kubernetes/pkg/features"
|
_ "k8s.io/kubernetes/pkg/features"
|
||||||
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
|
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
|
||||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
|
"k8s.io/kubernetes/pkg/serviceaccount/externaljwt/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options define the flags and validation for a generic controlplane. If the
|
// Options define the flags and validation for a generic controlplane. If the
|
||||||
@@ -85,6 +88,8 @@ type Options struct {
|
|||||||
ShowHiddenMetricsForVersion string
|
ShowHiddenMetricsForVersion string
|
||||||
|
|
||||||
SystemNamespaces []string
|
SystemNamespaces []string
|
||||||
|
|
||||||
|
ServiceAccountSigningEndpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
// completedServerRunOptions is a private wrapper that enforces a call of Complete() before Run can be invoked.
|
// completedServerRunOptions is a private wrapper that enforces a call of Complete() before Run can be invoked.
|
||||||
@@ -191,9 +196,12 @@ func (s *Options) AddFlags(fss *cliflag.NamedFlagSets) {
|
|||||||
|
|
||||||
fs.StringVar(&s.ServiceAccountSigningKeyFile, "service-account-signing-key-file", s.ServiceAccountSigningKeyFile, ""+
|
fs.StringVar(&s.ServiceAccountSigningKeyFile, "service-account-signing-key-file", s.ServiceAccountSigningKeyFile, ""+
|
||||||
"Path to the file that contains the current private key of the service account token issuer. The issuer will sign issued ID tokens with this private key.")
|
"Path to the file that contains the current private key of the service account token issuer. The issuer will sign issued ID tokens with this private key.")
|
||||||
|
|
||||||
|
fs.StringVar(&s.ServiceAccountSigningEndpoint, "service-account-signing-endpoint", s.ServiceAccountSigningEndpoint, ""+
|
||||||
|
"Path to socket where a external JWT signer is listening. This flag is mutually exclusive with --service-account-signing-key-file and --service-account-key-file. Requires enabling feature gate (ExternalServiceAccountTokenSigner)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) Complete(alternateDNS []string, alternateIPs []net.IP) (CompletedOptions, error) {
|
func (o *Options) Complete(ctx context.Context, alternateDNS []string, alternateIPs []net.IP) (CompletedOptions, error) {
|
||||||
if o == nil {
|
if o == nil {
|
||||||
return CompletedOptions{completedOptions: &completedOptions{}}, nil
|
return CompletedOptions{completedOptions: &completedOptions{}}, nil
|
||||||
}
|
}
|
||||||
@@ -233,36 +241,9 @@ func (o *Options) Complete(alternateDNS []string, alternateIPs []net.IP) (Comple
|
|||||||
// adjust authentication for completed authorization
|
// adjust authentication for completed authorization
|
||||||
completed.Authentication.ApplyAuthorization(completed.Authorization)
|
completed.Authentication.ApplyAuthorization(completed.Authorization)
|
||||||
|
|
||||||
// verify and adjust ServiceAccountTokenMaxExpiration
|
err := o.completeServiceAccountOptions(ctx, &completed)
|
||||||
if completed.Authentication.ServiceAccounts.MaxExpiration != 0 {
|
|
||||||
lowBound := time.Hour
|
|
||||||
upBound := time.Duration(1<<32) * time.Second
|
|
||||||
if completed.Authentication.ServiceAccounts.MaxExpiration < lowBound ||
|
|
||||||
completed.Authentication.ServiceAccounts.MaxExpiration > upBound {
|
|
||||||
return CompletedOptions{}, fmt.Errorf("the service-account-max-token-expiration must be between 1 hour and 2^32 seconds")
|
|
||||||
}
|
|
||||||
if completed.Authentication.ServiceAccounts.ExtendExpiration {
|
|
||||||
if completed.Authentication.ServiceAccounts.MaxExpiration < serviceaccount.WarnOnlyBoundTokenExpirationSeconds*time.Second {
|
|
||||||
klog.Warningf("service-account-extend-token-expiration is true, in order to correctly trigger safe transition logic, service-account-max-token-expiration must be set longer than %d seconds (currently %s)", serviceaccount.WarnOnlyBoundTokenExpirationSeconds, completed.Authentication.ServiceAccounts.MaxExpiration)
|
|
||||||
}
|
|
||||||
if completed.Authentication.ServiceAccounts.MaxExpiration < serviceaccount.ExpirationExtensionSeconds*time.Second {
|
|
||||||
klog.Warningf("service-account-extend-token-expiration is true, enabling tokens valid up to %d seconds, which is longer than service-account-max-token-expiration set to %s seconds", serviceaccount.ExpirationExtensionSeconds, completed.Authentication.ServiceAccounts.MaxExpiration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completed.ServiceAccountTokenMaxExpiration = completed.Authentication.ServiceAccounts.MaxExpiration
|
|
||||||
|
|
||||||
if len(completed.Authentication.ServiceAccounts.Issuers) != 0 && completed.Authentication.ServiceAccounts.Issuers[0] != "" {
|
|
||||||
if completed.ServiceAccountSigningKeyFile != "" {
|
|
||||||
sk, err := keyutil.PrivateKeyFromFile(completed.ServiceAccountSigningKeyFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CompletedOptions{}, fmt.Errorf("failed to parse service-account-issuer-key-file: %w", err)
|
return CompletedOptions{}, err
|
||||||
}
|
|
||||||
completed.ServiceAccountIssuer, err = serviceaccount.JWTTokenGenerator(completed.Authentication.ServiceAccounts.Issuers[0], sk)
|
|
||||||
if err != nil {
|
|
||||||
return CompletedOptions{}, fmt.Errorf("failed to build token generator: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range completed.APIEnablement.RuntimeConfig {
|
for key, value := range completed.APIEnablement.RuntimeConfig {
|
||||||
@@ -281,6 +262,72 @@ func (o *Options) Complete(alternateDNS []string, alternateIPs []net.IP) (Comple
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Options) completeServiceAccountOptions(ctx context.Context, completed *completedOptions) error {
|
||||||
|
transitionWarningFmt := "service-account-extend-token-expiration is true, in order to correctly trigger safe transition logic, service-account-max-token-expiration must be set longer than %d seconds (currently %s)"
|
||||||
|
expExtensionWarningFmt := "service-account-extend-token-expiration is true, enabling tokens valid up to %d seconds, which is longer than service-account-max-token-expiration set to %s"
|
||||||
|
// verify service-account-max-token-expiration
|
||||||
|
if completed.Authentication.ServiceAccounts.MaxExpiration != 0 {
|
||||||
|
lowBound := time.Hour
|
||||||
|
upBound := time.Duration(1<<32) * time.Second
|
||||||
|
if completed.Authentication.ServiceAccounts.MaxExpiration < lowBound ||
|
||||||
|
completed.Authentication.ServiceAccounts.MaxExpiration > upBound {
|
||||||
|
return fmt.Errorf("the service-account-max-token-expiration must be between 1 hour and 2^32 seconds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(completed.Authentication.ServiceAccounts.Issuers) != 0 && completed.Authentication.ServiceAccounts.Issuers[0] != "" {
|
||||||
|
switch {
|
||||||
|
case completed.ServiceAccountSigningEndpoint != "" && completed.ServiceAccountSigningKeyFile != "":
|
||||||
|
return fmt.Errorf("service-account-signing-key-file and service-account-signing-endpoint are mutually exclusive and cannot be set at the same time")
|
||||||
|
case completed.ServiceAccountSigningKeyFile != "":
|
||||||
|
sk, err := keyutil.PrivateKeyFromFile(completed.ServiceAccountSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse service-account-issuer-key-file: %w", err)
|
||||||
|
}
|
||||||
|
completed.ServiceAccountIssuer, err = serviceaccount.JWTTokenGenerator(completed.Authentication.ServiceAccounts.Issuers[0], sk)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build token generator: %w", err)
|
||||||
|
}
|
||||||
|
case completed.ServiceAccountSigningEndpoint != "":
|
||||||
|
plugin, cache, err := plugin.New(ctx, completed.Authentication.ServiceAccounts.Issuers[0], completed.ServiceAccountSigningEndpoint, 60*time.Second, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while setting up external-jwt-signer: %w", err)
|
||||||
|
}
|
||||||
|
timedContext, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
metadata, err := plugin.GetServiceMetadata(timedContext)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while setting up external-jwt-signer: %w", err)
|
||||||
|
}
|
||||||
|
if metadata.MaxTokenExpirationSeconds < validation.MinTokenAgeSec {
|
||||||
|
return fmt.Errorf("max token life supported by external-jwt-signer (%ds) is less than acceptable (min %ds)", metadata.MaxTokenExpirationSeconds, validation.MinTokenAgeSec)
|
||||||
|
}
|
||||||
|
if completed.Authentication.ServiceAccounts.MaxExpiration != 0 {
|
||||||
|
return fmt.Errorf("service-account-max-token-expiration and service-account-signing-endpoint are mutually exclusive and cannot be set at the same time")
|
||||||
|
}
|
||||||
|
transitionWarningFmt = "service-account-extend-token-expiration is true, in order to correctly trigger safe transition logic, token lifetime supported by external-jwt-signer must be longer than %d seconds (currently %s)"
|
||||||
|
expExtensionWarningFmt = "service-account-extend-token-expiration is true, tokens validity will be caped at the smaller of %d seconds and maximum token lifetime supported by external-jwt-signer (%s)"
|
||||||
|
completed.ServiceAccountIssuer = plugin
|
||||||
|
completed.Authentication.ServiceAccounts.ExternalPublicKeysGetter = cache
|
||||||
|
completed.Authentication.ServiceAccounts.MaxExpiration = time.Duration(metadata.MaxTokenExpirationSeconds) * time.Second
|
||||||
|
completed.Authentication.ServiceAccounts.IsTokenSignerExternal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Max expiration and warn on conflicting configuration.
|
||||||
|
if completed.Authentication.ServiceAccounts.ExtendExpiration && completed.Authentication.ServiceAccounts.MaxExpiration != 0 {
|
||||||
|
if completed.Authentication.ServiceAccounts.MaxExpiration < serviceaccount.WarnOnlyBoundTokenExpirationSeconds*time.Second {
|
||||||
|
klog.Warningf(transitionWarningFmt, serviceaccount.WarnOnlyBoundTokenExpirationSeconds, completed.Authentication.ServiceAccounts.MaxExpiration)
|
||||||
|
}
|
||||||
|
if completed.Authentication.ServiceAccounts.MaxExpiration < serviceaccount.ExpirationExtensionSeconds*time.Second {
|
||||||
|
klog.Warningf(expExtensionWarningFmt, serviceaccount.ExpirationExtensionSeconds, completed.Authentication.ServiceAccounts.MaxExpiration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completed.ServiceAccountTokenMaxExpiration = completed.Authentication.ServiceAccounts.MaxExpiration
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceIPRange checks if the serviceClusterIPRange flag is nil, raising a warning if so and
|
// ServiceIPRange checks if the serviceClusterIPRange flag is nil, raising a warning if so and
|
||||||
// setting service ip range to the default value in kubeoptions.DefaultServiceIPCIDR
|
// setting service ip range to the default value in kubeoptions.DefaultServiceIPCIDR
|
||||||
// for now until the default is removed per the deprecation timeline guidelines.
|
// for now until the default is removed per the deprecation timeline guidelines.
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ limitations under the License.
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -39,6 +46,7 @@ import (
|
|||||||
"k8s.io/component-base/metrics"
|
"k8s.io/component-base/metrics"
|
||||||
utilversion "k8s.io/component-base/version"
|
utilversion "k8s.io/component-base/version"
|
||||||
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
|
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
|
||||||
|
v1alpha1testing "k8s.io/kubernetes/pkg/serviceaccount/externaljwt/plugin/testing/v1alpha1"
|
||||||
netutils "k8s.io/utils/net"
|
netutils "k8s.io/utils/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -308,3 +316,198 @@ func TestAddFlags(t *testing.T) {
|
|||||||
t.Errorf("Got emulation version %s, wanted %s", testEffectiveVersion.EmulationVersion().String(), "1.31")
|
t.Errorf("Got emulation version %s, wanted %s", testEffectiveVersion.EmulationVersion().String(), "1.31")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompleteForServiceAccount(t *testing.T) {
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
panic("Error while generating first RSA key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the private key into PEM format
|
||||||
|
privateKeyPEM := &pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a file to write the private key
|
||||||
|
privateKeyFile, err := os.Create("private_key.pem")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create private key file: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = privateKeyFile.Close()
|
||||||
|
_ = os.Remove("private_key.pem")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Write the PEM-encoded private key to the file
|
||||||
|
if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
|
||||||
|
t.Fatalf("Failed to encode private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create and start mock signer.
|
||||||
|
socketPath := "@mock-external-jwt-signer.sock"
|
||||||
|
mockSigner := v1alpha1testing.NewMockSigner(t, socketPath)
|
||||||
|
defer mockSigner.CleanUp()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
issuers []string
|
||||||
|
signingEndpoint string
|
||||||
|
signingKeyFiles string
|
||||||
|
maxExpiration time.Duration
|
||||||
|
externalMaxExpirationSec int64
|
||||||
|
fetchError error
|
||||||
|
metadataError error
|
||||||
|
|
||||||
|
wantError error
|
||||||
|
expectedMaxtokenExp time.Duration
|
||||||
|
expectedIsExternalSigner bool
|
||||||
|
externalPublicKeyGetterPresent bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "no endpoint or key file",
|
||||||
|
issuers: []string{
|
||||||
|
"iss",
|
||||||
|
},
|
||||||
|
signingEndpoint: socketPath,
|
||||||
|
signingKeyFiles: "private_key.pem",
|
||||||
|
maxExpiration: time.Second * 3600,
|
||||||
|
|
||||||
|
wantError: fmt.Errorf("service-account-signing-key-file and service-account-signing-endpoint are mutually exclusive and cannot be set at the same time"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "max token expiration breaching accepteable values",
|
||||||
|
issuers: []string{
|
||||||
|
"iss",
|
||||||
|
},
|
||||||
|
signingEndpoint: socketPath,
|
||||||
|
signingKeyFiles: "private_key.pem",
|
||||||
|
maxExpiration: time.Second * 10,
|
||||||
|
|
||||||
|
wantError: fmt.Errorf("the service-account-max-token-expiration must be between 1 hour and 2^32 seconds"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "path to a signing key provided",
|
||||||
|
issuers: []string{
|
||||||
|
"iss",
|
||||||
|
},
|
||||||
|
signingEndpoint: "",
|
||||||
|
signingKeyFiles: "private_key.pem",
|
||||||
|
maxExpiration: time.Second * 3600,
|
||||||
|
|
||||||
|
expectedIsExternalSigner: false,
|
||||||
|
externalPublicKeyGetterPresent: false,
|
||||||
|
expectedMaxtokenExp: time.Second * 3600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "signing endpoint provided",
|
||||||
|
issuers: []string{
|
||||||
|
"iss",
|
||||||
|
},
|
||||||
|
signingEndpoint: socketPath,
|
||||||
|
signingKeyFiles: "",
|
||||||
|
maxExpiration: 0,
|
||||||
|
externalMaxExpirationSec: 600, // 10m
|
||||||
|
|
||||||
|
expectedIsExternalSigner: true,
|
||||||
|
externalPublicKeyGetterPresent: true,
|
||||||
|
expectedMaxtokenExp: time.Second * 600, // 10m
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "signing endpoint provided and max token expiration set",
|
||||||
|
issuers: []string{
|
||||||
|
"iss",
|
||||||
|
},
|
||||||
|
signingEndpoint: socketPath,
|
||||||
|
signingKeyFiles: "",
|
||||||
|
maxExpiration: time.Second * 3600,
|
||||||
|
externalMaxExpirationSec: 600, // 10m
|
||||||
|
|
||||||
|
wantError: fmt.Errorf("service-account-max-token-expiration and service-account-signing-endpoint are mutually exclusive and cannot be set at the same time"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "signing endpoint provided but return smaller than accaptable max token exp",
|
||||||
|
issuers: []string{
|
||||||
|
"iss",
|
||||||
|
},
|
||||||
|
signingEndpoint: socketPath,
|
||||||
|
signingKeyFiles: "",
|
||||||
|
maxExpiration: 0,
|
||||||
|
externalMaxExpirationSec: 300, // 5m
|
||||||
|
|
||||||
|
wantError: fmt.Errorf("max token life supported by external-jwt-signer (300s) is less than acceptable (min 600s)"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "signing endpoint provided and error when getting metadata",
|
||||||
|
issuers: []string{
|
||||||
|
"iss",
|
||||||
|
},
|
||||||
|
signingEndpoint: socketPath,
|
||||||
|
signingKeyFiles: "",
|
||||||
|
maxExpiration: 0,
|
||||||
|
externalMaxExpirationSec: 900, // 15m
|
||||||
|
metadataError: fmt.Errorf("metadata error"),
|
||||||
|
|
||||||
|
wantError: fmt.Errorf("while setting up external-jwt-signer: rpc error: code = Unknown desc = metadata error"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "signing endpoint provided and error when creating plugin (during initial fetch)",
|
||||||
|
issuers: []string{
|
||||||
|
"iss",
|
||||||
|
},
|
||||||
|
signingEndpoint: socketPath,
|
||||||
|
signingKeyFiles: "",
|
||||||
|
maxExpiration: 0,
|
||||||
|
externalMaxExpirationSec: 900, // 15m
|
||||||
|
fetchError: fmt.Errorf("keys fetch error"),
|
||||||
|
|
||||||
|
wantError: fmt.Errorf("while setting up external-jwt-signer: while initially filling key cache: while performing initial cache fill: while fetching token verification keys: while getting externally supported jwt signing keys: rpc error: code = Unknown desc = keys fetch error"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
options := NewOptions()
|
||||||
|
options.ServiceAccountSigningEndpoint = tc.signingEndpoint
|
||||||
|
options.ServiceAccountSigningKeyFile = tc.signingKeyFiles
|
||||||
|
options.Authentication = &kubeoptions.BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
|
||||||
|
Issuers: tc.issuers,
|
||||||
|
MaxExpiration: tc.maxExpiration,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = mockSigner.Reset()
|
||||||
|
mockSigner.MaxTokenExpirationSeconds = tc.externalMaxExpirationSec
|
||||||
|
mockSigner.MetadataError = tc.metadataError
|
||||||
|
mockSigner.FetchError = tc.fetchError
|
||||||
|
|
||||||
|
co := completedOptions{
|
||||||
|
Options: *options,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := options.completeServiceAccountOptions(context.Background(), &co)
|
||||||
|
|
||||||
|
if tc.wantError != nil {
|
||||||
|
if err == nil || tc.wantError.Error() != err.Error() {
|
||||||
|
t.Errorf("Expected error: %v, got: %v", tc.wantError, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Didn't expect any error but got: %v", err)
|
||||||
|
}
|
||||||
|
if tc.externalPublicKeyGetterPresent != (co.Authentication.ServiceAccounts.ExternalPublicKeysGetter != nil) {
|
||||||
|
t.Errorf("Unexpected value of ExternalPublicKeysGetter: %v", co.Authentication.ServiceAccounts.ExternalPublicKeysGetter)
|
||||||
|
}
|
||||||
|
if tc.expectedIsExternalSigner != co.Authentication.ServiceAccounts.IsTokenSignerExternal {
|
||||||
|
t.Errorf("Expected IsTokenSignerExternal %v, found %v", tc.expectedIsExternalSigner, co.Authentication.ServiceAccounts.IsTokenSignerExternal)
|
||||||
|
}
|
||||||
|
if tc.expectedMaxtokenExp.Seconds() != co.Authentication.ServiceAccounts.MaxExpiration.Seconds() {
|
||||||
|
t.Errorf("Expected MaxExpiration to be %v, found %v", tc.expectedMaxtokenExp, co.Authentication.ServiceAccounts.MaxExpiration)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package options
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
|
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
|
||||||
@@ -103,6 +104,29 @@ func validateUnknownVersionInteroperabilityProxyFlags(options *Options) []error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pathOrSocket = regexp.MustCompile(`(^(/[^/ ]*)+/?$)|(^@([a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+$)`)
|
||||||
|
|
||||||
|
func validateServiceAccountTokenSigningConfig(options *Options) []error {
|
||||||
|
if len(options.ServiceAccountSigningEndpoint) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errors := []error{}
|
||||||
|
|
||||||
|
if len(options.ServiceAccountSigningKeyFile) != 0 || len(options.Authentication.ServiceAccounts.KeyFiles) != 0 {
|
||||||
|
errors = append(errors, fmt.Errorf("can't set `--service-account-signing-key-file` and/or `--service-account-key-file` with `--service-account-signing-endpoint` (They are mutually exclusive)"))
|
||||||
|
}
|
||||||
|
if !utilfeature.DefaultFeatureGate.Enabled(features.ExternalServiceAccountTokenSigner) {
|
||||||
|
errors = append(errors, fmt.Errorf("setting `--service-account-signing-endpoint` requires enabling ExternalServiceAccountTokenSigner feature gate"))
|
||||||
|
}
|
||||||
|
// Check if ServiceAccountSigningEndpoint is a linux file path or an abstract socket name.
|
||||||
|
if !pathOrSocket.MatchString(options.ServiceAccountSigningEndpoint) {
|
||||||
|
errors = append(errors, fmt.Errorf("invalid value %q passed for `--service-account-signing-endpoint`, should be a valid location on the filesystem or must be prefixed with @ to name UDS in abstract namespace", options.ServiceAccountSigningEndpoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
// Validate checks Options and return a slice of found errs.
|
// Validate checks Options and return a slice of found errs.
|
||||||
func (s *Options) Validate() []error {
|
func (s *Options) Validate() []error {
|
||||||
var errs []error
|
var errs []error
|
||||||
@@ -121,6 +145,7 @@ func (s *Options) Validate() []error {
|
|||||||
errs = append(errs, validateUnknownVersionInteroperabilityProxyFeature()...)
|
errs = append(errs, validateUnknownVersionInteroperabilityProxyFeature()...)
|
||||||
errs = append(errs, validateUnknownVersionInteroperabilityProxyFlags(s)...)
|
errs = append(errs, validateUnknownVersionInteroperabilityProxyFlags(s)...)
|
||||||
errs = append(errs, validateNodeSelectorAuthorizationFeature()...)
|
errs = append(errs, validateNodeSelectorAuthorizationFeature()...)
|
||||||
|
errs = append(errs, validateServiceAccountTokenSigningConfig(s)...)
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -262,3 +264,186 @@ func TestValidateOptions(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateServcieAccountTokenSigningConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
featureEnabled bool
|
||||||
|
options *Options
|
||||||
|
expectedErrors []error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Signing keys file provided while external signer endpoint is provided",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("can't set `--service-account-signing-key-file` and/or `--service-account-key-file` with `--service-account-signing-endpoint` (They are mutually exclusive)"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "@ebc.eng.hij",
|
||||||
|
ServiceAccountSigningKeyFile: "/abc/efg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Verification keys file provided while external signer endpoint is provided",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("can't set `--service-account-signing-key-file` and/or `--service-account-key-file` with `--service-account-signing-endpoint` (They are mutually exclusive)"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "@ebc.eng.hij",
|
||||||
|
Authentication: &kubeoptions.BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
|
||||||
|
KeyFiles: []string{
|
||||||
|
"abc",
|
||||||
|
"efg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Verification key and signing key file provided while external signer endpoint is provided",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("can't set `--service-account-signing-key-file` and/or `--service-account-key-file` with `--service-account-signing-endpoint` (They are mutually exclusive)"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "@ebc.eng.hij",
|
||||||
|
ServiceAccountSigningKeyFile: "/abc/efg",
|
||||||
|
Authentication: &kubeoptions.BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
|
||||||
|
KeyFiles: []string{
|
||||||
|
"/abc/efg",
|
||||||
|
"/abc/xyz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature disabled and external signer endpoint is provided",
|
||||||
|
featureEnabled: false,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("setting `--service-account-signing-endpoint` requires enabling ExternalServiceAccountTokenSigner feature gate"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "@ebc.eng.hij",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid external signer endpoint provided - 1",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("invalid value \"abc\" passed for `--service-account-signing-endpoint`, should be a valid location on the filesystem or must be prefixed with @ to name UDS in abstract namespace"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "abc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid external signer endpoint provided - 2",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("invalid value \"@abc@\" passed for `--service-account-signing-endpoint`, should be a valid location on the filesystem or must be prefixed with @ to name UDS in abstract namespace"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "@abc@",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid external signer endpoint provided - 3",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("invalid value \"@abc.abc .ae\" passed for `--service-account-signing-endpoint`, should be a valid location on the filesystem or must be prefixed with @ to name UDS in abstract namespace"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "@abc.abc .ae",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid external signer endpoint provided - 4",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("invalid value \"/@e_adnb/xyz /efg\" passed for `--service-account-signing-endpoint`, should be a valid location on the filesystem or must be prefixed with @ to name UDS in abstract namespace"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "/@e_adnb/xyz /efg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid external signer endpoint provided - 5",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("invalid value \"/e /xyz /efg\" passed for `--service-account-signing-endpoint`, should be a valid location on the filesystem or must be prefixed with @ to name UDS in abstract namespace"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "/e /xyz /efg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid external signer endpoint provided - 1",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "/e/an_b-d/efg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid external signer endpoint provided - 2",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "@ebc.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid external signer endpoint provided - 3",
|
||||||
|
featureEnabled: true,
|
||||||
|
expectedErrors: []error{},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "@ebc.eng.hij",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All errors at once",
|
||||||
|
featureEnabled: false,
|
||||||
|
expectedErrors: []error{
|
||||||
|
fmt.Errorf("can't set `--service-account-signing-key-file` and/or `--service-account-key-file` with `--service-account-signing-endpoint` (They are mutually exclusive)"),
|
||||||
|
fmt.Errorf("setting `--service-account-signing-endpoint` requires enabling ExternalServiceAccountTokenSigner feature gate"),
|
||||||
|
fmt.Errorf("invalid value \"/e /xyz /efg\" passed for `--service-account-signing-endpoint`, should be a valid location on the filesystem or must be prefixed with @ to name UDS in abstract namespace"),
|
||||||
|
},
|
||||||
|
options: &Options{
|
||||||
|
ServiceAccountSigningEndpoint: "/e /xyz /efg",
|
||||||
|
ServiceAccountSigningKeyFile: "/abc/efg",
|
||||||
|
Authentication: &kubeoptions.BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
|
||||||
|
KeyFiles: []string{
|
||||||
|
"/abc/xyz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
if test.options.Authentication == nil {
|
||||||
|
test.options.Authentication = &kubeoptions.BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
|
||||||
|
KeyFiles: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.featureEnabled {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExternalServiceAccountTokenSigner, true)
|
||||||
|
}
|
||||||
|
errs := validateServiceAccountTokenSigningConfig(test.options)
|
||||||
|
if !reflect.DeepEqual(errs, test.expectedErrors) {
|
||||||
|
t.Errorf("Expected errors message: %v \n but got: %v", test.expectedErrors, errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ APIs.`,
|
|||||||
}
|
}
|
||||||
cliflag.PrintFlags(fs)
|
cliflag.PrintFlags(fs)
|
||||||
|
|
||||||
completedOptions, err := s.Complete([]string{}, []net.IP{})
|
ctx := genericapiserver.SetupSignalContext()
|
||||||
|
|
||||||
|
completedOptions, err := s.Complete(ctx, []string{}, []net.IP{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -94,7 +96,7 @@ APIs.`,
|
|||||||
|
|
||||||
// add feature enablement metrics
|
// add feature enablement metrics
|
||||||
utilfeature.DefaultMutableFeatureGate.AddMetrics()
|
utilfeature.DefaultMutableFeatureGate.AddMetrics()
|
||||||
ctx := genericapiserver.SetupSignalContext()
|
|
||||||
return Run(ctx, completedOptions)
|
return Run(ctx, completedOptions)
|
||||||
},
|
},
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func StartTestServer(t ktesting.TB, instanceOptions *TestServerInstanceOptions,
|
|||||||
o.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"}
|
o.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"}
|
||||||
o.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()}
|
o.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()}
|
||||||
|
|
||||||
completedOptions, err := o.Complete(nil, nil)
|
completedOptions, err := o.Complete(tCtx, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("failed to set default ServerRunOptions: %w", err)
|
return result, fmt.Errorf("failed to set default ServerRunOptions: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -819,6 +819,13 @@ const (
|
|||||||
// instead of changing each file on the volumes recursively.
|
// instead of changing each file on the volumes recursively.
|
||||||
// Enables the SELinuxChangePolicy field in PodSecurityContext before SELinuxMount featgure gate is enabled.
|
// Enables the SELinuxChangePolicy field in PodSecurityContext before SELinuxMount featgure gate is enabled.
|
||||||
SELinuxChangePolicy featuregate.Feature = "SELinuxChangePolicy"
|
SELinuxChangePolicy featuregate.Feature = "SELinuxChangePolicy"
|
||||||
|
|
||||||
|
// owner: @HarshalNeelkamal
|
||||||
|
// alpha: v1.32
|
||||||
|
//
|
||||||
|
// Enables external service account JWT signing and key management.
|
||||||
|
// If enabled, it allows passing --service-account-signing-endpoint flag to configure external signer.
|
||||||
|
ExternalServiceAccountTokenSigner featuregate.Feature = "ExternalServiceAccountTokenSigner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
{Version: version.MustParse("1.20"), Default: true, PreRelease: featuregate.GA}, // lock to default and remove after v1.22 based on KEP #1972 update
|
{Version: version.MustParse("1.20"), Default: true, PreRelease: featuregate.GA}, // lock to default and remove after v1.22 based on KEP #1972 update
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ExternalServiceAccountTokenSigner: {
|
||||||
|
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
|
||||||
|
},
|
||||||
|
|
||||||
genericfeatures.AdmissionWebhookMatchConditions: {
|
genericfeatures.AdmissionWebhookMatchConditions: {
|
||||||
{Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha},
|
{Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha},
|
||||||
{Version: version.MustParse("1.28"), Default: true, PreRelease: featuregate.Beta},
|
{Version: version.MustParse("1.28"), Default: true, PreRelease: featuregate.Beta},
|
||||||
|
|||||||
@@ -133,9 +133,13 @@ type ServiceAccountAuthenticationOptions struct {
|
|||||||
JWKSURI string
|
JWKSURI string
|
||||||
MaxExpiration time.Duration
|
MaxExpiration time.Duration
|
||||||
ExtendExpiration bool
|
ExtendExpiration bool
|
||||||
|
IsTokenSignerExternal bool
|
||||||
// OptionalTokenGetter is a function that returns a service account token getter.
|
// OptionalTokenGetter is a function that returns a service account token getter.
|
||||||
// If not set, the default token getter will be used.
|
// If not set, the default token getter will be used.
|
||||||
OptionalTokenGetter func(factory informers.SharedInformerFactory) serviceaccount.ServiceAccountTokenGetter
|
OptionalTokenGetter func(factory informers.SharedInformerFactory) serviceaccount.ServiceAccountTokenGetter
|
||||||
|
// ExternalPublicKeysGetter gets set if `--service-account-signing-endpoint` is passed.
|
||||||
|
// ExternalPublicKeysGetter is mutually exclusive with KeyFiles.
|
||||||
|
ExternalPublicKeysGetter serviceaccount.PublicKeysGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenFileAuthenticationOptions contains token file authentication options for API Server
|
// TokenFileAuthenticationOptions contains token file authentication options for API Server
|
||||||
@@ -270,8 +274,8 @@ func (o *BuiltInAuthenticationOptions) Validate() []error {
|
|||||||
if len(o.ServiceAccounts.Issuers) == 0 {
|
if len(o.ServiceAccounts.Issuers) == 0 {
|
||||||
allErrors = append(allErrors, errors.New("service-account-issuer is a required flag"))
|
allErrors = append(allErrors, errors.New("service-account-issuer is a required flag"))
|
||||||
}
|
}
|
||||||
if len(o.ServiceAccounts.KeyFiles) == 0 {
|
if len(o.ServiceAccounts.KeyFiles) == 0 && o.ServiceAccounts.ExternalPublicKeysGetter == nil {
|
||||||
allErrors = append(allErrors, errors.New("service-account-key-file is a required flag"))
|
allErrors = append(allErrors, errors.New("either `--service-account-key-file` or `--service-account-signing-endpoint` must be set"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the JWKS URI when it is explicitly set.
|
// Validate the JWKS URI when it is explicitly set.
|
||||||
@@ -592,7 +596,11 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
|
|||||||
if len(o.ServiceAccounts.Issuers) != 0 && len(o.APIAudiences) == 0 {
|
if len(o.ServiceAccounts.Issuers) != 0 && len(o.APIAudiences) == 0 {
|
||||||
ret.APIAudiences = authenticator.Audiences(o.ServiceAccounts.Issuers)
|
ret.APIAudiences = authenticator.Audiences(o.ServiceAccounts.Issuers)
|
||||||
}
|
}
|
||||||
if len(o.ServiceAccounts.KeyFiles) > 0 {
|
|
||||||
|
switch {
|
||||||
|
case len(o.ServiceAccounts.KeyFiles) > 0 && o.ServiceAccounts.ExternalPublicKeysGetter != nil:
|
||||||
|
return kubeauthenticator.Config{}, fmt.Errorf("cannot set mutually exclusive flags `--service-account-key-file` and `--service-account-signing-endpoint` at the same time")
|
||||||
|
case len(o.ServiceAccounts.KeyFiles) > 0:
|
||||||
allPublicKeys := []interface{}{}
|
allPublicKeys := []interface{}{}
|
||||||
for _, keyfile := range o.ServiceAccounts.KeyFiles {
|
for _, keyfile := range o.ServiceAccounts.KeyFiles {
|
||||||
publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
|
publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
|
||||||
@@ -606,7 +614,10 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
|
|||||||
return kubeauthenticator.Config{}, fmt.Errorf("failed to set up public service account keys: %w", err)
|
return kubeauthenticator.Config{}, fmt.Errorf("failed to set up public service account keys: %w", err)
|
||||||
}
|
}
|
||||||
ret.ServiceAccountPublicKeysGetter = keysGetter
|
ret.ServiceAccountPublicKeysGetter = keysGetter
|
||||||
|
case o.ServiceAccounts.ExternalPublicKeysGetter != nil:
|
||||||
|
ret.ServiceAccountPublicKeysGetter = o.ServiceAccounts.ExternalPublicKeysGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.ServiceAccountIssuers = o.ServiceAccounts.Issuers
|
ret.ServiceAccountIssuers = o.ServiceAccounts.Issuers
|
||||||
ret.ServiceAccountLookup = o.ServiceAccounts.Lookup
|
ret.ServiceAccountLookup = o.ServiceAccounts.Lookup
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ package options
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -45,6 +50,7 @@ import (
|
|||||||
openapicommon "k8s.io/kube-openapi/pkg/common"
|
openapicommon "k8s.io/kube-openapi/pkg/common"
|
||||||
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
|
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
|
||||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
|
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,7 +108,7 @@ func TestAuthenticationValidate(t *testing.T) {
|
|||||||
testSA: &ServiceAccountAuthenticationOptions{
|
testSA: &ServiceAccountAuthenticationOptions{
|
||||||
Issuers: []string{"http://foo.bar.com"},
|
Issuers: []string{"http://foo.bar.com"},
|
||||||
},
|
},
|
||||||
expectErr: "service-account-key-file is a required flag",
|
expectErr: "either `--service-account-key-file` or `--service-account-signing-endpoint` must be set",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test when ServiceAccounts doesn't have issuer",
|
name: "test when ServiceAccounts doesn't have issuer",
|
||||||
@@ -1511,3 +1517,171 @@ func errString(err error) string {
|
|||||||
}
|
}
|
||||||
return err.Error()
|
return err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToAuthenticationConfigForServiceAccount(t *testing.T) {
|
||||||
|
|
||||||
|
dummyExternalGetter := &dummyPublicKeyGetter{}
|
||||||
|
keyFileName := "public_key.pem"
|
||||||
|
|
||||||
|
key1, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
panic("Error while generating first RSA key")
|
||||||
|
}
|
||||||
|
pubKey1Bytes, err := x509.MarshalPKIXPublicKey(&key1.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
panic("Error while marshaling first public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyBlock := &pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: pubKey1Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyFile, err := os.Create(keyFileName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error creating public key file:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// An open file cannot be removed on Windows. Close it first.
|
||||||
|
if err := publicKeyFile.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(publicKeyFile.Name()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := pem.Encode(publicKeyFile, publicKeyBlock); err != nil {
|
||||||
|
fmt.Println("Error encoding public key:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
options *BuiltInAuthenticationOptions
|
||||||
|
expectConfig kubeauthenticator.Config
|
||||||
|
expectedErr error
|
||||||
|
expectedExternalGetter bool
|
||||||
|
expectedStaticGetter bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "neither key file nor external getter configured",
|
||||||
|
options: &BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &ServiceAccountAuthenticationOptions{
|
||||||
|
Lookup: true,
|
||||||
|
Issuers: []string{"http://foo.bar.com"},
|
||||||
|
KeyFiles: []string{},
|
||||||
|
ExternalPublicKeysGetter: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectConfig: kubeauthenticator.Config{
|
||||||
|
APIAudiences: authenticator.Audiences{"http://foo.bar.com"},
|
||||||
|
ServiceAccountLookup: true,
|
||||||
|
ServiceAccountIssuers: []string{"http://foo.bar.com"},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "both key file and external getter configured",
|
||||||
|
options: &BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &ServiceAccountAuthenticationOptions{
|
||||||
|
Lookup: true,
|
||||||
|
Issuers: []string{"http://foo.bar.com"},
|
||||||
|
KeyFiles: []string{keyFileName},
|
||||||
|
ExternalPublicKeysGetter: dummyExternalGetter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectConfig: kubeauthenticator.Config{
|
||||||
|
APIAudiences: authenticator.Audiences{"http://foo.bar.com"},
|
||||||
|
ServiceAccountLookup: true,
|
||||||
|
ServiceAccountIssuers: []string{"http://foo.bar.com"},
|
||||||
|
},
|
||||||
|
expectedErr: fmt.Errorf("cannot set mutually exclusive flags `--service-account-key-file` and `--service-account-signing-endpoint` at the same time"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "external getter configured",
|
||||||
|
options: &BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &ServiceAccountAuthenticationOptions{
|
||||||
|
Lookup: true,
|
||||||
|
Issuers: []string{"http://foo.bar.com"},
|
||||||
|
KeyFiles: []string{},
|
||||||
|
ExternalPublicKeysGetter: dummyExternalGetter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectConfig: kubeauthenticator.Config{
|
||||||
|
APIAudiences: authenticator.Audiences{"http://foo.bar.com"},
|
||||||
|
ServiceAccountLookup: true,
|
||||||
|
ServiceAccountIssuers: []string{"http://foo.bar.com"},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
expectedExternalGetter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "external getter configured",
|
||||||
|
options: &BuiltInAuthenticationOptions{
|
||||||
|
ServiceAccounts: &ServiceAccountAuthenticationOptions{
|
||||||
|
Lookup: true,
|
||||||
|
Issuers: []string{"http://foo.bar.com"},
|
||||||
|
KeyFiles: []string{keyFileName},
|
||||||
|
ExternalPublicKeysGetter: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectConfig: kubeauthenticator.Config{
|
||||||
|
APIAudiences: authenticator.Audiences{"http://foo.bar.com"},
|
||||||
|
ServiceAccountLookup: true,
|
||||||
|
ServiceAccountIssuers: []string{"http://foo.bar.com"},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
expectedStaticGetter: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
resultConfig, err := tc.options.ToAuthenticationConfig()
|
||||||
|
if tc.expectedErr != nil {
|
||||||
|
if err == nil || tc.expectedErr.Error() != err.Error() {
|
||||||
|
t.Fatalf("Expected error: %v and got: %v", tc.expectedErr, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make out of scope fields nil
|
||||||
|
resultConfig.AuthenticationConfig = nil
|
||||||
|
|
||||||
|
if tc.expectedExternalGetter {
|
||||||
|
if resultConfig.ServiceAccountPublicKeysGetter == nil {
|
||||||
|
t.Fatalf("Expected external getter but none")
|
||||||
|
} else if resultConfig.ServiceAccountPublicKeysGetter != dummyExternalGetter {
|
||||||
|
t.Fatalf("Expected external getter but found someting else")
|
||||||
|
}
|
||||||
|
resultConfig.ServiceAccountPublicKeysGetter = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedStaticGetter {
|
||||||
|
if resultConfig.ServiceAccountPublicKeysGetter == nil {
|
||||||
|
t.Fatalf("Expected static getter but none")
|
||||||
|
} else if resultConfig.ServiceAccountPublicKeysGetter == dummyExternalGetter {
|
||||||
|
t.Fatalf("Expected static getter but found external getter")
|
||||||
|
}
|
||||||
|
resultConfig.ServiceAccountPublicKeysGetter = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(resultConfig, tc.expectConfig) {
|
||||||
|
t.Error(cmp.Diff(resultConfig, tc.expectConfig))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyPublicKeyGetter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyPublicKeyGetter) AddListener(listener serviceaccount.Listener) {}
|
||||||
|
|
||||||
|
func (d *dummyPublicKeyGetter) GetCacheAgeMaxSeconds() int {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyPublicKeyGetter) GetPublicKeys(ctx context.Context, keyIDHint string) []serviceaccount.PublicKey {
|
||||||
|
return []serviceaccount.PublicKey{}
|
||||||
|
}
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ func (p *legacyProvider) NewRESTStorage(apiResourceConfigSource serverstorage.AP
|
|||||||
utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
|
utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
|
||||||
nodeGetter = nodeStorage.Node.Store
|
nodeGetter = nodeStorage.Node.Store
|
||||||
}
|
}
|
||||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, p.ServiceAccountIssuer, p.APIAudiences, p.ServiceAccountMaxExpiration, podStorage.Pod.Store, storage["secrets"].(rest.Getter), nodeGetter, p.ExtendExpiration)
|
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, p.ServiceAccountIssuer, p.APIAudiences, p.ServiceAccountMaxExpiration, podStorage.Pod.Store, storage["secrets"].(rest.Getter), nodeGetter, p.ExtendExpiration, p.IsTokenSignerExternal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return genericapiserver.APIGroupInfo{}, err
|
return genericapiserver.APIGroupInfo{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type GenericConfig struct {
|
|||||||
ServiceAccountIssuer serviceaccount.TokenGenerator
|
ServiceAccountIssuer serviceaccount.TokenGenerator
|
||||||
ServiceAccountMaxExpiration time.Duration
|
ServiceAccountMaxExpiration time.Duration
|
||||||
ExtendExpiration bool
|
ExtendExpiration bool
|
||||||
|
IsTokenSignerExternal bool
|
||||||
|
|
||||||
APIAudiences authenticator.Audiences
|
APIAudiences authenticator.Audiences
|
||||||
|
|
||||||
@@ -102,9 +103,9 @@ func (c *GenericConfig) NewRESTStorage(apiResourceConfigSource serverstorage.API
|
|||||||
|
|
||||||
var serviceAccountStorage *serviceaccountstore.REST
|
var serviceAccountStorage *serviceaccountstore.REST
|
||||||
if c.ServiceAccountIssuer != nil {
|
if c.ServiceAccountIssuer != nil {
|
||||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), secretStorage.Store, newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), c.ExtendExpiration)
|
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), secretStorage.Store, newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), c.ExtendExpiration, c.IsTokenSignerExternal)
|
||||||
} else {
|
} else {
|
||||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), newNotFoundGetter(schema.GroupResource{Resource: "secrets"}), newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), false)
|
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), newNotFoundGetter(schema.GroupResource{Resource: "secrets"}), newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), false, c.IsTokenSignerExternal)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return genericapiserver.APIGroupInfo{}, err
|
return genericapiserver.APIGroupInfo{}, err
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type REST struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewREST returns a RESTStorage object that will work against service accounts.
|
// NewREST returns a RESTStorage object that will work against service accounts.
|
||||||
func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, auds authenticator.Audiences, max time.Duration, podStorage, secretStorage, nodeStorage rest.Getter, extendExpiration bool) (*REST, error) {
|
func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, auds authenticator.Audiences, max time.Duration, podStorage, secretStorage, nodeStorage rest.Getter, extendExpiration bool, isTokenSignerExternal bool) (*REST, error) {
|
||||||
store := &genericregistry.Store{
|
store := &genericregistry.Store{
|
||||||
NewFunc: func() runtime.Object { return &api.ServiceAccount{} },
|
NewFunc: func() runtime.Object { return &api.ServiceAccount{} },
|
||||||
NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} },
|
NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} },
|
||||||
@@ -70,6 +70,7 @@ func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator,
|
|||||||
audsSet: sets.NewString(auds...),
|
audsSet: sets.NewString(auds...),
|
||||||
maxExpirationSeconds: int64(max.Seconds()),
|
maxExpirationSeconds: int64(max.Seconds()),
|
||||||
extendExpiration: extendExpiration,
|
extendExpiration: extendExpiration,
|
||||||
|
isTokenSignerExternal: isTokenSignerExternal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/audit"
|
"k8s.io/apiserver/pkg/audit"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
@@ -42,6 +43,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
|
return newTokenStorage(t, fakeTokenGenerator{"fake"}, nil, panicGetter{}, panicGetter{}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenStorage(t *testing.T, issuer token.TokenGenerator, auds authenticator.Audiences, podStorage, secretStorage, nodeStorage rest.Getter) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
|
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
|
||||||
restOptions := generic.RESTOptions{
|
restOptions := generic.RESTOptions{
|
||||||
StorageConfig: etcdStorage,
|
StorageConfig: etcdStorage,
|
||||||
@@ -50,7 +55,7 @@ func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
|||||||
ResourcePrefix: "serviceaccounts",
|
ResourcePrefix: "serviceaccounts",
|
||||||
}
|
}
|
||||||
// set issuer, podStore and secretStore to allow the token endpoint to be initialised
|
// set issuer, podStore and secretStore to allow the token endpoint to be initialised
|
||||||
rest, err := NewREST(restOptions, fakeTokenGenerator{"fake"}, nil, 0, panicGetter{}, panicGetter{}, nil, false)
|
rest, err := NewREST(restOptions, issuer, auds, 0, podStorage, secretStorage, nodeStorage, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
}
|
}
|
||||||
@@ -62,7 +67,7 @@ type fakeTokenGenerator struct {
|
|||||||
staticToken string
|
staticToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeTokenGenerator) GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error) {
|
func (f fakeTokenGenerator) GenerateToken(ctx context.Context, claims *jwt.Claims, privateClaims interface{}) (string, error) {
|
||||||
return f.staticToken, nil
|
return f.staticToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ type TokenREST struct {
|
|||||||
audsSet sets.String
|
audsSet sets.String
|
||||||
maxExpirationSeconds int64
|
maxExpirationSeconds int64
|
||||||
extendExpiration bool
|
extendExpiration bool
|
||||||
|
isTokenSignerExternal bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = rest.NamedCreater(&TokenREST{})
|
var _ = rest.NamedCreater(&TokenREST{})
|
||||||
@@ -217,16 +218,22 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
|
|||||||
exp := req.Spec.ExpirationSeconds
|
exp := req.Spec.ExpirationSeconds
|
||||||
if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) {
|
if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) {
|
||||||
warnAfter = exp
|
warnAfter = exp
|
||||||
|
// If token issuer is external-jwt-signer, then choose the smaller of
|
||||||
|
// ExpirationExtensionSeconds and max token lifetime supported by external signer.
|
||||||
|
if r.isTokenSignerExternal {
|
||||||
|
exp = min(r.maxExpirationSeconds, token.ExpirationExtensionSeconds)
|
||||||
|
} else {
|
||||||
exp = token.ExpirationExtensionSeconds
|
exp = token.ExpirationExtensionSeconds
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sc, pc, err := token.Claims(*svcacct, pod, secret, node, exp, warnAfter, req.Spec.Audiences)
|
sc, pc, err := token.Claims(*svcacct, pod, secret, node, exp, warnAfter, req.Spec.Audiences)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tokdata, err := r.issuer.GenerateToken(sc, pc)
|
tokdata, err := r.issuer.GenerateToken(ctx, sc, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate token: %v", err)
|
return nil, errors.NewInternalError(fmt.Errorf("failed to generate token: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate status
|
// populate status
|
||||||
|
|||||||
202
pkg/registry/core/serviceaccount/storage/token_test.go
Normal file
202
pkg/registry/core/serviceaccount/storage/token_test.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
|
||||||
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
|
token "k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreate_Token_WithExpiryCap(t *testing.T) {
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
desc string
|
||||||
|
extendExpiration bool
|
||||||
|
maxExpirationSeconds int
|
||||||
|
expectedTokenAgeSec int
|
||||||
|
isExternal bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "maxExpirationSeconds honoured",
|
||||||
|
extendExpiration: true,
|
||||||
|
maxExpirationSeconds: 5 * 60 * 60, // 5h
|
||||||
|
expectedTokenAgeSec: 5 * 60 * 60, // 5h
|
||||||
|
isExternal: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ExpirationExtensionSeconds used for exp",
|
||||||
|
extendExpiration: true,
|
||||||
|
maxExpirationSeconds: 2 * 365 * 24 * 60 * 60, // 2 years
|
||||||
|
expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y
|
||||||
|
isExternal: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ExpirationExtensionSeconds used for exp",
|
||||||
|
extendExpiration: true,
|
||||||
|
maxExpirationSeconds: 5 * 60 * 60, // 5h
|
||||||
|
expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y
|
||||||
|
isExternal: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "requested time use with extension disabled",
|
||||||
|
extendExpiration: false,
|
||||||
|
maxExpirationSeconds: 5 * 60 * 60, // 5h
|
||||||
|
expectedTokenAgeSec: 3607, // 1h
|
||||||
|
isExternal: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "maxExpirationSeconds honoured with extension disabled",
|
||||||
|
extendExpiration: false,
|
||||||
|
maxExpirationSeconds: 30 * 60, // 30m
|
||||||
|
expectedTokenAgeSec: 30 * 60, // 30m
|
||||||
|
isExternal: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a test service account
|
||||||
|
serviceAccount := validNewServiceAccount("foo")
|
||||||
|
|
||||||
|
// Create a new pod
|
||||||
|
pod := &api.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
Namespace: serviceAccount.Namespace,
|
||||||
|
},
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
ServiceAccountName: serviceAccount.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
podGetter := &objectGetter{obj: pod}
|
||||||
|
aud := authenticator.Audiences{
|
||||||
|
"aud-1",
|
||||||
|
"aud-2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
storage, server := newTokenStorage(t, testTokenGenerator{"fake"}, aud, podGetter, panicGetter{}, nil)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
// add the namespace to the context as it is required
|
||||||
|
ctx = request.WithNamespace(ctx, serviceAccount.Namespace)
|
||||||
|
|
||||||
|
// Enable ExternalServiceAccountTokenSigner feature
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExternalServiceAccountTokenSigner, true)
|
||||||
|
|
||||||
|
// record namespace in the store.
|
||||||
|
_, err := storage.Store.Create(ctx, serviceAccount, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed creating test service account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the namespace to the context as it is required
|
||||||
|
ctx = request.WithNamespace(ctx, serviceAccount.Namespace)
|
||||||
|
storage.Token.extendExpiration = tc.extendExpiration
|
||||||
|
storage.Token.maxExpirationSeconds = int64(tc.maxExpirationSeconds)
|
||||||
|
storage.Token.isTokenSignerExternal = tc.isExternal
|
||||||
|
|
||||||
|
tokenReqTimeStamp := time.Now()
|
||||||
|
out, err := storage.Token.Create(ctx, serviceAccount.Name, &authenticationapi.TokenRequest{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: serviceAccount.Name,
|
||||||
|
Namespace: serviceAccount.Namespace,
|
||||||
|
},
|
||||||
|
Spec: authenticationapi.TokenRequestSpec{
|
||||||
|
ExpirationSeconds: 3607,
|
||||||
|
BoundObjectRef: &authenticationapi.BoundObjectReference{
|
||||||
|
Name: pod.Name,
|
||||||
|
Kind: "Pod",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
Audiences: aud,
|
||||||
|
},
|
||||||
|
}, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed calling /token endpoint for service account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenReq := out.(*authenticationapi.TokenRequest)
|
||||||
|
payload := strings.Split(tokenReq.Status.Token, ".")[1]
|
||||||
|
claims, err := base64.RawURLEncoding.DecodeString(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed when decoding payload: %v", err)
|
||||||
|
}
|
||||||
|
structuredClaim := jwt.Claims{}
|
||||||
|
err = json.Unmarshal(claims, &structuredClaim)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error unmarshalling Claims: %v", err)
|
||||||
|
}
|
||||||
|
structuredClaim.Expiry.Time()
|
||||||
|
upperBound := tokenReqTimeStamp.Add(time.Duration(tc.expectedTokenAgeSec+10) * time.Second)
|
||||||
|
lowerBound := tokenReqTimeStamp.Add(time.Duration(tc.expectedTokenAgeSec-10) * time.Second)
|
||||||
|
|
||||||
|
// check for token expiration with a toleration of +/-10s after tokenReqTimeStamp to make for latencies.
|
||||||
|
if structuredClaim.Expiry.Time().After(upperBound) ||
|
||||||
|
structuredClaim.Expiry.Time().Before(lowerBound) {
|
||||||
|
t.Fatalf("expected token expiration to be between %v to %v\n was %v", upperBound, lowerBound, structuredClaim.Expiry.Time())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type objectGetter struct {
|
||||||
|
obj runtime.Object
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f objectGetter) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||||
|
return f.obj, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ rest.Getter = objectGetter{}
|
||||||
|
|
||||||
|
// A basic fake token generator which always returns a static string
|
||||||
|
type testTokenGenerator struct {
|
||||||
|
staticToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f testTokenGenerator) GenerateToken(ctx context.Context, claims *jwt.Claims, privateClaims interface{}) (string, error) {
|
||||||
|
c, err := json.Marshal(claims)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return f.staticToken + "." + base64.RawURLEncoding.EncodeToString(c) + "." + f.staticToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ token.TokenGenerator = testTokenGenerator{}
|
||||||
148
pkg/serviceaccount/externaljwt/metrics/metrics.go
Normal file
148
pkg/serviceaccount/externaljwt/metrics/metrics.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"k8s.io/component-base/metrics"
|
||||||
|
"k8s.io/component-base/metrics/legacyregistry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
namespace = "apiserver"
|
||||||
|
subsystem = "externaljwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lastKeyFetchTimeStamp = metrics.NewGaugeVec(
|
||||||
|
&metrics.GaugeOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "fetch_keys_success_timestamp",
|
||||||
|
Help: "Unix Timestamp in seconds of the last successful FetchKeys request",
|
||||||
|
StabilityLevel: metrics.ALPHA,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
dataTimeStamp = metrics.NewGaugeVec(
|
||||||
|
&metrics.GaugeOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "fetch_keys_data_timestamp",
|
||||||
|
Help: "Unix Timestamp in seconds of the last successful FetchKeys data_timestamp value returned by the external signer",
|
||||||
|
StabilityLevel: metrics.ALPHA,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
totalKeyFetch = metrics.NewCounterVec(
|
||||||
|
&metrics.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "fetch_keys_request_total",
|
||||||
|
Help: "Total attempts at syncing supported JWKs",
|
||||||
|
StabilityLevel: metrics.ALPHA,
|
||||||
|
},
|
||||||
|
[]string{"code"},
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenGenReqTotal = metrics.NewCounterVec(
|
||||||
|
&metrics.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "sign_request_total",
|
||||||
|
Help: "Total attempts at signing JWT",
|
||||||
|
StabilityLevel: metrics.ALPHA,
|
||||||
|
},
|
||||||
|
[]string{"code"},
|
||||||
|
)
|
||||||
|
|
||||||
|
requestDurationSeconds = metrics.NewHistogramVec(
|
||||||
|
&metrics.HistogramOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "request_duration_seconds",
|
||||||
|
Help: "Request duration and time for calls to external-jwt-signer",
|
||||||
|
Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 30, 60},
|
||||||
|
StabilityLevel: metrics.ALPHA,
|
||||||
|
},
|
||||||
|
[]string{"method", "code"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var registerMetrics sync.Once
|
||||||
|
|
||||||
|
func RegisterMetrics() {
|
||||||
|
registerMetrics.Do(func() {
|
||||||
|
legacyregistry.MustRegister(lastKeyFetchTimeStamp)
|
||||||
|
legacyregistry.MustRegister(dataTimeStamp)
|
||||||
|
legacyregistry.MustRegister(totalKeyFetch)
|
||||||
|
legacyregistry.MustRegister(tokenGenReqTotal)
|
||||||
|
legacyregistry.MustRegister(requestDurationSeconds)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordFetchKeysAttempt(err error) {
|
||||||
|
totalKeyFetch.WithLabelValues(getErrorCode(err)).Inc()
|
||||||
|
if err == nil {
|
||||||
|
lastKeyFetchTimeStamp.WithLabelValues().SetToCurrentTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordTokenGenAttempt(err error) {
|
||||||
|
tokenGenReqTotal.WithLabelValues(getErrorCode(err)).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordKeyDataTimeStamp(timestamp int64) {
|
||||||
|
dataTimeStamp.WithLabelValues().Set(float64(timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
|
type gRPCError interface {
|
||||||
|
GRPCStatus() *status.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func getErrorCode(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return codes.OK.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle errors wrapped with fmt.Errorf and similar
|
||||||
|
var s gRPCError
|
||||||
|
if errors.As(err, &s) {
|
||||||
|
return s.GRPCStatus().Code().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not gRPC error. The operation must have failed before gRPC
|
||||||
|
// method was called, otherwise we would get gRPC error.
|
||||||
|
return "unknown-non-grpc"
|
||||||
|
}
|
||||||
|
|
||||||
|
func OuboundRequestMetricsInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||||
|
start := time.Now()
|
||||||
|
err := invoker(ctx, method, req, reply, cc, opts...)
|
||||||
|
requestDurationSeconds.WithLabelValues(method, getErrorCode(err)).Observe(time.Since(start).Seconds())
|
||||||
|
return err
|
||||||
|
}
|
||||||
244
pkg/serviceaccount/externaljwt/metrics/metrics_test.go
Normal file
244
pkg/serviceaccount/externaljwt/metrics/metrics_test.go
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"k8s.io/component-base/metrics/legacyregistry"
|
||||||
|
"k8s.io/component-base/metrics/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetchMetrics(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
metrics []string
|
||||||
|
want string
|
||||||
|
emit func()
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "basic test",
|
||||||
|
metrics: []string{
|
||||||
|
"apiserver_externaljwt_fetch_keys_request_total",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordFetchKeysAttempt(status.New(codes.Internal, "error ocured").Err())
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`
|
||||||
|
# HELP apiserver_externaljwt_fetch_keys_request_total [ALPHA] Total attempts at syncing supported JWKs
|
||||||
|
# TYPE apiserver_externaljwt_fetch_keys_request_total counter
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 1
|
||||||
|
`, codes.Internal.String()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wrapped error code",
|
||||||
|
metrics: []string{
|
||||||
|
"apiserver_externaljwt_fetch_keys_request_total",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordFetchKeysAttempt(fmt.Errorf("some error %w", status.New(codes.Canceled, "error ocured").Err()))
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`
|
||||||
|
# HELP apiserver_externaljwt_fetch_keys_request_total [ALPHA] Total attempts at syncing supported JWKs
|
||||||
|
# TYPE apiserver_externaljwt_fetch_keys_request_total counter
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 1
|
||||||
|
`, codes.Internal.String(), codes.Canceled.String()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success count appears",
|
||||||
|
metrics: []string{
|
||||||
|
"apiserver_externaljwt_fetch_keys_request_total",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordFetchKeysAttempt(nil)
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`
|
||||||
|
# HELP apiserver_externaljwt_fetch_keys_request_total [ALPHA] Total attempts at syncing supported JWKs
|
||||||
|
# TYPE apiserver_externaljwt_fetch_keys_request_total counter
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 1
|
||||||
|
`, codes.Internal.String(), codes.Canceled.String(), codes.OK.String()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success count increments",
|
||||||
|
metrics: []string{
|
||||||
|
"apiserver_externaljwt_fetch_keys_request_total",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordFetchKeysAttempt(nil)
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`
|
||||||
|
# HELP apiserver_externaljwt_fetch_keys_request_total [ALPHA] Total attempts at syncing supported JWKs
|
||||||
|
# TYPE apiserver_externaljwt_fetch_keys_request_total counter
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_fetch_keys_request_total{code="%s"} 2
|
||||||
|
`, codes.Internal.String(), codes.Canceled.String(), codes.OK.String()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterMetrics()
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
tt.emit()
|
||||||
|
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenGenMetrics(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
metrics []string
|
||||||
|
want string
|
||||||
|
emit func()
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "basic test",
|
||||||
|
metrics: []string{
|
||||||
|
"apiserver_externaljwt_sign_request_total",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordTokenGenAttempt(fmt.Errorf("some error %w", status.New(codes.Internal, "error ocured").Err()))
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`
|
||||||
|
# HELP apiserver_externaljwt_sign_request_total [ALPHA] Total attempts at signing JWT
|
||||||
|
# TYPE apiserver_externaljwt_sign_request_total counter
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 1
|
||||||
|
`, codes.Internal.String()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wrapped error code",
|
||||||
|
metrics: []string{
|
||||||
|
"apiserver_externaljwt_sign_request_total",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordTokenGenAttempt(fmt.Errorf("some error %w", fmt.Errorf("some error %w", status.New(codes.Canceled, "error ocured").Err())))
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`
|
||||||
|
# HELP apiserver_externaljwt_sign_request_total [ALPHA] Total attempts at signing JWT
|
||||||
|
# TYPE apiserver_externaljwt_sign_request_total counter
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 1
|
||||||
|
`, codes.Internal.String(), codes.Canceled.String()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success count appears",
|
||||||
|
metrics: []string{
|
||||||
|
"apiserver_externaljwt_sign_request_total",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordTokenGenAttempt(nil)
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`
|
||||||
|
# HELP apiserver_externaljwt_sign_request_total [ALPHA] Total attempts at signing JWT
|
||||||
|
# TYPE apiserver_externaljwt_sign_request_total counter
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 1
|
||||||
|
`, codes.Internal.String(), codes.Canceled.String(), codes.OK.String()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success count increments",
|
||||||
|
metrics: []string{
|
||||||
|
"apiserver_externaljwt_sign_request_total",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordTokenGenAttempt(nil)
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`
|
||||||
|
# HELP apiserver_externaljwt_sign_request_total [ALPHA] Total attempts at signing JWT
|
||||||
|
# TYPE apiserver_externaljwt_sign_request_total counter
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 1
|
||||||
|
apiserver_externaljwt_sign_request_total{code="%s"} 2
|
||||||
|
`, codes.Internal.String(), codes.Canceled.String(), codes.OK.String()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterMetrics()
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
tt.emit()
|
||||||
|
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordKeyDataTimeStamp(t *testing.T) {
|
||||||
|
|
||||||
|
dataTimeStamp1 := time.Now().Unix()
|
||||||
|
dataTimeStamp2 := time.Now().Add(time.Second * 1200).Unix()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
metrics []string
|
||||||
|
want int64
|
||||||
|
emit func()
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "basic test",
|
||||||
|
metrics: []string{
|
||||||
|
"fetch_keys_data_timestamp",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordKeyDataTimeStamp(dataTimeStamp1)
|
||||||
|
},
|
||||||
|
want: dataTimeStamp1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "update to a new value",
|
||||||
|
metrics: []string{
|
||||||
|
"fetch_keys_data_timestamp",
|
||||||
|
},
|
||||||
|
emit: func() {
|
||||||
|
RecordKeyDataTimeStamp(dataTimeStamp1)
|
||||||
|
RecordKeyDataTimeStamp(dataTimeStamp2)
|
||||||
|
},
|
||||||
|
want: dataTimeStamp2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterMetrics()
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
tt.emit()
|
||||||
|
actualValue, err := testutil.GetGaugeMetricValue(dataTimeStamp.WithLabelValues())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error when getting gauge value for dataTimeStamp: %v", err)
|
||||||
|
}
|
||||||
|
if actualValue != float64(tt.want) {
|
||||||
|
t.Errorf("Expected dataTimeStamp to be %v, got %v", tt.want, actualValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
236
pkg/serviceaccount/externaljwt/plugin/keycache.go
Normal file
236
pkg/serviceaccount/externaljwt/plugin/keycache.go
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
|
|
||||||
|
externaljwtv1alpha1 "k8s.io/externaljwt/apis/v1alpha1"
|
||||||
|
externaljwtmetrics "k8s.io/kubernetes/pkg/serviceaccount/externaljwt/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fallbackRefreshDuration = 10 * time.Second
|
||||||
|
|
||||||
|
type keyCache struct {
|
||||||
|
client externaljwtv1alpha1.ExternalJWTSignerClient
|
||||||
|
|
||||||
|
syncGroup singleflight.Group
|
||||||
|
listenersLock sync.Mutex
|
||||||
|
listeners []serviceaccount.Listener
|
||||||
|
|
||||||
|
verificationKeys atomic.Pointer[VerificationKeys]
|
||||||
|
}
|
||||||
|
|
||||||
|
// newKeyCache constructs an implementation of KeyCache.
|
||||||
|
func newKeyCache(client externaljwtv1alpha1.ExternalJWTSignerClient) *keyCache {
|
||||||
|
cache := &keyCache{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
cache.verificationKeys.Store(&VerificationKeys{})
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitialFill can be used to perform an initial fetch for keys get the
|
||||||
|
// refresh interval as recommended by external signer.
|
||||||
|
func (p *keyCache) initialFill(ctx context.Context) error {
|
||||||
|
if _, err := p.syncKeys(ctx); err != nil {
|
||||||
|
return fmt.Errorf("while performing initial cache fill: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *keyCache) scheduleSync(ctx context.Context, keySyncTimeout time.Duration) {
|
||||||
|
timer := time.NewTimer(p.verificationKeys.Load().NextRefreshHint.Sub(time.Now()))
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
var lastDataTimestamp time.Time
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
klog.InfoS("Key cache shutting down")
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, keySyncTimeout)
|
||||||
|
dataTimestamp, err := p.syncKeys(timedCtx)
|
||||||
|
if err != nil {
|
||||||
|
klog.Errorf("when syncing supported public keys(Stale set of keys will be supported): %v", err)
|
||||||
|
timer.Reset(fallbackRefreshDuration)
|
||||||
|
} else {
|
||||||
|
timer.Reset(p.verificationKeys.Load().NextRefreshHint.Sub(time.Now()))
|
||||||
|
if lastDataTimestamp.IsZero() || !dataTimestamp.Equal(lastDataTimestamp) {
|
||||||
|
lastDataTimestamp = dataTimestamp
|
||||||
|
p.broadcastUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *keyCache) AddListener(listener serviceaccount.Listener) {
|
||||||
|
p.listenersLock.Lock()
|
||||||
|
defer p.listenersLock.Unlock()
|
||||||
|
|
||||||
|
p.listeners = append(p.listeners, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *keyCache) GetCacheAgeMaxSeconds() int {
|
||||||
|
val := int(p.verificationKeys.Load().NextRefreshHint.Sub(time.Now()).Seconds())
|
||||||
|
if val < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPublicKeys returns the public key corresponding to requested keyID.
|
||||||
|
// Getter is expected to return All keys for keyID ""
|
||||||
|
func (p *keyCache) GetPublicKeys(ctx context.Context, keyID string) []serviceaccount.PublicKey {
|
||||||
|
pubKeys, ok := p.findKeyForKeyID(keyID)
|
||||||
|
if ok {
|
||||||
|
return pubKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't find it, trigger a sync.
|
||||||
|
if _, err := p.syncKeys(ctx); err != nil {
|
||||||
|
klog.ErrorS(err, "Error while syncing keys")
|
||||||
|
return []serviceaccount.PublicKey{}
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKeys, ok = p.findKeyForKeyID(keyID)
|
||||||
|
if ok {
|
||||||
|
return pubKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still didn't find it, then it's an unknown keyID.
|
||||||
|
klog.Errorf("Key id %q not found after refresh", keyID)
|
||||||
|
return []serviceaccount.PublicKey{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *keyCache) findKeyForKeyID(keyID string) ([]serviceaccount.PublicKey, bool) {
|
||||||
|
if len(p.verificationKeys.Load().Keys) == 0 {
|
||||||
|
klog.Error("No keys currently in cache. Initial fill has not completed")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyID == "" {
|
||||||
|
return p.verificationKeys.Load().Keys, true
|
||||||
|
}
|
||||||
|
|
||||||
|
keysToReturn := []serviceaccount.PublicKey{}
|
||||||
|
for _, key := range p.verificationKeys.Load().Keys {
|
||||||
|
if key.KeyID == keyID {
|
||||||
|
keysToReturn = append(keysToReturn, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keysToReturn, len(keysToReturn) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync supported external keys.
|
||||||
|
// completely re-writes the set of supported keys.
|
||||||
|
func (p *keyCache) syncKeys(ctx context.Context) (time.Time, error) {
|
||||||
|
val, err, _ := p.syncGroup.Do("", func() (any, error) {
|
||||||
|
newPublicKeys, err := p.getTokenVerificationKeys(ctx)
|
||||||
|
externaljwtmetrics.RecordFetchKeysAttempt(err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("while fetching token verification keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.verificationKeys.Store(newPublicKeys)
|
||||||
|
|
||||||
|
externaljwtmetrics.RecordKeyDataTimeStamp(newPublicKeys.DataTimestamp.Unix())
|
||||||
|
|
||||||
|
return newPublicKeys, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vk := val.(*VerificationKeys)
|
||||||
|
|
||||||
|
return vk.DataTimestamp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *keyCache) broadcastUpdate() {
|
||||||
|
p.listenersLock.Lock()
|
||||||
|
defer p.listenersLock.Unlock()
|
||||||
|
|
||||||
|
for _, l := range p.listeners {
|
||||||
|
l.Enqueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenVerificationKeys returns a map of supported external keyIDs to keys
|
||||||
|
// the keys are PKIX-serialized. It calls external-jwt-signer with a timeout of keySyncTimeoutSec.
|
||||||
|
func (p *keyCache) getTokenVerificationKeys(ctx context.Context) (*VerificationKeys, error) {
|
||||||
|
req := &externaljwtv1alpha1.FetchKeysRequest{}
|
||||||
|
resp, err := p.client.FetchKeys(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("while getting externally supported jwt signing keys: %w", err)
|
||||||
|
}
|
||||||
|
// Validate the refresh hint.
|
||||||
|
if resp.RefreshHintSeconds <= 0 {
|
||||||
|
return nil, fmt.Errorf("found invalid refresh hint (%ds)", resp.RefreshHintSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Keys) == 0 {
|
||||||
|
return nil, fmt.Errorf("found no keys")
|
||||||
|
}
|
||||||
|
if err := resp.DataTimestamp.CheckValid(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid data timestamp: %w", err)
|
||||||
|
}
|
||||||
|
keys := make([]serviceaccount.PublicKey, 0, len(resp.Keys))
|
||||||
|
for _, protoKey := range resp.Keys {
|
||||||
|
if protoKey == nil {
|
||||||
|
return nil, fmt.Errorf("found nil public key")
|
||||||
|
}
|
||||||
|
if len(protoKey.KeyId) == 0 || len(protoKey.KeyId) > 1024 {
|
||||||
|
return nil, fmt.Errorf("found invalid public key id %q", protoKey.KeyId)
|
||||||
|
}
|
||||||
|
if len(protoKey.Key) == 0 {
|
||||||
|
return nil, fmt.Errorf("found empty public key")
|
||||||
|
}
|
||||||
|
parsedPublicKey, err := x509.ParsePKIXPublicKey(protoKey.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("while parsing external public keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys = append(keys, serviceaccount.PublicKey{
|
||||||
|
KeyID: protoKey.KeyId,
|
||||||
|
PublicKey: parsedPublicKey,
|
||||||
|
ExcludeFromOIDCDiscovery: protoKey.ExcludeFromOidcDiscovery,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
vk := &VerificationKeys{
|
||||||
|
Keys: keys,
|
||||||
|
DataTimestamp: resp.DataTimestamp.AsTime(),
|
||||||
|
NextRefreshHint: time.Now().Add(time.Duration(resp.RefreshHintSeconds) * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
return vk, nil
|
||||||
|
}
|
||||||
407
pkg/serviceaccount/externaljwt/plugin/keycache_test.go
Normal file
407
pkg/serviceaccount/externaljwt/plugin/keycache_test.go
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
externaljwtv1alpha1 "k8s.io/externaljwt/apis/v1alpha1"
|
||||||
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExternalPublicKeyGetter(t *testing.T) {
|
||||||
|
|
||||||
|
invalidKid := string(make([]byte, 1025))
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
expectedErr error
|
||||||
|
supportedKeys map[string]supportedKeyT
|
||||||
|
wantVerificationKeys *VerificationKeys
|
||||||
|
refreshHintSec int
|
||||||
|
dataTimeStamp *timestamppb.Timestamp
|
||||||
|
supportedKeysOverride []*externaljwtv1alpha1.Key
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "single key in signer",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantVerificationKeys: &VerificationKeys{
|
||||||
|
Keys: []serviceaccount.PublicKey{
|
||||||
|
{
|
||||||
|
KeyID: "key-1",
|
||||||
|
PublicKey: &rsaKey1.PublicKey,
|
||||||
|
ExcludeFromOIDCDiscovery: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refreshHintSec: 20,
|
||||||
|
dataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multiple keys in signer",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
},
|
||||||
|
"key-2": {
|
||||||
|
key: &rsaKey2.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantVerificationKeys: &VerificationKeys{
|
||||||
|
Keys: []serviceaccount.PublicKey{
|
||||||
|
{
|
||||||
|
KeyID: "key-1",
|
||||||
|
PublicKey: &rsaKey1.PublicKey,
|
||||||
|
ExcludeFromOIDCDiscovery: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
KeyID: "key-2",
|
||||||
|
PublicKey: &rsaKey2.PublicKey,
|
||||||
|
ExcludeFromOIDCDiscovery: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refreshHintSec: 10,
|
||||||
|
dataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty kid",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
},
|
||||||
|
"key-2": {
|
||||||
|
key: &rsaKey2.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: fmt.Errorf("found invalid public key id %q", ""),
|
||||||
|
refreshHintSec: 10,
|
||||||
|
dataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "kid longer than 1024",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
invalidKid: {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
},
|
||||||
|
"key-2": {
|
||||||
|
key: &rsaKey2.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: fmt.Errorf("found invalid public key id %q", invalidKid),
|
||||||
|
refreshHintSec: 10,
|
||||||
|
dataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "no keys",
|
||||||
|
supportedKeys: map[string]supportedKeyT{},
|
||||||
|
expectedErr: fmt.Errorf("found no keys"),
|
||||||
|
refreshHintSec: 10,
|
||||||
|
dataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid data timestamp",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-2": {
|
||||||
|
key: &rsaKey2.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: fmt.Errorf("invalid data timestamp"),
|
||||||
|
refreshHintSec: 10,
|
||||||
|
dataTimeStamp: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty public key",
|
||||||
|
expectedErr: fmt.Errorf("found empty public key"),
|
||||||
|
refreshHintSec: 10,
|
||||||
|
dataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-2": {
|
||||||
|
key: &rsaKey2.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
supportedKeysOverride: []*externaljwtv1alpha1.Key{
|
||||||
|
{
|
||||||
|
KeyId: "kid",
|
||||||
|
Key: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sockname := fmt.Sprintf("@test-external-public-key-getter-%d.sock", i)
|
||||||
|
t.Cleanup(func() { _ = os.Remove(sockname) })
|
||||||
|
|
||||||
|
addr := &net.UnixAddr{Name: sockname, Net: "unix"}
|
||||||
|
listener, err := net.ListenUnix(addr.Network(), addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start fake backend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer()
|
||||||
|
|
||||||
|
backend := &dummyExtrnalSigner{
|
||||||
|
supportedKeys: tc.supportedKeys,
|
||||||
|
refreshHintSeconds: tc.refreshHintSec,
|
||||||
|
}
|
||||||
|
backend.DataTimeStamp = tc.dataTimeStamp
|
||||||
|
backend.SupportedKeysOverride = tc.supportedKeysOverride
|
||||||
|
externaljwtv1alpha1.RegisterExternalJWTSignerServer(grpcServer, backend)
|
||||||
|
|
||||||
|
defer grpcServer.Stop()
|
||||||
|
go func() {
|
||||||
|
if err := grpcServer.Serve(listener); err != nil {
|
||||||
|
panic(fmt.Errorf("error returned from grpcServer: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientConn, err := grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
sockname,
|
||||||
|
grpc.WithContextDialer(func(ctx context.Context, path string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, "unix", path)
|
||||||
|
}),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to dial buffconn client: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = clientConn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
plugin := newPlugin("iss", clientConn, true)
|
||||||
|
|
||||||
|
signingKeys, err := plugin.keyCache.getTokenVerificationKeys(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if tc.expectedErr == nil {
|
||||||
|
t.Fatalf("error getting supported keys: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.expectedErr.Error()) {
|
||||||
|
t.Fatalf("want error: %v, got error: %v", tc.expectedErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedErr == nil {
|
||||||
|
if diff := cmp.Diff(signingKeys.Keys, tc.wantVerificationKeys.Keys, cmpopts.SortSlices(sortPublicKeySlice)); diff != "" {
|
||||||
|
t.Fatalf("Bad result from GetTokenSigningKeys; diff (-got +want)\n%s", diff)
|
||||||
|
}
|
||||||
|
expectedRefreshHintSec := time.Now().Add(time.Duration(tc.refreshHintSec) * time.Second)
|
||||||
|
difference := signingKeys.NextRefreshHint.Sub(expectedRefreshHintSec).Seconds()
|
||||||
|
if difference > 1 || difference < -1 { // tolerate 1 sec of skew for test
|
||||||
|
t.Fatalf("refreshHint not as expected; got: %v want: %v", signingKeys.NextRefreshHint, expectedRefreshHintSec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitialFill(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sockname := "@test-initial-fill.sock"
|
||||||
|
t.Cleanup(func() { _ = os.Remove(sockname) })
|
||||||
|
|
||||||
|
addr := &net.UnixAddr{Name: sockname, Net: "unix"}
|
||||||
|
listener, err := net.ListenUnix(addr.Network(), addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start fake backend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer()
|
||||||
|
|
||||||
|
supportedKeys := map[string]supportedKeyT{
|
||||||
|
"key-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wantPubKeys := []serviceaccount.PublicKey{
|
||||||
|
{
|
||||||
|
KeyID: "key-1",
|
||||||
|
PublicKey: &rsaKey1.PublicKey,
|
||||||
|
ExcludeFromOIDCDiscovery: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
backend := &dummyExtrnalSigner{
|
||||||
|
supportedKeys: supportedKeys,
|
||||||
|
refreshHintSeconds: 10,
|
||||||
|
DataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
}
|
||||||
|
externaljwtv1alpha1.RegisterExternalJWTSignerServer(grpcServer, backend)
|
||||||
|
|
||||||
|
defer grpcServer.Stop()
|
||||||
|
go func() {
|
||||||
|
if err := grpcServer.Serve(listener); err != nil {
|
||||||
|
panic(fmt.Errorf("error returned from grpcServer: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientConn, err := grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
sockname,
|
||||||
|
grpc.WithContextDialer(func(ctx context.Context, path string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, "unix", path)
|
||||||
|
}),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to dial buffconn client: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = clientConn.Close() }()
|
||||||
|
|
||||||
|
plugin := newPlugin("iss", clientConn, true)
|
||||||
|
|
||||||
|
if err := plugin.keyCache.initialFill(ctx); err != nil {
|
||||||
|
t.Fatalf("Error during InitialFill: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPubKeys := plugin.keyCache.GetPublicKeys(ctx, "")
|
||||||
|
if diff := cmp.Diff(gotPubKeys, wantPubKeys); diff != "" {
|
||||||
|
t.Fatalf("Bad public keys; diff (-got +want)\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReflectChanges(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sockname := "@test-reflect-changes.sock"
|
||||||
|
t.Cleanup(func() { _ = os.Remove(sockname) })
|
||||||
|
|
||||||
|
addr := &net.UnixAddr{Name: sockname, Net: "unix"}
|
||||||
|
listener, err := net.ListenUnix(addr.Network(), addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start fake backend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer()
|
||||||
|
|
||||||
|
supportedKeysT1 := map[string]supportedKeyT{
|
||||||
|
"key-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wantPubKeysT1 := []serviceaccount.PublicKey{
|
||||||
|
{
|
||||||
|
KeyID: "key-1",
|
||||||
|
PublicKey: &rsaKey1.PublicKey,
|
||||||
|
ExcludeFromOIDCDiscovery: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
backend := &dummyExtrnalSigner{
|
||||||
|
supportedKeys: supportedKeysT1,
|
||||||
|
refreshHintSeconds: 10,
|
||||||
|
DataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
}
|
||||||
|
externaljwtv1alpha1.RegisterExternalJWTSignerServer(grpcServer, backend)
|
||||||
|
|
||||||
|
defer grpcServer.Stop()
|
||||||
|
go func() {
|
||||||
|
if err := grpcServer.Serve(listener); err != nil {
|
||||||
|
panic(fmt.Errorf("error returned from grpcServer: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientConn, err := grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
sockname,
|
||||||
|
grpc.WithContextDialer(func(ctx context.Context, path string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, "unix", path)
|
||||||
|
}),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to dial buffconn client: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = clientConn.Close() }()
|
||||||
|
|
||||||
|
plugin := newPlugin("iss", clientConn, true)
|
||||||
|
|
||||||
|
if err := plugin.keyCache.initialFill(ctx); err != nil {
|
||||||
|
t.Fatalf("Error during InitialFill: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPubKeysT1 := plugin.keyCache.GetPublicKeys(ctx, "")
|
||||||
|
if diff := cmp.Diff(gotPubKeysT1, wantPubKeysT1, cmpopts.SortSlices(sortPublicKeySlice)); diff != "" {
|
||||||
|
t.Fatalf("Bad public keys; diff (-got +want)\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := plugin.keyCache.syncKeys(ctx); err != nil {
|
||||||
|
t.Fatalf("Error while calling syncKeys: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedKeysT2 := map[string]supportedKeyT{
|
||||||
|
"key-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
"key-2": {
|
||||||
|
key: &rsaKey2.PublicKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wantPubKeysT2 := []serviceaccount.PublicKey{
|
||||||
|
{
|
||||||
|
KeyID: "key-1",
|
||||||
|
PublicKey: &rsaKey1.PublicKey,
|
||||||
|
ExcludeFromOIDCDiscovery: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
KeyID: "key-2",
|
||||||
|
PublicKey: &rsaKey2.PublicKey,
|
||||||
|
ExcludeFromOIDCDiscovery: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
backend.keyLock.Lock()
|
||||||
|
backend.supportedKeys = supportedKeysT2
|
||||||
|
backend.keyLock.Unlock()
|
||||||
|
|
||||||
|
if _, err := plugin.keyCache.syncKeys(ctx); err != nil {
|
||||||
|
t.Fatalf("Error while calling syncKeys: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPubKeysT2 := plugin.keyCache.GetPublicKeys(ctx, "")
|
||||||
|
if diff := cmp.Diff(gotPubKeysT2, wantPubKeysT2, cmpopts.SortSlices(sortPublicKeySlice)); diff != "" {
|
||||||
|
t.Fatalf("Bad public keys; diff (-got +want)\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
218
pkg/serviceaccount/externaljwt/plugin/plugin.go
Normal file
218
pkg/serviceaccount/externaljwt/plugin/plugin.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
|
|
||||||
|
externaljwtv1alpha1 "k8s.io/externaljwt/apis/v1alpha1"
|
||||||
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
|
externaljwtmetrics "k8s.io/kubernetes/pkg/serviceaccount/externaljwt/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
externaljwtmetrics.RegisterMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerificationKeys struct {
|
||||||
|
Keys []serviceaccount.PublicKey
|
||||||
|
DataTimestamp time.Time
|
||||||
|
NextRefreshHint time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// New calls external signer to fill out supported keys.
|
||||||
|
// It also starts a periodic sync of external keys.
|
||||||
|
// In order for the key cache and external signing to work correctly, pass a context that will live as
|
||||||
|
// long as the dependent process; is used to maintain the lifetime of the connection to external signer.
|
||||||
|
func New(ctx context.Context, issuer, socketPath string, keySyncTimeout time.Duration, allowSigningWithNonOIDCKeys bool) (*Plugin, *keyCache, error) {
|
||||||
|
conn, err := grpc.Dial(
|
||||||
|
socketPath,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithAuthority("localhost"),
|
||||||
|
grpc.WithDefaultCallOptions(grpc.WaitForReady(true)),
|
||||||
|
grpc.WithContextDialer(func(ctx context.Context, path string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, "unix", path)
|
||||||
|
}),
|
||||||
|
grpc.WithChainUnaryInterceptor(externaljwtmetrics.OuboundRequestMetricsInterceptor),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("while dialing grpc socket at %q: %w", socketPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin := newPlugin(issuer, conn, allowSigningWithNonOIDCKeys)
|
||||||
|
|
||||||
|
initialFillCtx, cancel := context.WithTimeout(ctx, keySyncTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := plugin.keyCache.initialFill(initialFillCtx); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("while initially filling key cache: %w", err)
|
||||||
|
}
|
||||||
|
go plugin.keyCache.scheduleSync(ctx, keySyncTimeout)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return plugin, plugin.keyCache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// enables plugging in an external jwt signer.
|
||||||
|
type Plugin struct {
|
||||||
|
iss string
|
||||||
|
client externaljwtv1alpha1.ExternalJWTSignerClient
|
||||||
|
keyCache *keyCache
|
||||||
|
allowSigningWithNonOIDCKeys bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPlugin constructs an implementation of external JWT signer plugin.
|
||||||
|
func newPlugin(iss string, conn *grpc.ClientConn, allowSigningWithNonOIDCKeys bool) *Plugin {
|
||||||
|
client := externaljwtv1alpha1.NewExternalJWTSignerClient(conn)
|
||||||
|
plugin := &Plugin{
|
||||||
|
iss: iss,
|
||||||
|
client: client,
|
||||||
|
allowSigningWithNonOIDCKeys: allowSigningWithNonOIDCKeys,
|
||||||
|
keyCache: newKeyCache(client),
|
||||||
|
}
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken creates a service account token with the provided claims by
|
||||||
|
// calling out to the external signer binary.
|
||||||
|
func (p *Plugin) GenerateToken(ctx context.Context, claims *jwt.Claims, privateClaims interface{}) (string, error) {
|
||||||
|
jwt, err := p.signAndAssembleJWT(ctx, claims, privateClaims)
|
||||||
|
externaljwtmetrics.RecordTokenGenAttempt(err)
|
||||||
|
return jwt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) signAndAssembleJWT(ctx context.Context, claims *jwt.Claims, privateClaims interface{}) (string, error) {
|
||||||
|
payload, err := mergeClaims(p.iss, claims, privateClaims)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("while merging claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBase64 := base64.RawURLEncoding.EncodeToString(payload)
|
||||||
|
|
||||||
|
request := &externaljwtv1alpha1.SignJWTRequest{
|
||||||
|
Claims: payloadBase64,
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := p.client.Sign(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("while signing jwt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.validateJWTHeader(ctx, response); err != nil {
|
||||||
|
return "", fmt.Errorf("while validating header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(response.Signature) == 0 {
|
||||||
|
return "", fmt.Errorf("empty signature returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Header + "." + payloadBase64 + "." + response.Signature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServiceMetadata returns metadata associated with externalJWTSigner
|
||||||
|
// It Includes details like max token lifetime supported by externalJWTSigner, etc.
|
||||||
|
func (p *Plugin) GetServiceMetadata(ctx context.Context) (*externaljwtv1alpha1.MetadataResponse, error) {
|
||||||
|
req := &externaljwtv1alpha1.MetadataRequest{}
|
||||||
|
return p.client.Metadata(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) validateJWTHeader(ctx context.Context, response *externaljwtv1alpha1.SignJWTResponse) error {
|
||||||
|
jsonBytes, err := base64.RawURLEncoding.DecodeString(response.Header)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while unwrapping header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(bytes.NewBuffer(jsonBytes))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
|
||||||
|
header := &struct {
|
||||||
|
Algorithm string `json:"alg,omitempty"`
|
||||||
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
Type string `json:"typ,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := decoder.Decode(header); err != nil {
|
||||||
|
return fmt.Errorf("while parsing header JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if header.Type != "JWT" {
|
||||||
|
return fmt.Errorf("bad type")
|
||||||
|
}
|
||||||
|
if len(header.KeyID) == 0 {
|
||||||
|
return fmt.Errorf("key id missing")
|
||||||
|
}
|
||||||
|
if len(header.KeyID) > 1024 {
|
||||||
|
return fmt.Errorf("key id longer than 1 kb")
|
||||||
|
}
|
||||||
|
switch header.Algorithm {
|
||||||
|
// IMPORTANT: If this function is updated to support additional algorithms,
|
||||||
|
// JWTTokenGenerator, signerFromRSAPrivateKey, signerFromECDSAPrivateKey in
|
||||||
|
// kubernetes/pkg/serviceaccount/jwt.go must also be updated to support the same Algorithms.
|
||||||
|
case "RS256", "ES256", "ES384", "ES512":
|
||||||
|
// OK
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("bad signing algorithm %q", header.Algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.allowSigningWithNonOIDCKeys {
|
||||||
|
publicKeys := p.keyCache.GetPublicKeys(ctx, header.KeyID)
|
||||||
|
for _, key := range publicKeys {
|
||||||
|
// Such keys shall only be used for validating formerly issued tokens.
|
||||||
|
if key.ExcludeFromOIDCDiscovery {
|
||||||
|
return fmt.Errorf("key used for signing JWT (kid: %s) is excluded from OIDC discovery docs", header.KeyID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeClaims(iss string, claims *jwt.Claims, privateClaims interface{}) ([]byte, error) {
|
||||||
|
var out []byte
|
||||||
|
signer := payloadGrabber(func(payload []byte) { out = payload })
|
||||||
|
_, err := serviceaccount.GenerateToken(signer, iss, claims, privateClaims)
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to marshal: %w", err)
|
||||||
|
}
|
||||||
|
return out, nil // error is safe to ignore as long as we have the payload bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jose.Signer = payloadGrabber(nil)
|
||||||
|
|
||||||
|
type payloadGrabber func(payload []byte)
|
||||||
|
|
||||||
|
func (p payloadGrabber) Sign(payload []byte) (*jose.JSONWebSignature, error) {
|
||||||
|
p(payload)
|
||||||
|
return nil, jose.ErrUnprotectedNonce // return some error to stop after we have the payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p payloadGrabber) Options() jose.SignerOptions { return jose.SignerOptions{} }
|
||||||
450
pkg/serviceaccount/externaljwt/plugin/plugin_test.go
Normal file
450
pkg/serviceaccount/externaljwt/plugin/plugin_test.go
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
|
|
||||||
|
externaljwtv1alpha1 "k8s.io/externaljwt/apis/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rsaKey1 *rsa.PrivateKey
|
||||||
|
rsaKey2 *rsa.PrivateKey
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
rsaKey1, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
panic("Error while generating first RSA key")
|
||||||
|
}
|
||||||
|
|
||||||
|
rsaKey2, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
panic("Error while generating second RSA key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExternalTokenGenerator(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
|
||||||
|
publicClaims jwt.Claims
|
||||||
|
privateClaims privateClaimsT
|
||||||
|
|
||||||
|
iss string
|
||||||
|
backendSetKeyID string
|
||||||
|
backendSetAlgorithm string
|
||||||
|
supportedKeys map[string]supportedKeyT
|
||||||
|
allowSigningWithNonOIDCKeys bool
|
||||||
|
|
||||||
|
wantClaims unifiedClaimsT
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "correct token with correct claims returned",
|
||||||
|
publicClaims: jwt.Claims{
|
||||||
|
Subject: "some-subject",
|
||||||
|
Audience: jwt.Audience{
|
||||||
|
"some-audience-1",
|
||||||
|
"some-audience-2",
|
||||||
|
},
|
||||||
|
ID: "id-1",
|
||||||
|
},
|
||||||
|
privateClaims: privateClaimsT{
|
||||||
|
Kubernetes: kubernetesT{
|
||||||
|
Namespace: "foo",
|
||||||
|
Svcacct: refT{
|
||||||
|
Name: "default",
|
||||||
|
UID: "abcdef",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iss: "some-issuer",
|
||||||
|
backendSetKeyID: "key-id-1",
|
||||||
|
backendSetAlgorithm: "RS256",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-id-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
wantClaims: unifiedClaimsT{
|
||||||
|
Issuer: "some-issuer",
|
||||||
|
Subject: "some-subject",
|
||||||
|
Audience: jwt.Audience{
|
||||||
|
"some-audience-1",
|
||||||
|
"some-audience-2",
|
||||||
|
},
|
||||||
|
ID: "id-1",
|
||||||
|
Kubernetes: kubernetesT{
|
||||||
|
Namespace: "foo",
|
||||||
|
Svcacct: refT{
|
||||||
|
Name: "default",
|
||||||
|
UID: "abcdef",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "correct token with correct claims signed by key that's excluded from OIDC",
|
||||||
|
publicClaims: jwt.Claims{
|
||||||
|
Subject: "some-subject",
|
||||||
|
Audience: jwt.Audience{
|
||||||
|
"some-audience-1",
|
||||||
|
"some-audience-2",
|
||||||
|
},
|
||||||
|
ID: "id-1",
|
||||||
|
},
|
||||||
|
privateClaims: privateClaimsT{
|
||||||
|
Kubernetes: kubernetesT{
|
||||||
|
Namespace: "foo",
|
||||||
|
Svcacct: refT{
|
||||||
|
Name: "default",
|
||||||
|
UID: "abcdef",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iss: "some-issuer",
|
||||||
|
backendSetKeyID: "key-id-1",
|
||||||
|
backendSetAlgorithm: "RS256",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-id-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
wantErr: fmt.Errorf("while validating header: key used for signing JWT (kid: key-id-1) is excluded from OIDC discovery docs"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "token signed with key that's excluded from OIDC but validation is disabled",
|
||||||
|
publicClaims: jwt.Claims{
|
||||||
|
Subject: "some-subject",
|
||||||
|
Audience: jwt.Audience{
|
||||||
|
"some-audience-1",
|
||||||
|
"some-audience-2",
|
||||||
|
},
|
||||||
|
ID: "key-id-1",
|
||||||
|
},
|
||||||
|
privateClaims: privateClaimsT{
|
||||||
|
Kubernetes: kubernetesT{
|
||||||
|
Namespace: "foo",
|
||||||
|
Svcacct: refT{
|
||||||
|
Name: "default",
|
||||||
|
UID: "abcdef",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iss: "some-issuer",
|
||||||
|
backendSetKeyID: "key-id-1",
|
||||||
|
backendSetAlgorithm: "RS256",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-id-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowSigningWithNonOIDCKeys: true,
|
||||||
|
|
||||||
|
wantClaims: unifiedClaimsT{
|
||||||
|
Issuer: "some-issuer",
|
||||||
|
Subject: "some-subject",
|
||||||
|
Audience: jwt.Audience{
|
||||||
|
"some-audience-1",
|
||||||
|
"some-audience-2",
|
||||||
|
},
|
||||||
|
ID: "key-id-1",
|
||||||
|
Kubernetes: kubernetesT{
|
||||||
|
Namespace: "foo",
|
||||||
|
Svcacct: refT{
|
||||||
|
Name: "default",
|
||||||
|
UID: "abcdef",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty key ID returned from signer",
|
||||||
|
iss: "some-issuer",
|
||||||
|
backendSetKeyID: "",
|
||||||
|
backendSetAlgorithm: "RS256",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-id-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: fmt.Errorf("while validating header: key id missing"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "key id longer than 1024 bytes returned from signer",
|
||||||
|
iss: "some-issuer",
|
||||||
|
backendSetKeyID: string(make([]byte, 1025)),
|
||||||
|
backendSetAlgorithm: "RS256",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-id-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: fmt.Errorf("while validating header: key id longer than 1 kb"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "unsupported alg returned from signer",
|
||||||
|
iss: "some-issuer",
|
||||||
|
backendSetKeyID: "key-id-1",
|
||||||
|
backendSetAlgorithm: "something-unsupported",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-id-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: fmt.Errorf("while validating header: bad signing algorithm \"something-unsupported\""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty alg returned from signer",
|
||||||
|
iss: "some-issuer",
|
||||||
|
backendSetKeyID: "key-id-1",
|
||||||
|
backendSetAlgorithm: "",
|
||||||
|
supportedKeys: map[string]supportedKeyT{
|
||||||
|
"key-id-1": {
|
||||||
|
key: &rsaKey1.PublicKey,
|
||||||
|
excludeFromOidc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: fmt.Errorf("while validating header: bad signing algorithm \"\""),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sockname := fmt.Sprintf("@test-external-token-generator-%d.sock", i)
|
||||||
|
t.Cleanup(func() { _ = os.Remove(sockname) })
|
||||||
|
|
||||||
|
addr := &net.UnixAddr{Name: sockname, Net: "unix"}
|
||||||
|
listener, err := net.ListenUnix(addr.Network(), addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start fake backend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer()
|
||||||
|
|
||||||
|
backend := &dummyExtrnalSigner{
|
||||||
|
keyID: tc.backendSetKeyID,
|
||||||
|
signingAlgorithm: tc.backendSetAlgorithm,
|
||||||
|
signature: "abcdef",
|
||||||
|
supportedKeys: tc.supportedKeys,
|
||||||
|
refreshHintSeconds: 10,
|
||||||
|
DataTimeStamp: timestamppb.New(time.Time{}),
|
||||||
|
}
|
||||||
|
externaljwtv1alpha1.RegisterExternalJWTSignerServer(grpcServer, backend)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := grpcServer.Serve(listener); err != nil {
|
||||||
|
panic(fmt.Errorf("error returned from grpcServer: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer grpcServer.Stop()
|
||||||
|
|
||||||
|
clientConn, err := grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
sockname,
|
||||||
|
grpc.WithContextDialer(func(ctx context.Context, path string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, "unix", path)
|
||||||
|
}),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to dial buffconn client: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = clientConn.Close() }()
|
||||||
|
|
||||||
|
plugin := newPlugin(tc.iss, clientConn, tc.allowSigningWithNonOIDCKeys)
|
||||||
|
err = plugin.keyCache.initialFill(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("initial fill failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotToken, err := plugin.GenerateToken(ctx, &tc.publicClaims, tc.privateClaims)
|
||||||
|
if err != nil && tc.wantErr != nil {
|
||||||
|
if err.Error() != tc.wantErr.Error() {
|
||||||
|
t.Fatalf("want error: %v, got error: %v", tc.wantErr, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if err != nil && tc.wantErr == nil {
|
||||||
|
t.Fatalf("Unexpected error generating token: %v", err)
|
||||||
|
} else if err == nil && tc.wantErr != nil {
|
||||||
|
t.Fatalf("Wanted error %q, but got nil", tc.wantErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenPieces := strings.Split(gotToken, ".")
|
||||||
|
payloadBase64 := tokenPieces[1]
|
||||||
|
|
||||||
|
gotClaimBytes, err := base64.RawURLEncoding.DecodeString(payloadBase64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error converting received tokens to bytes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotClaims := unifiedClaimsT{}
|
||||||
|
if err := json.Unmarshal(gotClaimBytes, &gotClaims); err != nil {
|
||||||
|
t.Fatalf("Error while unmarshaling claims from backend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(gotClaims, tc.wantClaims); diff != "" {
|
||||||
|
t.Fatalf("Bad claims; diff (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't check header or signature values since we're not testing
|
||||||
|
// our (fake) backends.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortPublicKeySlice(a, b serviceaccount.PublicKey) bool {
|
||||||
|
return a.KeyID < b.KeyID
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerT struct {
|
||||||
|
Algorithm string `json:"alg"`
|
||||||
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
Type string `json:"typ"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type unifiedClaimsT struct {
|
||||||
|
Issuer string `json:"iss,omitempty"`
|
||||||
|
Subject string `json:"sub,omitempty"`
|
||||||
|
Audience jwt.Audience `json:"aud,omitempty"`
|
||||||
|
Expiry *jwt.NumericDate `json:"exp,omitempty"`
|
||||||
|
NotBefore *jwt.NumericDate `json:"nbf,omitempty"`
|
||||||
|
IssuedAt *jwt.NumericDate `json:"iat,omitempty"`
|
||||||
|
ID string `json:"jti,omitempty"`
|
||||||
|
Kubernetes kubernetesT `json:"kubernetes.io,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type privateClaimsT struct {
|
||||||
|
Kubernetes kubernetesT `json:"kubernetes.io,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type kubernetesT struct {
|
||||||
|
Namespace string `json:"namespace,omitempty"`
|
||||||
|
Svcacct refT `json:"serviceaccount,omitempty"`
|
||||||
|
Pod *refT `json:"pod,omitempty"`
|
||||||
|
Secret *refT `json:"secret,omitempty"`
|
||||||
|
Node *refT `json:"node,omitempty"`
|
||||||
|
WarnAfter *jwt.NumericDate `json:"warnafter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type refT struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
UID string `json:"uid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type supportedKeyT struct {
|
||||||
|
key *rsa.PublicKey
|
||||||
|
excludeFromOidc bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyExtrnalSigner struct {
|
||||||
|
externaljwtv1alpha1.UnimplementedExternalJWTSignerServer
|
||||||
|
|
||||||
|
// required for Sign()
|
||||||
|
keyID string
|
||||||
|
signingAlgorithm string
|
||||||
|
signature string
|
||||||
|
|
||||||
|
// required for FetchKeys()
|
||||||
|
keyLock sync.Mutex
|
||||||
|
supportedKeys map[string]supportedKeyT
|
||||||
|
refreshHintSeconds int
|
||||||
|
DataTimeStamp *timestamppb.Timestamp
|
||||||
|
SupportedKeysOverride []*externaljwtv1alpha1.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (des *dummyExtrnalSigner) Sign(ctx context.Context, r *externaljwtv1alpha1.SignJWTRequest) (*externaljwtv1alpha1.SignJWTResponse, error) {
|
||||||
|
header := &headerT{
|
||||||
|
Type: "JWT",
|
||||||
|
Algorithm: des.signingAlgorithm,
|
||||||
|
KeyID: des.keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
headerJSON, err := json.Marshal(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create header for JWT response")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &externaljwtv1alpha1.SignJWTResponse{
|
||||||
|
Header: base64.RawURLEncoding.EncodeToString(headerJSON),
|
||||||
|
Signature: des.signature,
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (des *dummyExtrnalSigner) FetchKeys(ctx context.Context, r *externaljwtv1alpha1.FetchKeysRequest) (*externaljwtv1alpha1.FetchKeysResponse, error) {
|
||||||
|
des.keyLock.Lock()
|
||||||
|
defer des.keyLock.Unlock()
|
||||||
|
|
||||||
|
pbKeys := []*externaljwtv1alpha1.Key{}
|
||||||
|
if des.SupportedKeysOverride != nil {
|
||||||
|
pbKeys = des.SupportedKeysOverride
|
||||||
|
} else {
|
||||||
|
for kid, k := range des.supportedKeys {
|
||||||
|
keyBytes, err := x509.MarshalPKIXPublicKey(k.key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("while marshaling key: %w", err)
|
||||||
|
}
|
||||||
|
pbKey := &externaljwtv1alpha1.Key{
|
||||||
|
KeyId: kid,
|
||||||
|
Key: keyBytes,
|
||||||
|
ExcludeFromOidcDiscovery: k.excludeFromOidc,
|
||||||
|
}
|
||||||
|
pbKeys = append(pbKeys, pbKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &externaljwtv1alpha1.FetchKeysResponse{
|
||||||
|
Keys: pbKeys,
|
||||||
|
DataTimestamp: des.DataTimeStamp,
|
||||||
|
RefreshHintSeconds: int64(des.refreshHintSeconds),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"k8s.io/externaljwt/apis/v1alpha1"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockSigner struct {
|
||||||
|
socketPath string
|
||||||
|
server *grpc.Server
|
||||||
|
listener net.Listener
|
||||||
|
|
||||||
|
SigningKey *rsa.PrivateKey
|
||||||
|
SigningKeyID string
|
||||||
|
SigningAlg string
|
||||||
|
TokenType string
|
||||||
|
SupportedKeys atomic.Pointer[map[string]KeyT]
|
||||||
|
AckKeyFetch chan bool
|
||||||
|
MaxTokenExpirationSeconds int64
|
||||||
|
|
||||||
|
FetchError error
|
||||||
|
MetadataError error
|
||||||
|
errorLock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyT struct {
|
||||||
|
Key []byte
|
||||||
|
ExcludeFromOidcDiscovery bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockSigner starts and returns a new MockSigner
|
||||||
|
// It servers on the provided socket.
|
||||||
|
func NewMockSigner(t *testing.T, socketPath string) *MockSigner {
|
||||||
|
server := grpc.NewServer()
|
||||||
|
|
||||||
|
m := &MockSigner{
|
||||||
|
socketPath: socketPath,
|
||||||
|
server: server,
|
||||||
|
AckKeyFetch: make(chan bool),
|
||||||
|
MaxTokenExpirationSeconds: 10 * 60, // 10m
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Reset(); err != nil {
|
||||||
|
t.Fatalf("failed to load keys for mock signer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v1alpha1.RegisterExternalJWTSignerServer(server, m)
|
||||||
|
if err := m.start(t); err != nil {
|
||||||
|
t.Fatalf("failed to start Mock Signer with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(m.CleanUp)
|
||||||
|
if err := m.waitForMockServerToStart(); err != nil {
|
||||||
|
t.Fatalf("failed to start Mock Signer with error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSigner) Sign(ctx context.Context, req *v1alpha1.SignJWTRequest) (*v1alpha1.SignJWTResponse, error) {
|
||||||
|
|
||||||
|
header := &struct {
|
||||||
|
Algorithm string `json:"alg,omitempty"`
|
||||||
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
Type string `json:"typ,omitempty"`
|
||||||
|
}{
|
||||||
|
Type: m.TokenType,
|
||||||
|
Algorithm: m.SigningAlg,
|
||||||
|
KeyID: m.SigningKeyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
headerJSON, err := json.Marshal(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create header for JWT response")
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Header := base64.RawURLEncoding.EncodeToString(headerJSON)
|
||||||
|
|
||||||
|
toBeSignedHash := hashBytes([]byte(base64Header + "." + req.Claims))
|
||||||
|
|
||||||
|
signature, err := rsa.SignPKCS1v15(rand.Reader, m.SigningKey, crypto.SHA256, toBeSignedHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to sign payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &v1alpha1.SignJWTResponse{
|
||||||
|
Header: base64Header,
|
||||||
|
Signature: base64.RawURLEncoding.EncodeToString(signature),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSigner) FetchKeys(ctx context.Context, req *v1alpha1.FetchKeysRequest) (*v1alpha1.FetchKeysResponse, error) {
|
||||||
|
m.errorLock.RLocker().Lock()
|
||||||
|
defer m.errorLock.RLocker().Unlock()
|
||||||
|
if m.FetchError != nil {
|
||||||
|
return nil, m.FetchError
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := []*v1alpha1.Key{}
|
||||||
|
|
||||||
|
for id, k := range *m.SupportedKeys.Load() {
|
||||||
|
keys = append(keys, &v1alpha1.Key{
|
||||||
|
KeyId: id,
|
||||||
|
Key: k.Key,
|
||||||
|
ExcludeFromOidcDiscovery: k.ExcludeFromOidcDiscovery,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.AckKeyFetch:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return &v1alpha1.FetchKeysResponse{
|
||||||
|
RefreshHintSeconds: 5,
|
||||||
|
DataTimestamp: ×tamppb.Timestamp{Seconds: time.Now().Unix()},
|
||||||
|
Keys: keys,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockSigner) Metadata(ctx context.Context, req *v1alpha1.MetadataRequest) (*v1alpha1.MetadataResponse, error) {
|
||||||
|
m.errorLock.RLocker().Lock()
|
||||||
|
defer m.errorLock.RLocker().Unlock()
|
||||||
|
if m.MetadataError != nil {
|
||||||
|
return nil, m.MetadataError
|
||||||
|
}
|
||||||
|
return &v1alpha1.MetadataResponse{
|
||||||
|
MaxTokenExpirationSeconds: m.MaxTokenExpirationSeconds,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset genrate and adds signing/supported keys to MockSigner instance.
|
||||||
|
func (m *MockSigner) Reset() error {
|
||||||
|
|
||||||
|
priv1, pub1, err := generateKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, pub2, err := generateKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, pub3, err := generateKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SigningKey = priv1
|
||||||
|
m.SigningKeyID = "kid-1"
|
||||||
|
m.SigningAlg = "RS256"
|
||||||
|
m.TokenType = "JWT"
|
||||||
|
m.SupportedKeys.Store(&map[string]KeyT{
|
||||||
|
"kid-1": {Key: pub1},
|
||||||
|
"kid-2": {Key: pub2},
|
||||||
|
"kid-3": {Key: pub3},
|
||||||
|
})
|
||||||
|
m.errorLock.Lock()
|
||||||
|
defer m.errorLock.Unlock()
|
||||||
|
m.FetchError = nil
|
||||||
|
m.MetadataError = nil
|
||||||
|
m.MaxTokenExpirationSeconds = 10 * 60 // 10m
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// start makes the gRpc MockServer listen on unix socket.
|
||||||
|
func (m *MockSigner) start(t *testing.T) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
m.listener, err = net.Listen("unix", m.socketPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to listen on the unix socket, error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
klog.Infof("Starting Mock Signer at socketPath %s", m.socketPath)
|
||||||
|
go func() {
|
||||||
|
if err := m.server.Serve(m.listener); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
klog.Infof("Mock Signer listening at socketPath %s", m.socketPath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForMockServerToStart waits until Mock signer is ready to server.
|
||||||
|
// waits for a max of 30s before failing.
|
||||||
|
func (m *MockSigner) waitForMockServerToStart() error {
|
||||||
|
var gRPCErr error
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
doneCh := ctx.Done()
|
||||||
|
|
||||||
|
for range 30 {
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
return fmt.Errorf("failed to start Mock signer: %w", ctx.Err())
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if _, gRPCErr = m.FetchKeys(context.Background(), &v1alpha1.FetchKeysRequest{}); gRPCErr == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gRPCErr != nil {
|
||||||
|
return fmt.Errorf("failed to start Mock signer, gRPC error: %w", gRPCErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp stops gRPC server and the underlying listener.
|
||||||
|
func (m *MockSigner) CleanUp() {
|
||||||
|
m.server.Stop()
|
||||||
|
_ = m.listener.Close()
|
||||||
|
_ = os.Remove(m.socketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKeyPair() (*rsa.PrivateKey, []byte, error) {
|
||||||
|
|
||||||
|
// Generate a new private key
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
klog.Errorf("Error generating private key: %v", err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
klog.Errorf("Error marshaling public key: %v", err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateKey, publicKeyBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashBytes(bytes []byte) []byte {
|
||||||
|
hasher := crypto.SHA256.New()
|
||||||
|
hasher.Write(bytes)
|
||||||
|
return hasher.Sum(nil)
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ type TokenGenerator interface {
|
|||||||
// the payload object. Public claims take precedent over private
|
// the payload object. Public claims take precedent over private
|
||||||
// claims i.e. if both claims and privateClaims have an "exp" field,
|
// claims i.e. if both claims and privateClaims have an "exp" field,
|
||||||
// the value in claims will be used.
|
// the value in claims will be used.
|
||||||
GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error)
|
GenerateToken(ctx context.Context, claims *jwt.Claims, privateClaims interface{}) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.
|
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.
|
||||||
@@ -118,8 +118,9 @@ func signerFromRSAPrivateKey(keyPair *rsa.PrivateKey) (jose.Signer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: If this function is updated to support additional key sizes,
|
// IMPORTANT: If this function is updated to support additional key sizes,
|
||||||
// algorithmForPublicKey in serviceaccount/openidmetadata.go must also be
|
// algorithmForPublicKey in serviceaccount/openidmetadata.go and
|
||||||
// updated to support the same key sizes. Today we only support RS256.
|
// validateJWTHeader in externaljwt/pkg/plugin/plugin.go must also
|
||||||
|
// be updated to support the same key sizes. Today we only support RS256.
|
||||||
|
|
||||||
// Wrap the RSA keypair in a JOSE JWK with the designated key ID.
|
// Wrap the RSA keypair in a JOSE JWK with the designated key ID.
|
||||||
privateJWK := &jose.JSONWebKey{
|
privateJWK := &jose.JSONWebKey{
|
||||||
@@ -146,6 +147,11 @@ func signerFromRSAPrivateKey(keyPair *rsa.PrivateKey) (jose.Signer, error) {
|
|||||||
|
|
||||||
func signerFromECDSAPrivateKey(keyPair *ecdsa.PrivateKey) (jose.Signer, error) {
|
func signerFromECDSAPrivateKey(keyPair *ecdsa.PrivateKey) (jose.Signer, error) {
|
||||||
var alg jose.SignatureAlgorithm
|
var alg jose.SignatureAlgorithm
|
||||||
|
|
||||||
|
// IMPORTANT: If this function is updated to support additional algorithms,
|
||||||
|
// validateJWTHeader in externaljwt/pkg/plugin/plugin.go must also be updated
|
||||||
|
// to support the same Algorithms. Today we only support "ES256", "ES384", "ES512".
|
||||||
|
|
||||||
switch keyPair.Curve {
|
switch keyPair.Curve {
|
||||||
case elliptic.P256():
|
case elliptic.P256():
|
||||||
alg = jose.ES256
|
alg = jose.ES256
|
||||||
@@ -211,15 +217,8 @@ type jwtTokenGenerator struct {
|
|||||||
signer jose.Signer
|
signer jose.Signer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jwtTokenGenerator) GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error) {
|
func (j *jwtTokenGenerator) GenerateToken(ctx context.Context, claims *jwt.Claims, privateClaims interface{}) (string, error) {
|
||||||
// claims are applied in reverse precedence
|
return GenerateToken(j.signer, j.iss, claims, privateClaims)
|
||||||
return jwt.Signed(j.signer).
|
|
||||||
Claims(privateClaims).
|
|
||||||
Claims(claims).
|
|
||||||
Claims(&jwt.Claims{
|
|
||||||
Issuer: j.iss,
|
|
||||||
}).
|
|
||||||
CompactSerialize()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
|
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
|
||||||
@@ -257,12 +256,13 @@ type PublicKeysGetter interface {
|
|||||||
|
|
||||||
// GetPublicKeys returns public keys to use for verifying a token with the given key id.
|
// GetPublicKeys returns public keys to use for verifying a token with the given key id.
|
||||||
// keyIDHint may be empty if the token did not have a kid header, or if all public keys are desired.
|
// keyIDHint may be empty if the token did not have a kid header, or if all public keys are desired.
|
||||||
GetPublicKeys(keyIDHint string) []PublicKey
|
GetPublicKeys(ctx context.Context, keyIDHint string) []PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublicKey struct {
|
type PublicKey struct {
|
||||||
KeyID string
|
KeyID string
|
||||||
PublicKey interface{}
|
PublicKey interface{}
|
||||||
|
ExcludeFromOIDCDiscovery bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type staticPublicKeysGetter struct {
|
type staticPublicKeysGetter struct {
|
||||||
@@ -306,7 +306,7 @@ func (s staticPublicKeysGetter) GetCacheAgeMaxSeconds() int {
|
|||||||
return 3600
|
return 3600
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s staticPublicKeysGetter) GetPublicKeys(keyID string) []PublicKey {
|
func (s staticPublicKeysGetter) GetPublicKeys(ctx context.Context, keyID string) []PublicKey {
|
||||||
if len(keyID) == 0 {
|
if len(keyID) == 0 {
|
||||||
return s.allPublicKeys
|
return s.allPublicKeys
|
||||||
}
|
}
|
||||||
@@ -357,7 +357,7 @@ func (j *jwtTokenAuthenticator[PrivateClaims]) AuthenticateToken(ctx context.Con
|
|||||||
found bool
|
found bool
|
||||||
errlist []error
|
errlist []error
|
||||||
)
|
)
|
||||||
keys := j.keysGetter.GetPublicKeys(kid)
|
keys := j.keysGetter.GetPublicKeys(ctx, kid)
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
return nil, false, fmt.Errorf("invalid signature, no keys found")
|
return nil, false, fmt.Errorf("invalid signature, no keys found")
|
||||||
}
|
}
|
||||||
@@ -438,3 +438,15 @@ func (j *jwtTokenAuthenticator[PrivateClaims]) hasCorrectIssuer(tokenData string
|
|||||||
}
|
}
|
||||||
return j.issuers[claims.Issuer]
|
return j.issuers[claims.Issuer]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateToken is shared between internal and external signer code to ensure that claim merging logic remains consistent between them.
|
||||||
|
func GenerateToken(signer jose.Signer, iss string, claims *jwt.Claims, privateClaims interface{}) (string, error) {
|
||||||
|
// claims are applied in reverse precedence
|
||||||
|
return jwt.Signed(signer).
|
||||||
|
Claims(privateClaims).
|
||||||
|
Claims(claims).
|
||||||
|
Claims(&jwt.Claims{
|
||||||
|
Issuer: iss,
|
||||||
|
}).
|
||||||
|
CompactSerialize()
|
||||||
|
}
|
||||||
|
|||||||
@@ -174,7 +174,8 @@ func TestTokenGenerateAndValidate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error making generator: %v", err)
|
t.Fatalf("error making generator: %v", err)
|
||||||
}
|
}
|
||||||
rsaToken, err := rsaGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret))
|
c, pc := serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret)
|
||||||
|
rsaToken, err := rsaGenerator.GenerateToken(context.TODO(), c, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error generating token: %v", err)
|
t.Fatalf("error generating token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -188,7 +189,8 @@ func TestTokenGenerateAndValidate(t *testing.T) {
|
|||||||
checkJSONWebSignatureHasKeyID(t, rsaToken, rsaKeyID)
|
checkJSONWebSignatureHasKeyID(t, rsaToken, rsaKeyID)
|
||||||
|
|
||||||
// Generate RSA token with invalidAutoSecret
|
// Generate RSA token with invalidAutoSecret
|
||||||
invalidAutoSecretToken, err := rsaGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *invalidAutoSecret))
|
c, pc = serviceaccount.LegacyClaims(*serviceAccount, *invalidAutoSecret)
|
||||||
|
invalidAutoSecretToken, err := rsaGenerator.GenerateToken(context.TODO(), c, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error generating token: %v", err)
|
t.Fatalf("error generating token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -217,7 +219,8 @@ func TestTokenGenerateAndValidate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error making generator: %v", err)
|
t.Fatalf("error making generator: %v", err)
|
||||||
}
|
}
|
||||||
badIssuerToken, err := badIssuerGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret))
|
c, pc = serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret)
|
||||||
|
badIssuerToken, err := badIssuerGenerator.GenerateToken(context.TODO(), c, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error generating token: %v", err)
|
t.Fatalf("error generating token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -227,7 +230,8 @@ func TestTokenGenerateAndValidate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error making generator: %v", err)
|
t.Fatalf("error making generator: %v", err)
|
||||||
}
|
}
|
||||||
differentIssuerToken, err := differentIssuerGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret))
|
c, pc = serviceaccount.LegacyClaims(*serviceAccount, *rsaSecret)
|
||||||
|
differentIssuerToken, err := differentIssuerGenerator.GenerateToken(context.TODO(), c, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error generating token: %v", err)
|
t.Fatalf("error generating token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -394,7 +398,7 @@ func TestTokenGenerateAndValidate(t *testing.T) {
|
|||||||
authn := serviceaccount.JWTTokenAuthenticator([]string{serviceaccount.LegacyIssuer, "bar"}, keysGetter, auds, validator)
|
authn := serviceaccount.JWTTokenAuthenticator([]string{serviceaccount.LegacyIssuer, "bar"}, keysGetter, auds, validator)
|
||||||
|
|
||||||
// An invalid, non-JWT token should always fail
|
// An invalid, non-JWT token should always fail
|
||||||
ctx := authenticator.WithAudiences(context.Background(), auds)
|
ctx := authenticator.WithAudiences(context.TODO(), auds)
|
||||||
if _, ok, err := authn.AuthenticateToken(ctx, "invalid token"); err != nil || ok {
|
if _, ok, err := authn.AuthenticateToken(ctx, "invalid token"); err != nil || ok {
|
||||||
t.Errorf("%s: Expected err=nil, ok=false for non-JWT token", k)
|
t.Errorf("%s: Expected err=nil, ok=false for non-JWT token", k)
|
||||||
continue
|
continue
|
||||||
@@ -445,15 +449,15 @@ type keyIDPrefixer struct {
|
|||||||
keyIDPrefix string
|
keyIDPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *keyIDPrefixer) GetPublicKeys(keyIDHint string) []serviceaccount.PublicKey {
|
func (k *keyIDPrefixer) GetPublicKeys(ctx context.Context, keyIDHint string) []serviceaccount.PublicKey {
|
||||||
if k.keyIDPrefix == "" {
|
if k.keyIDPrefix == "" {
|
||||||
return k.PublicKeysGetter.GetPublicKeys(keyIDHint)
|
return k.PublicKeysGetter.GetPublicKeys(context.TODO(), keyIDHint)
|
||||||
}
|
}
|
||||||
if keyIDHint != "" {
|
if keyIDHint != "" {
|
||||||
keyIDHint = k.keyIDPrefix + keyIDHint
|
keyIDHint = k.keyIDPrefix + keyIDHint
|
||||||
}
|
}
|
||||||
var retval []serviceaccount.PublicKey
|
var retval []serviceaccount.PublicKey
|
||||||
for _, key := range k.PublicKeysGetter.GetPublicKeys(keyIDHint) {
|
for _, key := range k.PublicKeysGetter.GetPublicKeys(context.TODO(), keyIDHint) {
|
||||||
key.KeyID = k.keyIDPrefix + key.KeyID
|
key.KeyID = k.keyIDPrefix + key.KeyID
|
||||||
retval = append(retval, key)
|
retval = append(retval, key)
|
||||||
}
|
}
|
||||||
@@ -503,7 +507,8 @@ func generateECDSAToken(t *testing.T, iss string, serviceAccount *v1.ServiceAcco
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error making generator: %v", err)
|
t.Fatalf("error making generator: %v", err)
|
||||||
}
|
}
|
||||||
ecdsaToken, err := ecdsaGenerator.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *ecdsaSecret))
|
c, pc := serviceaccount.LegacyClaims(*serviceAccount, *ecdsaSecret)
|
||||||
|
ecdsaToken, err := ecdsaGenerator.GenerateToken(context.TODO(), c, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error generating token: %v", err)
|
t.Fatalf("error generating token: %v", err)
|
||||||
}
|
}
|
||||||
@@ -590,17 +595,17 @@ func TestStaticPublicKeysGetter(t *testing.T) {
|
|||||||
t.Fatalf("unexpected construction error: %v", err)
|
t.Fatalf("unexpected construction error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bogusKeys := getter.GetPublicKeys("bogus")
|
bogusKeys := getter.GetPublicKeys(context.TODO(), "bogus")
|
||||||
if len(bogusKeys) != 0 {
|
if len(bogusKeys) != 0 {
|
||||||
t.Fatalf("unexpected bogus keys: %#v", bogusKeys)
|
t.Fatalf("unexpected bogus keys: %#v", bogusKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
allKeys := getter.GetPublicKeys("")
|
allKeys := getter.GetPublicKeys(context.TODO(), "")
|
||||||
if !reflect.DeepEqual(tc.ExpectKeys, allKeys) {
|
if !reflect.DeepEqual(tc.ExpectKeys, allKeys) {
|
||||||
t.Fatalf("unexpected keys: %#v", allKeys)
|
t.Fatalf("unexpected keys: %#v", allKeys)
|
||||||
}
|
}
|
||||||
for _, key := range allKeys {
|
for _, key := range allKeys {
|
||||||
keysByID := getter.GetPublicKeys(key.KeyID)
|
keysByID := getter.GetPublicKeys(context.TODO(), key.KeyID)
|
||||||
if len(keysByID) != 1 {
|
if len(keysByID) != 1 {
|
||||||
t.Fatalf("expected 1 key for id %s, got %d", key.KeyID, len(keysByID))
|
t.Fatalf("expected 1 key for id %s, got %d", key.KeyID, len(keysByID))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package serviceaccount
|
package serviceaccount
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
@@ -75,7 +76,17 @@ func (p *openidConfigProvider) Enqueue() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (p *openidConfigProvider) Update() error {
|
func (p *openidConfigProvider) Update() error {
|
||||||
pubKeys := p.pubKeyGetter.GetPublicKeys("")
|
pubKeys := []PublicKey{}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
unfilteredPubKeys := p.pubKeyGetter.GetPublicKeys(ctx, "")
|
||||||
|
for _, key := range unfilteredPubKeys {
|
||||||
|
if !key.ExcludeFromOIDCDiscovery {
|
||||||
|
pubKeys = append(pubKeys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(pubKeys) == 0 {
|
if len(pubKeys) == 0 {
|
||||||
return fmt.Errorf("no keys provided for validating keyset")
|
return fmt.Errorf("no keys provided for validating keyset")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package serviceaccount_test
|
package serviceaccount_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
@@ -161,10 +162,18 @@ func expectConfiguration(t *testing.T, reqURL string, want Configuration) {
|
|||||||
func TestServeKeys(t *testing.T) {
|
func TestServeKeys(t *testing.T) {
|
||||||
wantPubRSA := getPublicKey(rsaPublicKey).(*rsa.PublicKey)
|
wantPubRSA := getPublicKey(rsaPublicKey).(*rsa.PublicKey)
|
||||||
wantPubECDSA := getPublicKey(ecdsaPublicKey).(*ecdsa.PublicKey)
|
wantPubECDSA := getPublicKey(ecdsaPublicKey).(*ecdsa.PublicKey)
|
||||||
|
|
||||||
|
alternateGetter, err := serviceaccount.StaticPublicKeysGetter([]interface{}{wantPubRSA})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
var serveKeysTests = []struct {
|
var serveKeysTests = []struct {
|
||||||
Name string
|
Name string
|
||||||
Keys []interface{}
|
Keys []interface{}
|
||||||
WantKeys []jose.JSONWebKey
|
WantKeys []jose.JSONWebKey
|
||||||
|
updatedKeysGetter serviceaccount.PublicKeysGetter
|
||||||
|
WantKeysPostUpdate []jose.JSONWebKey
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "configured public keys",
|
Name: "configured public keys",
|
||||||
@@ -220,6 +229,97 @@ func TestServeKeys(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "configured public keys reacting to update",
|
||||||
|
Keys: []interface{}{
|
||||||
|
getPublicKey(rsaPublicKey),
|
||||||
|
getPublicKey(ecdsaPublicKey),
|
||||||
|
},
|
||||||
|
WantKeys: []jose.JSONWebKey{
|
||||||
|
{
|
||||||
|
Algorithm: "RS256",
|
||||||
|
Key: wantPubRSA,
|
||||||
|
KeyID: rsaKeyID,
|
||||||
|
Use: "sig",
|
||||||
|
Certificates: []*x509.Certificate{},
|
||||||
|
CertificateThumbprintSHA1: []uint8{},
|
||||||
|
CertificateThumbprintSHA256: []uint8{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Algorithm: "ES256",
|
||||||
|
Key: wantPubECDSA,
|
||||||
|
KeyID: ecdsaKeyID,
|
||||||
|
Use: "sig",
|
||||||
|
Certificates: []*x509.Certificate{},
|
||||||
|
CertificateThumbprintSHA1: []uint8{},
|
||||||
|
CertificateThumbprintSHA256: []uint8{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updatedKeysGetter: alternateGetter,
|
||||||
|
WantKeysPostUpdate: []jose.JSONWebKey{
|
||||||
|
{
|
||||||
|
Algorithm: "RS256",
|
||||||
|
Key: wantPubRSA,
|
||||||
|
KeyID: rsaKeyID,
|
||||||
|
Use: "sig",
|
||||||
|
Certificates: []*x509.Certificate{},
|
||||||
|
CertificateThumbprintSHA1: []uint8{},
|
||||||
|
CertificateThumbprintSHA256: []uint8{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "configured public keys reacting to update while excluding keys",
|
||||||
|
Keys: []interface{}{
|
||||||
|
getPublicKey(rsaPublicKey),
|
||||||
|
getPublicKey(ecdsaPublicKey),
|
||||||
|
},
|
||||||
|
WantKeys: []jose.JSONWebKey{
|
||||||
|
{
|
||||||
|
Algorithm: "RS256",
|
||||||
|
Key: wantPubRSA,
|
||||||
|
KeyID: rsaKeyID,
|
||||||
|
Use: "sig",
|
||||||
|
Certificates: []*x509.Certificate{},
|
||||||
|
CertificateThumbprintSHA1: []uint8{},
|
||||||
|
CertificateThumbprintSHA256: []uint8{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Algorithm: "ES256",
|
||||||
|
Key: wantPubECDSA,
|
||||||
|
KeyID: ecdsaKeyID,
|
||||||
|
Use: "sig",
|
||||||
|
Certificates: []*x509.Certificate{},
|
||||||
|
CertificateThumbprintSHA1: []uint8{},
|
||||||
|
CertificateThumbprintSHA256: []uint8{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updatedKeysGetter: dummyPublicKeyGetter{
|
||||||
|
keys: []serviceaccount.PublicKey{
|
||||||
|
{
|
||||||
|
KeyID: rsaKeyID,
|
||||||
|
PublicKey: wantPubRSA,
|
||||||
|
ExcludeFromOIDCDiscovery: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
KeyID: ecdsaKeyID,
|
||||||
|
PublicKey: wantPubECDSA,
|
||||||
|
ExcludeFromOIDCDiscovery: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WantKeysPostUpdate: []jose.JSONWebKey{
|
||||||
|
{
|
||||||
|
Algorithm: "ES256",
|
||||||
|
Key: wantPubECDSA,
|
||||||
|
KeyID: ecdsaKeyID,
|
||||||
|
Use: "sig",
|
||||||
|
Certificates: []*x509.Certificate{},
|
||||||
|
CertificateThumbprintSHA1: []uint8{},
|
||||||
|
CertificateThumbprintSHA256: []uint8{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range serveKeysTests {
|
for _, tt := range serveKeysTests {
|
||||||
@@ -228,10 +328,6 @@ func TestServeKeys(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
updatedKeysGetter, err := serviceaccount.StaticPublicKeysGetter([]interface{}{wantPubRSA})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
keysGetter := &proxyKeyGetter{PublicKeysGetter: initialKeysGetter}
|
keysGetter := &proxyKeyGetter{PublicKeysGetter: initialKeysGetter}
|
||||||
s, _ := setupServer(t, exampleIssuer, keysGetter)
|
s, _ := setupServer(t, exampleIssuer, keysGetter)
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
@@ -239,23 +335,17 @@ func TestServeKeys(t *testing.T) {
|
|||||||
reqURL := s.URL + "/openid/v1/jwks"
|
reqURL := s.URL + "/openid/v1/jwks"
|
||||||
expectKeys(t, reqURL, tt.WantKeys)
|
expectKeys(t, reqURL, tt.WantKeys)
|
||||||
|
|
||||||
|
if tt.updatedKeysGetter != nil {
|
||||||
// modify the underlying keys, expect the same response
|
// modify the underlying keys, expect the same response
|
||||||
keysGetter.PublicKeysGetter = updatedKeysGetter
|
keysGetter.PublicKeysGetter = tt.updatedKeysGetter
|
||||||
expectKeys(t, reqURL, tt.WantKeys)
|
expectKeys(t, reqURL, tt.WantKeys)
|
||||||
|
|
||||||
// notify the metadata the keys changed, expected a modified response
|
// notify the metadata the keys changed, expected a modified response
|
||||||
for _, listener := range keysGetter.listeners {
|
for _, listener := range keysGetter.listeners {
|
||||||
listener.Enqueue()
|
listener.Enqueue()
|
||||||
}
|
}
|
||||||
expectKeys(t, reqURL, []jose.JSONWebKey{{
|
expectKeys(t, reqURL, tt.WantKeysPostUpdate)
|
||||||
Algorithm: "RS256",
|
}
|
||||||
Key: wantPubRSA,
|
|
||||||
KeyID: rsaKeyID,
|
|
||||||
Use: "sig",
|
|
||||||
Certificates: []*x509.Certificate{},
|
|
||||||
CertificateThumbprintSHA1: []uint8{},
|
|
||||||
CertificateThumbprintSHA256: []uint8{},
|
|
||||||
}})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,3 +569,19 @@ func TestNewOpenIDMetadata(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dummyPublicKeyGetter struct {
|
||||||
|
keys []serviceaccount.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dummyPublicKeyGetter) AddListener(listener serviceaccount.Listener) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dummyPublicKeyGetter) GetCacheAgeMaxSeconds() int {
|
||||||
|
return 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dummyPublicKeyGetter) GetPublicKeys(ctx context.Context, keyIDHint string) []serviceaccount.PublicKey {
|
||||||
|
return d.keys
|
||||||
|
}
|
||||||
|
|||||||
@@ -317,3 +317,7 @@
|
|||||||
- k8s.io/cri-client
|
- k8s.io/cri-client
|
||||||
- k8s.io/klog/v2
|
- k8s.io/klog/v2
|
||||||
- k8s.io/utils
|
- k8s.io/utils
|
||||||
|
|
||||||
|
- baseImportPath: "./staging/src/k8s.io/externaljwt"
|
||||||
|
allowedImports:
|
||||||
|
- k8s.io/externaljwt
|
||||||
|
|||||||
@@ -2441,6 +2441,13 @@ rules:
|
|||||||
branch: release-1.31
|
branch: release-1.31
|
||||||
dirs:
|
dirs:
|
||||||
- staging/src/k8s.io/endpointslice
|
- staging/src/k8s.io/endpointslice
|
||||||
|
- destination: externaljwt
|
||||||
|
branches:
|
||||||
|
- name: master
|
||||||
|
source:
|
||||||
|
branch: master
|
||||||
|
dirs:
|
||||||
|
- staging/src/k8s.io/externaljwt
|
||||||
recursive-delete-patterns:
|
recursive-delete-patterns:
|
||||||
- '*/.gitattributes'
|
- '*/.gitattributes'
|
||||||
default-go-version: 1.23.2
|
default-go-version: 1.23.2
|
||||||
|
|||||||
2
staging/src/k8s.io/externaljwt/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
2
staging/src/k8s.io/externaljwt/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Sorry, we do not accept changes directly against this repository. Please see
|
||||||
|
CONTRIBUTING.md for information on where and how to contribute instead.
|
||||||
7
staging/src/k8s.io/externaljwt/CONTRIBUTING.md
Normal file
7
staging/src/k8s.io/externaljwt/CONTRIBUTING.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Contributing guidelines
|
||||||
|
|
||||||
|
Do not open pull requests directly against this repository, they will be ignored. Instead, please open pull requests against [kubernetes/kubernetes](https://git.k8s.io/kubernetes/). Please follow the same [contributing guide](https://git.k8s.io/kubernetes/CONTRIBUTING.md) you would follow for any other pull request made to kubernetes/kubernetes.
|
||||||
|
|
||||||
|
This repository is published from [kubernetes/kubernetes/staging/src/k8s.io/externaljwt](https://git.k8s.io/kubernetes/staging/src/k8s.io/externaljwt) by the [kubernetes publishing-bot](https://git.k8s.io/publishing-bot).
|
||||||
|
|
||||||
|
Please see [Staging Directory and Publishing](https://git.k8s.io/community/contributors/devel/sig-architecture/staging.md) for more information
|
||||||
201
staging/src/k8s.io/externaljwt/LICENSE
Normal file
201
staging/src/k8s.io/externaljwt/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
8
staging/src/k8s.io/externaljwt/OWNERS
Normal file
8
staging/src/k8s.io/externaljwt/OWNERS
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# See the OWNERS docs at https://go.k8s.io/owners
|
||||||
|
|
||||||
|
approvers:
|
||||||
|
- sig-auth-serviceaccounts-approvers
|
||||||
|
reviewers:
|
||||||
|
- sig-auth-serviceaccounts-reviewers
|
||||||
|
labels:
|
||||||
|
- sig/auth
|
||||||
20
staging/src/k8s.io/externaljwt/README.md
Normal file
20
staging/src/k8s.io/externaljwt/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# ExternalJWT
|
||||||
|
|
||||||
|
This repository contains proto APIs which enable plugging external JWT signing and key management.
|
||||||
|
|
||||||
|
See [KEP 740](https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/740-service-account-external-signing) for more details.
|
||||||
|
|
||||||
|
## Community, discussion, contribution, and support
|
||||||
|
|
||||||
|
ExternalJWT a sub-project of [SIG-Auth](https://github.com/kubernetes/community/tree/master/sig-auth).
|
||||||
|
|
||||||
|
You can reach the maintainers of this project at:
|
||||||
|
|
||||||
|
- Slack: [#sig-auth](https://kubernetes.slack.com/messages/sig-auth)
|
||||||
|
- Mailing List: [kubernetes-sig-auth](https://groups.google.com/forum/#!forum/kubernetes-sig-auth)
|
||||||
|
|
||||||
|
Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/).
|
||||||
|
|
||||||
|
### Code of conduct
|
||||||
|
|
||||||
|
Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md).
|
||||||
15
staging/src/k8s.io/externaljwt/SECURITY_CONTACTS
Normal file
15
staging/src/k8s.io/externaljwt/SECURITY_CONTACTS
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Defined below are the security contacts for this repo.
|
||||||
|
#
|
||||||
|
# They are the contact point for the Product Security Committee to reach out
|
||||||
|
# to for triaging and handling of incoming issues.
|
||||||
|
#
|
||||||
|
# The below names agree to abide by the
|
||||||
|
# [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy)
|
||||||
|
# and will be removed and replaced if they violate that agreement.
|
||||||
|
#
|
||||||
|
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
|
||||||
|
# INSTRUCTIONS AT https://kubernetes.io/security/
|
||||||
|
|
||||||
|
liggitt
|
||||||
|
enj
|
||||||
|
taahm
|
||||||
9
staging/src/k8s.io/externaljwt/apis/OWNERS
Normal file
9
staging/src/k8s.io/externaljwt/apis/OWNERS
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# See the OWNERS docs at https://go.k8s.io/owners
|
||||||
|
|
||||||
|
# Disable inheritance as this is an api owners file
|
||||||
|
options:
|
||||||
|
no_parent_owners: true
|
||||||
|
approvers:
|
||||||
|
- api-approvers
|
||||||
|
reviewers:
|
||||||
|
- sig-auth-api-reviewers
|
||||||
591
staging/src/k8s.io/externaljwt/apis/v1alpha1/api.pb.go
Normal file
591
staging/src/k8s.io/externaljwt/apis/v1alpha1/api.pb.go
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by protoc-gen-gogo. DO NOT EDIT.
|
||||||
|
// source: api.proto
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
fmt "fmt"
|
||||||
|
proto "github.com/gogo/protobuf/proto"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
math "math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package it is being compiled against.
|
||||||
|
// A compilation error at this line likely means your copy of the
|
||||||
|
// proto package needs to be updated.
|
||||||
|
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
||||||
|
|
||||||
|
type SignJWTRequest struct {
|
||||||
|
// URL-safe base64 wrapped payload to be signed.
|
||||||
|
// Exactly as it appears in the second segment of the JWT
|
||||||
|
Claims string `protobuf:"bytes,1,opt,name=claims,proto3" json:"claims,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SignJWTRequest) Reset() { *m = SignJWTRequest{} }
|
||||||
|
func (m *SignJWTRequest) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*SignJWTRequest) ProtoMessage() {}
|
||||||
|
func (*SignJWTRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_00212fb1f9d3bf1c, []int{0}
|
||||||
|
}
|
||||||
|
func (m *SignJWTRequest) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_SignJWTRequest.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *SignJWTRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_SignJWTRequest.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (m *SignJWTRequest) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_SignJWTRequest.Merge(m, src)
|
||||||
|
}
|
||||||
|
func (m *SignJWTRequest) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_SignJWTRequest.Size(m)
|
||||||
|
}
|
||||||
|
func (m *SignJWTRequest) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_SignJWTRequest.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_SignJWTRequest proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *SignJWTRequest) GetClaims() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Claims
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignJWTResponse struct {
|
||||||
|
// header must contain only alg, kid, typ claims.
|
||||||
|
// typ must be “JWT”.
|
||||||
|
// kid must be non-empty, <=1024 characters, and its corresponding public key should not be excluded from OIDC discovery.
|
||||||
|
// alg must be one of the algorithms supported by kube-apiserver (currently RS256, ES256, ES384, ES512).
|
||||||
|
// header cannot have any additional data that kube-apiserver does not recognize.
|
||||||
|
// Already wrapped in URL-safe base64, exactly as it appears in the first segment of the JWT.
|
||||||
|
Header string `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
|
||||||
|
// The signature for the JWT.
|
||||||
|
// Already wrapped in URL-safe base64, exactly as it appears in the final segment of the JWT.
|
||||||
|
Signature string `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SignJWTResponse) Reset() { *m = SignJWTResponse{} }
|
||||||
|
func (m *SignJWTResponse) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*SignJWTResponse) ProtoMessage() {}
|
||||||
|
func (*SignJWTResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_00212fb1f9d3bf1c, []int{1}
|
||||||
|
}
|
||||||
|
func (m *SignJWTResponse) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_SignJWTResponse.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *SignJWTResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_SignJWTResponse.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (m *SignJWTResponse) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_SignJWTResponse.Merge(m, src)
|
||||||
|
}
|
||||||
|
func (m *SignJWTResponse) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_SignJWTResponse.Size(m)
|
||||||
|
}
|
||||||
|
func (m *SignJWTResponse) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_SignJWTResponse.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_SignJWTResponse proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *SignJWTResponse) GetHeader() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Header
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SignJWTResponse) GetSignature() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Signature
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchKeysRequest struct {
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FetchKeysRequest) Reset() { *m = FetchKeysRequest{} }
|
||||||
|
func (m *FetchKeysRequest) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*FetchKeysRequest) ProtoMessage() {}
|
||||||
|
func (*FetchKeysRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_00212fb1f9d3bf1c, []int{2}
|
||||||
|
}
|
||||||
|
func (m *FetchKeysRequest) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_FetchKeysRequest.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *FetchKeysRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_FetchKeysRequest.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (m *FetchKeysRequest) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_FetchKeysRequest.Merge(m, src)
|
||||||
|
}
|
||||||
|
func (m *FetchKeysRequest) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_FetchKeysRequest.Size(m)
|
||||||
|
}
|
||||||
|
func (m *FetchKeysRequest) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_FetchKeysRequest.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_FetchKeysRequest proto.InternalMessageInfo
|
||||||
|
|
||||||
|
type FetchKeysResponse struct {
|
||||||
|
Keys []*Key `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"`
|
||||||
|
// The timestamp when this data was pulled from the authoritative source of
|
||||||
|
// truth for verification keys.
|
||||||
|
// kube-apiserver can export this from metrics, to enable end-to-end SLOs.
|
||||||
|
DataTimestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=data_timestamp,json=dataTimestamp,proto3" json:"data_timestamp,omitempty"`
|
||||||
|
// refresh interval for verification keys to pick changes if any.
|
||||||
|
// any value <= 0 is considered a misconfiguration.
|
||||||
|
RefreshHintSeconds int64 `protobuf:"varint,3,opt,name=refresh_hint_seconds,json=refreshHintSeconds,proto3" json:"refresh_hint_seconds,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FetchKeysResponse) Reset() { *m = FetchKeysResponse{} }
|
||||||
|
func (m *FetchKeysResponse) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*FetchKeysResponse) ProtoMessage() {}
|
||||||
|
func (*FetchKeysResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_00212fb1f9d3bf1c, []int{3}
|
||||||
|
}
|
||||||
|
func (m *FetchKeysResponse) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_FetchKeysResponse.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *FetchKeysResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_FetchKeysResponse.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (m *FetchKeysResponse) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_FetchKeysResponse.Merge(m, src)
|
||||||
|
}
|
||||||
|
func (m *FetchKeysResponse) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_FetchKeysResponse.Size(m)
|
||||||
|
}
|
||||||
|
func (m *FetchKeysResponse) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_FetchKeysResponse.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_FetchKeysResponse proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *FetchKeysResponse) GetKeys() []*Key {
|
||||||
|
if m != nil {
|
||||||
|
return m.Keys
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FetchKeysResponse) GetDataTimestamp() *timestamppb.Timestamp {
|
||||||
|
if m != nil {
|
||||||
|
return m.DataTimestamp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FetchKeysResponse) GetRefreshHintSeconds() int64 {
|
||||||
|
if m != nil {
|
||||||
|
return m.RefreshHintSeconds
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key struct {
|
||||||
|
// A unique identifier for this key.
|
||||||
|
// Length must be <=1024.
|
||||||
|
KeyId string `protobuf:"bytes,1,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"`
|
||||||
|
// The public key, PKIX-serialized.
|
||||||
|
// must be a public key supported by kube-apiserver (currently RSA 256 or ECDSA 256/384/521)
|
||||||
|
Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
|
||||||
|
// Set only for keys that are not used to sign bound tokens.
|
||||||
|
// eg: supported keys for legacy tokens.
|
||||||
|
// If set, key is used for verification but excluded from OIDC discovery docs.
|
||||||
|
// if set, external signer should not use this key to sign a JWT.
|
||||||
|
ExcludeFromOidcDiscovery bool `protobuf:"varint,3,opt,name=exclude_from_oidc_discovery,json=excludeFromOidcDiscovery,proto3" json:"exclude_from_oidc_discovery,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Key) Reset() { *m = Key{} }
|
||||||
|
func (m *Key) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Key) ProtoMessage() {}
|
||||||
|
func (*Key) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_00212fb1f9d3bf1c, []int{4}
|
||||||
|
}
|
||||||
|
func (m *Key) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_Key.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *Key) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_Key.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (m *Key) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_Key.Merge(m, src)
|
||||||
|
}
|
||||||
|
func (m *Key) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_Key.Size(m)
|
||||||
|
}
|
||||||
|
func (m *Key) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_Key.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_Key proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *Key) GetKeyId() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.KeyId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Key) GetKey() []byte {
|
||||||
|
if m != nil {
|
||||||
|
return m.Key
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Key) GetExcludeFromOidcDiscovery() bool {
|
||||||
|
if m != nil {
|
||||||
|
return m.ExcludeFromOidcDiscovery
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetadataRequest struct {
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MetadataRequest) Reset() { *m = MetadataRequest{} }
|
||||||
|
func (m *MetadataRequest) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*MetadataRequest) ProtoMessage() {}
|
||||||
|
func (*MetadataRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_00212fb1f9d3bf1c, []int{5}
|
||||||
|
}
|
||||||
|
func (m *MetadataRequest) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_MetadataRequest.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *MetadataRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_MetadataRequest.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (m *MetadataRequest) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_MetadataRequest.Merge(m, src)
|
||||||
|
}
|
||||||
|
func (m *MetadataRequest) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_MetadataRequest.Size(m)
|
||||||
|
}
|
||||||
|
func (m *MetadataRequest) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_MetadataRequest.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_MetadataRequest proto.InternalMessageInfo
|
||||||
|
|
||||||
|
type MetadataResponse struct {
|
||||||
|
// used by kube-apiserver for defaulting/validation of JWT lifetime while accounting for configuration flag values:
|
||||||
|
// 1. `--service-account-max-token-expiration`
|
||||||
|
// 2. `--service-account-extend-token-expiration`
|
||||||
|
//
|
||||||
|
// * If `--service-account-max-token-expiration` is greater than `max_token_expiration_seconds`, kube-apiserver treats that as misconfiguration and exits.
|
||||||
|
// * If `--service-account-max-token-expiration` is not explicitly set, kube-apiserver defaults to `max_token_expiration_seconds`.
|
||||||
|
// * If `--service-account-extend-token-expiration` is true, the extended expiration is `min(1 year, max_token_expiration_seconds)`.
|
||||||
|
//
|
||||||
|
// `max_token_expiration_seconds` must be at least 600s.
|
||||||
|
MaxTokenExpirationSeconds int64 `protobuf:"varint,1,opt,name=max_token_expiration_seconds,json=maxTokenExpirationSeconds,proto3" json:"max_token_expiration_seconds,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MetadataResponse) Reset() { *m = MetadataResponse{} }
|
||||||
|
func (m *MetadataResponse) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*MetadataResponse) ProtoMessage() {}
|
||||||
|
func (*MetadataResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_00212fb1f9d3bf1c, []int{6}
|
||||||
|
}
|
||||||
|
func (m *MetadataResponse) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_MetadataResponse.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *MetadataResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_MetadataResponse.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (m *MetadataResponse) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_MetadataResponse.Merge(m, src)
|
||||||
|
}
|
||||||
|
func (m *MetadataResponse) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_MetadataResponse.Size(m)
|
||||||
|
}
|
||||||
|
func (m *MetadataResponse) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_MetadataResponse.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_MetadataResponse proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *MetadataResponse) GetMaxTokenExpirationSeconds() int64 {
|
||||||
|
if m != nil {
|
||||||
|
return m.MaxTokenExpirationSeconds
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterType((*SignJWTRequest)(nil), "v1alpha1.SignJWTRequest")
|
||||||
|
proto.RegisterType((*SignJWTResponse)(nil), "v1alpha1.SignJWTResponse")
|
||||||
|
proto.RegisterType((*FetchKeysRequest)(nil), "v1alpha1.FetchKeysRequest")
|
||||||
|
proto.RegisterType((*FetchKeysResponse)(nil), "v1alpha1.FetchKeysResponse")
|
||||||
|
proto.RegisterType((*Key)(nil), "v1alpha1.Key")
|
||||||
|
proto.RegisterType((*MetadataRequest)(nil), "v1alpha1.MetadataRequest")
|
||||||
|
proto.RegisterType((*MetadataResponse)(nil), "v1alpha1.MetadataResponse")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) }
|
||||||
|
|
||||||
|
var fileDescriptor_00212fb1f9d3bf1c = []byte{
|
||||||
|
// 483 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x92, 0xcd, 0x6e, 0xd3, 0x40,
|
||||||
|
0x10, 0xc7, 0x31, 0x29, 0x51, 0x32, 0xa5, 0x6d, 0xb2, 0x02, 0xe4, 0xba, 0x95, 0x08, 0x3e, 0xe5,
|
||||||
|
0x64, 0xd3, 0x70, 0xe1, 0x52, 0x21, 0x3e, 0x1a, 0xa0, 0x11, 0x42, 0x72, 0x22, 0x55, 0xe2, 0x62,
|
||||||
|
0x6d, 0xed, 0x49, 0xbc, 0xf8, 0x63, 0xcd, 0xee, 0xa6, 0xd8, 0xcf, 0xc4, 0x43, 0xf1, 0x2a, 0xc8,
|
||||||
|
0x1f, 0xeb, 0x94, 0x2a, 0xb7, 0xdd, 0xf9, 0xff, 0x77, 0x66, 0x7e, 0xb3, 0x03, 0x43, 0x9a, 0x33,
|
||||||
|
0x27, 0x17, 0x5c, 0x71, 0x32, 0xb8, 0xbb, 0xa0, 0x49, 0x1e, 0xd1, 0x0b, 0xeb, 0xe5, 0x86, 0xf3,
|
||||||
|
0x4d, 0x82, 0x6e, 0x1d, 0xbf, 0xdd, 0xae, 0x5d, 0xc5, 0x52, 0x94, 0x8a, 0xa6, 0x79, 0x63, 0xb5,
|
||||||
|
0xa7, 0x70, 0xbc, 0x64, 0x9b, 0xec, 0xfa, 0x66, 0xe5, 0xe1, 0xaf, 0x2d, 0x4a, 0x45, 0x5e, 0x40,
|
||||||
|
0x3f, 0x48, 0x28, 0x4b, 0xa5, 0x69, 0x4c, 0x8c, 0xe9, 0xd0, 0x6b, 0x6f, 0xf6, 0x67, 0x38, 0xe9,
|
||||||
|
0x9c, 0x32, 0xe7, 0x99, 0xc4, 0xca, 0x1a, 0x21, 0x0d, 0x51, 0x68, 0x6b, 0x73, 0x23, 0xe7, 0x30,
|
||||||
|
0x94, 0x6c, 0x93, 0x51, 0xb5, 0x15, 0x68, 0x3e, 0xae, 0xa5, 0x5d, 0xc0, 0x26, 0x30, 0x9a, 0xa3,
|
||||||
|
0x0a, 0xa2, 0x05, 0x96, 0xb2, 0x2d, 0x6a, 0xff, 0x31, 0x60, 0x7c, 0x2f, 0xd8, 0xe6, 0x7f, 0x05,
|
||||||
|
0x07, 0x31, 0x96, 0x55, 0x23, 0xbd, 0xe9, 0xe1, 0xec, 0xc8, 0xd1, 0x58, 0xce, 0x02, 0x4b, 0xaf,
|
||||||
|
0x96, 0xc8, 0x7b, 0x38, 0x0e, 0xa9, 0xa2, 0x7e, 0xc7, 0x55, 0xd7, 0x3b, 0x9c, 0x59, 0x4e, 0x43,
|
||||||
|
0xee, 0x68, 0x72, 0x67, 0xa5, 0x1d, 0xde, 0x51, 0xf5, 0xa2, 0xbb, 0x92, 0xd7, 0xf0, 0x4c, 0xe0,
|
||||||
|
0x5a, 0xa0, 0x8c, 0xfc, 0x88, 0x65, 0xca, 0x97, 0x18, 0xf0, 0x2c, 0x94, 0x66, 0x6f, 0x62, 0x4c,
|
||||||
|
0x7b, 0x1e, 0x69, 0xb5, 0x2f, 0x2c, 0x53, 0xcb, 0x46, 0xb1, 0x53, 0xe8, 0x2d, 0xb0, 0x24, 0xcf,
|
||||||
|
0xa1, 0x1f, 0x63, 0xe9, 0xb3, 0xb0, 0xc5, 0x7f, 0x12, 0x63, 0xf9, 0x35, 0x24, 0x23, 0xe8, 0xc5,
|
||||||
|
0x58, 0xd6, 0x7d, 0x3c, 0xf5, 0xaa, 0x23, 0xb9, 0x84, 0x33, 0x2c, 0x82, 0x64, 0x1b, 0xa2, 0xbf,
|
||||||
|
0x16, 0x3c, 0xf5, 0x39, 0x0b, 0x03, 0x3f, 0x64, 0x32, 0xe0, 0x77, 0x28, 0xca, 0xba, 0xd0, 0xc0,
|
||||||
|
0x33, 0x5b, 0xcb, 0x5c, 0xf0, 0xf4, 0x3b, 0x0b, 0x83, 0x4f, 0x5a, 0xb7, 0xc7, 0x70, 0xf2, 0x0d,
|
||||||
|
0x15, 0xad, 0xba, 0xd6, 0xf3, 0x5a, 0xc2, 0x68, 0x17, 0x6a, 0xa7, 0xf5, 0x0e, 0xce, 0x53, 0x5a,
|
||||||
|
0xf8, 0x8a, 0xc7, 0x98, 0xf9, 0x58, 0xe4, 0x4c, 0x50, 0xc5, 0x78, 0xd6, 0xf1, 0x18, 0x35, 0xcf,
|
||||||
|
0x69, 0x4a, 0x8b, 0x55, 0x65, 0xb9, 0xea, 0x1c, 0x2d, 0xd6, 0xec, 0xaf, 0x01, 0xe3, 0xab, 0x42,
|
||||||
|
0xa1, 0xc8, 0x68, 0x72, 0x7d, 0xb3, 0xaa, 0x7e, 0x1b, 0x05, 0xb9, 0x84, 0x83, 0xea, 0x44, 0xcc,
|
||||||
|
0xdd, 0xf8, 0xff, 0xdf, 0x18, 0xeb, 0x74, 0x8f, 0xd2, 0xf4, 0x64, 0x3f, 0x22, 0x73, 0x18, 0x76,
|
||||||
|
0x1f, 0x4b, 0xac, 0x9d, 0xf3, 0xe1, 0x0a, 0x58, 0x67, 0x7b, 0xb5, 0x2e, 0xcf, 0x47, 0x18, 0x68,
|
||||||
|
0x62, 0x72, 0xaf, 0xe0, 0x83, 0xc1, 0x58, 0xd6, 0x3e, 0x49, 0x27, 0xf9, 0x60, 0xff, 0x98, 0xc4,
|
||||||
|
0x6f, 0xa5, 0xc3, 0xb8, 0x8b, 0x2d, 0xe7, 0xcf, 0xdf, 0xca, 0xa5, 0x39, 0x93, 0xae, 0x7e, 0x76,
|
||||||
|
0xdb, 0xaf, 0x37, 0xe6, 0xcd, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x90, 0x1b, 0xfb, 0x90, 0x50,
|
||||||
|
0x03, 0x00, 0x00,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ context.Context
|
||||||
|
var _ grpc.ClientConn
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
const _ = grpc.SupportPackageIsVersion4
|
||||||
|
|
||||||
|
// ExternalJWTSignerClient is the client API for ExternalJWTSigner service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
||||||
|
type ExternalJWTSignerClient interface {
|
||||||
|
// Sign takes a serialized JWT payload, and returns the serialized header and
|
||||||
|
// signature. The caller can then assemble the JWT from the header, payload,
|
||||||
|
// and signature.
|
||||||
|
//
|
||||||
|
// The plugin MUST set a key id in the returned JWT header.
|
||||||
|
Sign(ctx context.Context, in *SignJWTRequest, opts ...grpc.CallOption) (*SignJWTResponse, error)
|
||||||
|
// FetchKeys returns the set of public keys that are trusted to sign
|
||||||
|
// Kubernetes service account tokens. Kube-apiserver will call this RPC:
|
||||||
|
//
|
||||||
|
// * Every time it tries to validate a JWT from the service account issuer with an unknown key ID, and
|
||||||
|
//
|
||||||
|
// - Periodically, so it can serve reasonably-up-to-date keys from the OIDC
|
||||||
|
// JWKs endpoint.
|
||||||
|
FetchKeys(ctx context.Context, in *FetchKeysRequest, opts ...grpc.CallOption) (*FetchKeysResponse, error)
|
||||||
|
// Metadata is meant to be called once on startup.
|
||||||
|
// Enables sharing metadata with kube-apiserver (eg: the max token lifetime that signer supports)
|
||||||
|
Metadata(ctx context.Context, in *MetadataRequest, opts ...grpc.CallOption) (*MetadataResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type externalJWTSignerClient struct {
|
||||||
|
cc *grpc.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExternalJWTSignerClient(cc *grpc.ClientConn) ExternalJWTSignerClient {
|
||||||
|
return &externalJWTSignerClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *externalJWTSignerClient) Sign(ctx context.Context, in *SignJWTRequest, opts ...grpc.CallOption) (*SignJWTResponse, error) {
|
||||||
|
out := new(SignJWTResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/v1alpha1.ExternalJWTSigner/Sign", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *externalJWTSignerClient) FetchKeys(ctx context.Context, in *FetchKeysRequest, opts ...grpc.CallOption) (*FetchKeysResponse, error) {
|
||||||
|
out := new(FetchKeysResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/v1alpha1.ExternalJWTSigner/FetchKeys", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *externalJWTSignerClient) Metadata(ctx context.Context, in *MetadataRequest, opts ...grpc.CallOption) (*MetadataResponse, error) {
|
||||||
|
out := new(MetadataResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/v1alpha1.ExternalJWTSigner/Metadata", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalJWTSignerServer is the server API for ExternalJWTSigner service.
|
||||||
|
type ExternalJWTSignerServer interface {
|
||||||
|
// Sign takes a serialized JWT payload, and returns the serialized header and
|
||||||
|
// signature. The caller can then assemble the JWT from the header, payload,
|
||||||
|
// and signature.
|
||||||
|
//
|
||||||
|
// The plugin MUST set a key id in the returned JWT header.
|
||||||
|
Sign(context.Context, *SignJWTRequest) (*SignJWTResponse, error)
|
||||||
|
// FetchKeys returns the set of public keys that are trusted to sign
|
||||||
|
// Kubernetes service account tokens. Kube-apiserver will call this RPC:
|
||||||
|
//
|
||||||
|
// * Every time it tries to validate a JWT from the service account issuer with an unknown key ID, and
|
||||||
|
//
|
||||||
|
// - Periodically, so it can serve reasonably-up-to-date keys from the OIDC
|
||||||
|
// JWKs endpoint.
|
||||||
|
FetchKeys(context.Context, *FetchKeysRequest) (*FetchKeysResponse, error)
|
||||||
|
// Metadata is meant to be called once on startup.
|
||||||
|
// Enables sharing metadata with kube-apiserver (eg: the max token lifetime that signer supports)
|
||||||
|
Metadata(context.Context, *MetadataRequest) (*MetadataResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedExternalJWTSignerServer can be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedExternalJWTSignerServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UnimplementedExternalJWTSignerServer) Sign(ctx context.Context, req *SignJWTRequest) (*SignJWTResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Sign not implemented")
|
||||||
|
}
|
||||||
|
func (*UnimplementedExternalJWTSignerServer) FetchKeys(ctx context.Context, req *FetchKeysRequest) (*FetchKeysResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method FetchKeys not implemented")
|
||||||
|
}
|
||||||
|
func (*UnimplementedExternalJWTSignerServer) Metadata(ctx context.Context, req *MetadataRequest) (*MetadataResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Metadata not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterExternalJWTSignerServer(s *grpc.Server, srv ExternalJWTSignerServer) {
|
||||||
|
s.RegisterService(&_ExternalJWTSigner_serviceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ExternalJWTSigner_Sign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SignJWTRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ExternalJWTSignerServer).Sign(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/v1alpha1.ExternalJWTSigner/Sign",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ExternalJWTSignerServer).Sign(ctx, req.(*SignJWTRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ExternalJWTSigner_FetchKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(FetchKeysRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ExternalJWTSignerServer).FetchKeys(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/v1alpha1.ExternalJWTSigner/FetchKeys",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ExternalJWTSignerServer).FetchKeys(ctx, req.(*FetchKeysRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ExternalJWTSigner_Metadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(MetadataRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ExternalJWTSignerServer).Metadata(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/v1alpha1.ExternalJWTSigner/Metadata",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ExternalJWTSignerServer).Metadata(ctx, req.(*MetadataRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ExternalJWTSigner_serviceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "v1alpha1.ExternalJWTSigner",
|
||||||
|
HandlerType: (*ExternalJWTSignerServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "Sign",
|
||||||
|
Handler: _ExternalJWTSigner_Sign_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "FetchKeys",
|
||||||
|
Handler: _ExternalJWTSigner_FetchKeys_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Metadata",
|
||||||
|
Handler: _ExternalJWTSigner_Metadata_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "api.proto",
|
||||||
|
}
|
||||||
113
staging/src/k8s.io/externaljwt/apis/v1alpha1/api.proto
Normal file
113
staging/src/k8s.io/externaljwt/apis/v1alpha1/api.proto
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// To regenerate api.pb.go run `hack/update-codegen.sh protobindings`
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package v1alpha1;
|
||||||
|
|
||||||
|
option go_package = "k8s.io/externaljwt/apis/v1alpha1";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// This service is served by a process on a local Unix Domain Socket.
|
||||||
|
service ExternalJWTSigner {
|
||||||
|
// Sign takes a serialized JWT payload, and returns the serialized header and
|
||||||
|
// signature. The caller can then assemble the JWT from the header, payload,
|
||||||
|
// and signature.
|
||||||
|
//
|
||||||
|
// The plugin MUST set a key id in the returned JWT header.
|
||||||
|
rpc Sign(SignJWTRequest) returns (SignJWTResponse) {}
|
||||||
|
|
||||||
|
// FetchKeys returns the set of public keys that are trusted to sign
|
||||||
|
// Kubernetes service account tokens. Kube-apiserver will call this RPC:
|
||||||
|
//
|
||||||
|
// * Every time it tries to validate a JWT from the service account issuer with an unknown key ID, and
|
||||||
|
//
|
||||||
|
// * Periodically, so it can serve reasonably-up-to-date keys from the OIDC
|
||||||
|
// JWKs endpoint.
|
||||||
|
rpc FetchKeys(FetchKeysRequest) returns (FetchKeysResponse) {}
|
||||||
|
|
||||||
|
// Metadata is meant to be called once on startup.
|
||||||
|
// Enables sharing metadata with kube-apiserver (eg: the max token lifetime that signer supports)
|
||||||
|
rpc Metadata(MetadataRequest) returns (MetadataResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignJWTRequest {
|
||||||
|
// URL-safe base64 wrapped payload to be signed.
|
||||||
|
// Exactly as it appears in the second segment of the JWT
|
||||||
|
string claims = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignJWTResponse {
|
||||||
|
// header must contain only alg, kid, typ claims.
|
||||||
|
// typ must be “JWT”.
|
||||||
|
// kid must be non-empty, <=1024 characters, and its corresponding public key should not be excluded from OIDC discovery.
|
||||||
|
// alg must be one of the algorithms supported by kube-apiserver (currently RS256, ES256, ES384, ES512).
|
||||||
|
// header cannot have any additional data that kube-apiserver does not recognize.
|
||||||
|
// Already wrapped in URL-safe base64, exactly as it appears in the first segment of the JWT.
|
||||||
|
string header = 1;
|
||||||
|
|
||||||
|
// The signature for the JWT.
|
||||||
|
// Already wrapped in URL-safe base64, exactly as it appears in the final segment of the JWT.
|
||||||
|
string signature = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FetchKeysRequest {}
|
||||||
|
|
||||||
|
message FetchKeysResponse {
|
||||||
|
repeated Key keys = 1;
|
||||||
|
|
||||||
|
// The timestamp when this data was pulled from the authoritative source of
|
||||||
|
// truth for verification keys.
|
||||||
|
// kube-apiserver can export this from metrics, to enable end-to-end SLOs.
|
||||||
|
google.protobuf.Timestamp data_timestamp = 2;
|
||||||
|
|
||||||
|
// refresh interval for verification keys to pick changes if any.
|
||||||
|
// any value <= 0 is considered a misconfiguration.
|
||||||
|
int64 refresh_hint_seconds = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Key {
|
||||||
|
// A unique identifier for this key.
|
||||||
|
// Length must be <=1024.
|
||||||
|
string key_id = 1;
|
||||||
|
|
||||||
|
// The public key, PKIX-serialized.
|
||||||
|
// must be a public key supported by kube-apiserver (currently RSA 256 or ECDSA 256/384/521)
|
||||||
|
bytes key = 2;
|
||||||
|
|
||||||
|
// Set only for keys that are not used to sign bound tokens.
|
||||||
|
// eg: supported keys for legacy tokens.
|
||||||
|
// If set, key is used for verification but excluded from OIDC discovery docs.
|
||||||
|
// if set, external signer should not use this key to sign a JWT.
|
||||||
|
bool exclude_from_oidc_discovery = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MetadataRequest {}
|
||||||
|
|
||||||
|
message MetadataResponse {
|
||||||
|
// used by kube-apiserver for defaulting/validation of JWT lifetime while accounting for configuration flag values:
|
||||||
|
// 1. `--service-account-max-token-expiration`
|
||||||
|
// 2. `--service-account-extend-token-expiration`
|
||||||
|
//
|
||||||
|
// * If `--service-account-max-token-expiration` is greater than `max_token_expiration_seconds`, kube-apiserver treats that as misconfiguration and exits.
|
||||||
|
// * If `--service-account-max-token-expiration` is not explicitly set, kube-apiserver defaults to `max_token_expiration_seconds`.
|
||||||
|
// * If `--service-account-extend-token-expiration` is true, the extended expiration is `min(1 year, max_token_expiration_seconds)`.
|
||||||
|
//
|
||||||
|
// `max_token_expiration_seconds` must be at least 600s.
|
||||||
|
int64 max_token_expiration_seconds = 1;
|
||||||
|
}
|
||||||
3
staging/src/k8s.io/externaljwt/code-of-conduct.md
Normal file
3
staging/src/k8s.io/externaljwt/code-of-conduct.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Kubernetes Community Code of Conduct
|
||||||
|
|
||||||
|
Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)
|
||||||
18
staging/src/k8s.io/externaljwt/docs.go
Normal file
18
staging/src/k8s.io/externaljwt/docs.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package externaljwt contains the proto definitions for the ExternalJWTSigner.
|
||||||
|
package externaljwt // import "k8s.io/externaljwt"
|
||||||
20
staging/src/k8s.io/externaljwt/go.mod
Normal file
20
staging/src/k8s.io/externaljwt/go.mod
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// This is a generated file. Do not edit directly.
|
||||||
|
|
||||||
|
module k8s.io/externaljwt
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
godebug default=go1.23
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gogo/protobuf v1.3.2
|
||||||
|
google.golang.org/grpc v1.65.0
|
||||||
|
google.golang.org/protobuf v1.35.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/net v0.30.0 // indirect
|
||||||
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
|
golang.org/x/text v0.19.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
||||||
|
)
|
||||||
62
staging/src/k8s.io/externaljwt/go.sum
generated
Normal file
62
staging/src/k8s.io/externaljwt/go.sum
generated
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
|
||||||
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||||
|
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||||
|
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
|
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||||
|
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||||
|
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||||
|
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
@@ -102,7 +102,7 @@ func (a *APIServer) Start(ctx context.Context) error {
|
|||||||
go func() {
|
go func() {
|
||||||
defer close(errCh)
|
defer close(errCh)
|
||||||
defer cancel(errors.New("shutting down")) // Calling Stop is optional, but cancel always should be invoked.
|
defer cancel(errors.New("shutting down")) // Calling Stop is optional, but cancel always should be invoked.
|
||||||
completedOptions, err := o.Complete()
|
completedOptions, err := o.Complete(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- fmt.Errorf("set apiserver default options error: %w", err)
|
errCh <- fmt.Errorf("set apiserver default options error: %w", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -452,6 +452,12 @@
|
|||||||
lockToDefault: false
|
lockToDefault: false
|
||||||
preRelease: GA
|
preRelease: GA
|
||||||
version: "1.20"
|
version: "1.20"
|
||||||
|
- name: ExternalServiceAccountTokenSigner
|
||||||
|
versionedSpecs:
|
||||||
|
- default: false
|
||||||
|
lockToDefault: false
|
||||||
|
preRelease: Alpha
|
||||||
|
version: "1.32"
|
||||||
- name: GracefulNodeShutdown
|
- name: GracefulNodeShutdown
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: false
|
- default: false
|
||||||
|
|||||||
@@ -691,7 +691,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err calling Claims: %v", err)
|
t.Fatalf("err calling Claims: %v", err)
|
||||||
}
|
}
|
||||||
tok, err := tokenGenerator.GenerateToken(sc, pc)
|
tok, err := tokenGenerator.GenerateToken(context.TODO(), sc, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err signing expired token: %v", err)
|
t.Fatalf("err signing expired token: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func StartRealAPIServerOrDie(t *testing.T, configFuncs ...func(*options.ServerRu
|
|||||||
for _, f := range configFuncs {
|
for _, f := range configFuncs {
|
||||||
f(opts)
|
f(opts)
|
||||||
}
|
}
|
||||||
completedOptions, err := opts.Complete()
|
completedOptions, err := opts.Complete(tCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func StartTestServer(ctx context.Context, t testing.TB, setup TestServerSetup) (
|
|||||||
setup.ModifyServerRunOptions(opts)
|
setup.ModifyServerRunOptions(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
completedOptions, err := opts.Complete()
|
completedOptions, err := opts.Complete(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
305
test/integration/serviceaccount/external_jwt_signer_test.go
Normal file
305
test/integration/serviceaccount/external_jwt_signer_test.go
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package serviceaccount
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
||||||
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
|
"k8s.io/kubernetes/test/utils/ktesting"
|
||||||
|
|
||||||
|
authv1 "k8s.io/api/authentication/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
|
v1alpha1testing "k8s.io/kubernetes/pkg/serviceaccount/externaljwt/plugin/testing/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExternalJWTSigningAndAuth(t *testing.T) {
|
||||||
|
// Enable feature gate for external JWT signer.
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExternalServiceAccountTokenSigner, true)
|
||||||
|
|
||||||
|
// Prep some keys to use with test.
|
||||||
|
key1, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
panic("Error while generating first RSA key")
|
||||||
|
}
|
||||||
|
pubKey1Bytes, err := x509.MarshalPKIXPublicKey(&key1.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
panic("Error while marshaling first public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
tCtx := ktesting.Init(t)
|
||||||
|
ctx, cancel := context.WithCancel(tCtx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// create and start mock signer.
|
||||||
|
socketPath := "@mock-external-jwt-signer.sock"
|
||||||
|
t.Cleanup(func() { _ = os.Remove(socketPath) })
|
||||||
|
mockSigner := v1alpha1testing.NewMockSigner(t, socketPath)
|
||||||
|
defer mockSigner.CleanUp()
|
||||||
|
|
||||||
|
// Start Api server configured with external signer.
|
||||||
|
client, _, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{
|
||||||
|
ModifyServerRunOptions: func(opt *options.ServerRunOptions) {
|
||||||
|
opt.ServiceAccountSigningEndpoint = socketPath
|
||||||
|
opt.ServiceAccountSigningKeyFile = ""
|
||||||
|
opt.Authentication.ServiceAccounts.KeyFiles = []string{}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer tearDownFn()
|
||||||
|
|
||||||
|
// Create Namesapce (ns-1) to work with.
|
||||||
|
if _, err := client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "ns-1",
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{}); err != nil {
|
||||||
|
t.Fatalf("Error when creating namespace: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ServiceAccount (sa-1) to work with.
|
||||||
|
if _, err := client.CoreV1().ServiceAccounts("ns-1").Create(ctx, &corev1.ServiceAccount{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "sa-1",
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{}); err != nil {
|
||||||
|
t.Fatalf("Error when creating service-account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
preTestSignerUpdate func()
|
||||||
|
preValidationSignerUpdate func()
|
||||||
|
wantTokenReqErr error
|
||||||
|
shouldPassAuth bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "signing key supported.",
|
||||||
|
preTestSignerUpdate: func() { /*no-op*/ },
|
||||||
|
preValidationSignerUpdate: func() { /*no-op*/ },
|
||||||
|
shouldPassAuth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "signing key not among supported set",
|
||||||
|
preTestSignerUpdate: func() {
|
||||||
|
mockSigner.SigningKey = key1
|
||||||
|
mockSigner.SigningKeyID = "updated-kid-1"
|
||||||
|
},
|
||||||
|
preValidationSignerUpdate: func() { /*no-op*/ },
|
||||||
|
shouldPassAuth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "signing key corresponds to public key that is excluded from OIDC",
|
||||||
|
preTestSignerUpdate: func() {
|
||||||
|
mockSigner.SigningKey = key1
|
||||||
|
mockSigner.SigningKeyID = "updated-kid-1"
|
||||||
|
|
||||||
|
cpy := make(map[string]v1alpha1testing.KeyT)
|
||||||
|
for key, value := range *mockSigner.SupportedKeys.Load() {
|
||||||
|
cpy[key] = value
|
||||||
|
}
|
||||||
|
cpy["updated-kid-1"] = v1alpha1testing.KeyT{
|
||||||
|
Key: pubKey1Bytes,
|
||||||
|
ExcludeFromOidcDiscovery: true,
|
||||||
|
}
|
||||||
|
mockSigner.SupportedKeys.Store(&cpy)
|
||||||
|
},
|
||||||
|
preValidationSignerUpdate: func() { /*no-op*/ },
|
||||||
|
wantTokenReqErr: fmt.Errorf("failed to generate token: while validating header: key used for signing JWT (kid: updated-kid-1) is excluded from OIDC discovery docs"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "different signing and supported keys with same id",
|
||||||
|
preTestSignerUpdate: func() {
|
||||||
|
mockSigner.SigningKey = key1
|
||||||
|
},
|
||||||
|
preValidationSignerUpdate: func() { /*no-op*/ },
|
||||||
|
shouldPassAuth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "token gen failure with un-supported Alg type",
|
||||||
|
preTestSignerUpdate: func() {
|
||||||
|
mockSigner.SigningAlg = "ABC"
|
||||||
|
},
|
||||||
|
preValidationSignerUpdate: func() { /*no-op*/ },
|
||||||
|
wantTokenReqErr: fmt.Errorf("failed to generate token: while validating header: bad signing algorithm \"ABC\""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "token gen failure with un-supported token type",
|
||||||
|
preTestSignerUpdate: func() {
|
||||||
|
mockSigner.TokenType = "ABC"
|
||||||
|
},
|
||||||
|
preValidationSignerUpdate: func() { /*no-op*/ },
|
||||||
|
wantTokenReqErr: fmt.Errorf("failed to generate token: while validating header: bad type"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "change of supported keys not picked immediately",
|
||||||
|
preTestSignerUpdate: func() {
|
||||||
|
mockSigner.SigningKey = key1
|
||||||
|
},
|
||||||
|
preValidationSignerUpdate: func() {
|
||||||
|
mockSigner.SupportedKeys.Store(&map[string]v1alpha1testing.KeyT{})
|
||||||
|
},
|
||||||
|
shouldPassAuth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "change of supported keys picked up after periodic sync",
|
||||||
|
preTestSignerUpdate: func() {
|
||||||
|
mockSigner.SigningKey = key1
|
||||||
|
},
|
||||||
|
preValidationSignerUpdate: func() {
|
||||||
|
cpy := make(map[string]v1alpha1testing.KeyT)
|
||||||
|
for key, value := range *mockSigner.SupportedKeys.Load() {
|
||||||
|
cpy[key] = value
|
||||||
|
}
|
||||||
|
cpy["kid-1"] = v1alpha1testing.KeyT{Key: pubKey1Bytes}
|
||||||
|
mockSigner.SupportedKeys.Store(&cpy)
|
||||||
|
mockSigner.AckKeyFetch <- true
|
||||||
|
},
|
||||||
|
shouldPassAuth: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
// Start fresh
|
||||||
|
err := mockSigner.Reset()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to reset signer for the test %q: %v", tc.desc, err)
|
||||||
|
}
|
||||||
|
mockSigner.AckKeyFetch <- true
|
||||||
|
|
||||||
|
// Adjust parameters on mock signer for the test.
|
||||||
|
tc.preTestSignerUpdate()
|
||||||
|
|
||||||
|
// Request a token for ns-1:sa-1.
|
||||||
|
tokenExpirationSec := int64(2 * 60 * 60) // 2h
|
||||||
|
tokenRequest, err := client.CoreV1().ServiceAccounts("ns-1").CreateToken(ctx, "sa-1", &authv1.TokenRequest{
|
||||||
|
Spec: authv1.TokenRequestSpec{
|
||||||
|
ExpirationSeconds: &tokenExpirationSec,
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if tc.wantTokenReqErr != nil {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tc.wantTokenReqErr.Error()) {
|
||||||
|
t.Fatalf("wanted error: %v, got error: %v", tc.wantTokenReqErr, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
t.Fatalf("Error when creating token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust parameters on mock signer for the test.
|
||||||
|
tc.preValidationSignerUpdate()
|
||||||
|
|
||||||
|
// Try Validating the token.
|
||||||
|
tokenReviewResult, err := client.AuthenticationV1().TokenReviews().Create(ctx, &authv1.TokenReview{
|
||||||
|
Spec: authv1.TokenReviewSpec{
|
||||||
|
Token: tokenRequest.Status.Token,
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error when validating token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tokenReviewResult.Status.Authenticated && tc.shouldPassAuth {
|
||||||
|
t.Fatal("Expected Authentication to succeed")
|
||||||
|
} else if tokenReviewResult.Status.Authenticated && !tc.shouldPassAuth {
|
||||||
|
t.Fatal("Expected Authentication to fail")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelayedStartForSigner(t *testing.T) {
|
||||||
|
// Enable feature gate for external JWT signer.
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExternalServiceAccountTokenSigner, true)
|
||||||
|
|
||||||
|
tCtx := ktesting.Init(t)
|
||||||
|
ctx, cancel := context.WithCancel(tCtx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Schedule signer to start on socket after 20 sec
|
||||||
|
socketPath := "@mock-external-jwt-signer.sock"
|
||||||
|
t.Cleanup(func() { _ = os.Remove(socketPath) })
|
||||||
|
go func() {
|
||||||
|
time.Sleep(20 * time.Second)
|
||||||
|
v1alpha1testing.NewMockSigner(t, socketPath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start Api server configured with external signer.
|
||||||
|
client, _, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{
|
||||||
|
ModifyServerRunOptions: func(opt *options.ServerRunOptions) {
|
||||||
|
opt.ServiceAccountSigningEndpoint = socketPath
|
||||||
|
opt.ServiceAccountSigningKeyFile = ""
|
||||||
|
opt.Authentication.ServiceAccounts.KeyFiles = []string{}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer tearDownFn()
|
||||||
|
|
||||||
|
// Create Namesapce (ns-1) to work with.
|
||||||
|
if _, err := client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "ns-1",
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{}); err != nil {
|
||||||
|
t.Fatalf("Error when creating namespace: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ServiceAccount (sa-1) to work with.
|
||||||
|
if _, err := client.CoreV1().ServiceAccounts("ns-1").Create(ctx, &corev1.ServiceAccount{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "sa-1",
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{}); err != nil {
|
||||||
|
t.Fatalf("Error when creating service-account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a token for ns-1:sa-1.
|
||||||
|
tokenExpirationSec := int64(2 * 60 * 60) // 2h
|
||||||
|
tokenRequest, err := client.CoreV1().ServiceAccounts("ns-1").CreateToken(ctx, "sa-1", &authv1.TokenRequest{
|
||||||
|
Spec: authv1.TokenRequestSpec{
|
||||||
|
ExpirationSeconds: &tokenExpirationSec,
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error when creating token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Validating the token.
|
||||||
|
tokenReviewResult, err := client.AuthenticationV1().TokenReviews().Create(ctx, &authv1.TokenReview{
|
||||||
|
Spec: authv1.TokenReviewSpec{
|
||||||
|
Token: tokenRequest.Status.Token,
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error when validating token: %v", err)
|
||||||
|
}
|
||||||
|
if !tokenReviewResult.Status.Authenticated {
|
||||||
|
t.Fatal("Expected Authentication to succeed")
|
||||||
|
}
|
||||||
|
}
|
||||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -1055,6 +1055,8 @@ gopkg.in/yaml.v3
|
|||||||
## explicit; go 1.23.0
|
## explicit; go 1.23.0
|
||||||
# k8s.io/endpointslice v0.0.0 => ./staging/src/k8s.io/endpointslice
|
# k8s.io/endpointslice v0.0.0 => ./staging/src/k8s.io/endpointslice
|
||||||
## explicit; go 1.23.0
|
## explicit; go 1.23.0
|
||||||
|
# k8s.io/externaljwt v0.0.0 => ./staging/src/k8s.io/externaljwt
|
||||||
|
## explicit; go 1.23.0
|
||||||
# k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9
|
# k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
k8s.io/gengo/v2
|
k8s.io/gengo/v2
|
||||||
|
|||||||
Reference in New Issue
Block a user