introduce new configuration option to disable admission plugin types

This commit is contained in:
Ryan Richard
2025-03-17 14:49:17 -07:00
parent 035dbffd28
commit d90b3c23ef
13 changed files with 283 additions and 137 deletions

View File

@@ -1,4 +1,4 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -68,6 +68,7 @@ data:
apiGroupSuffix: (@= data.values.api_group_suffix @)
# aggregatedAPIServerPort may be set here, although other YAML references to the default port (10250) may also need to be updated
# impersonationProxyServerPort may be set here, although other YAML references to the default port (8444) may also need to be updated
aggregatedAPIServerDisableAdmissionPlugins: []
names:
servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @)
credentialIssuer: (@= defaultResourceNameWithSuffix("config") @)

View File

@@ -1,4 +1,4 @@
#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
#! Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
@@ -61,7 +61,8 @@ _: #@ template.replace(data.values.custom_labels)
#@ "audit": {
#@ "logUsernamesAndGroups": data.values.audit.log_usernames_and_groups,
#@ "logInternalPaths": data.values.audit.log_internal_paths
#@ }
#@ },
#@ "aggregatedAPIServerDisableAdmissionPlugins": []
#@ }
#@ if data.values.log_level:
#@ config["log"] = {}

View File

@@ -1,17 +1,15 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2024-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package admissionpluginconfig
import (
"fmt"
"slices"
"github.com/pkg/errors"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
"k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
"k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/discovery"
@@ -19,6 +17,22 @@ import (
"go.pinniped.dev/internal/plog"
)
// ValidateAdmissionPluginNames returns an error if any of the given pluginNames is unrecognized.
func ValidateAdmissionPluginNames(pluginNames []string) error {
var pluginsNotFound []string
admissionOptions := options.NewAdmissionOptions()
for _, pluginName := range pluginNames {
if !slices.Contains(admissionOptions.RecommendedPluginOrder, pluginName) {
pluginsNotFound = append(pluginsNotFound, pluginName)
}
}
if len(pluginsNotFound) > 0 {
return fmt.Errorf("admission plugin names not recognized: %s (each must be one of %s)",
pluginsNotFound, admissionOptions.RecommendedPluginOrder)
}
return nil
}
// ConfigureAdmissionPlugins may choose to reconfigure the admission plugins present on the given
// RecommendedOptions by mutating it.
//
@@ -27,34 +41,46 @@ import (
// then the new admission ValidatingAdmissionPolicy plugin prevents all our aggregated APIs from working, seemingly
// because it fails to sync informers created for watching the related resources. As a workaround, ask the k8s API
// server if it has the ValidatingAdmissionPolicy resource, and configure our admission plugins accordingly.
func ConfigureAdmissionPlugins(recommendedOptions *options.RecommendedOptions) error {
//
// Any plugin name passed via the disableAdmissionPlugins parameter will also be disabled.
// The values in this parameter should be validated by the caller using ValidateAdmissionPluginNames before
// being passed into this function.
func ConfigureAdmissionPlugins(recommendedOptions *options.RecommendedOptions, disableAdmissionPlugins []string) error {
k8sClient, err := kubeclient.New()
if err != nil {
return fmt.Errorf("failed to create kube client: %w", err)
}
return configureAdmissionPlugins(k8sClient.Kubernetes.Discovery(), recommendedOptions)
return configureAdmissionPlugins(k8sClient.Kubernetes.Discovery(), recommendedOptions, disableAdmissionPlugins)
}
// configureAdmissionPlugins is the same as ConfigureAdmissionPlugins but allows client injection for unit testing.
func configureAdmissionPlugins(discoveryClient discovery.ServerResourcesInterface, recommendedOptions *options.RecommendedOptions) error {
// Check if the API server has such a resource.
hasValidatingAdmissionPolicyResource, err := k8sAPIServerHasValidatingAdmissionPolicyResource(discoveryClient)
if err != nil {
return fmt.Errorf("failed looking up availability of ValidatingAdmissionPolicy resource: %w", err)
func configureAdmissionPlugins(
discoveryClient discovery.ServerResourcesInterface,
recommendedOptions *options.RecommendedOptions,
disableAdmissionPlugins []string,
) error {
if !slices.Contains(disableAdmissionPlugins, validatingadmissionpolicy.PluginName) {
// The admin did not explicitly disable the ValidatingAdmissionPolicy plugin, but we may still need disable it if
// the Kubernetes cluster on which we are running is too old. Check if the API server has such a resource.
hasValidatingAdmissionPolicyResource, err := k8sAPIServerHasValidatingAdmissionPolicyResource(discoveryClient)
if err != nil {
return fmt.Errorf("failed looking up availability of ValidatingAdmissionPolicy resource: %w", err)
}
if !hasValidatingAdmissionPolicyResource {
// Customize the admission plugins to avoid using the new ValidatingAdmissionPolicy plugin.
plog.Warning("could not find ValidatingAdmissionPolicy resource on this Kubernetes cluster " +
"(which is normal for clusters older than Kubernetes 1.30); " +
"disabling ValidatingAdmissionPolicy admission plugins for all Pinniped aggregated API resource types")
disableAdmissionPlugins = append(disableAdmissionPlugins, validatingadmissionpolicy.PluginName)
}
}
if hasValidatingAdmissionPolicyResource {
// Accept the default admission plugin configuration without any further modification.
return nil
// Mutate the recommendedOptions to potentially disable some admission plugins.
if len(disableAdmissionPlugins) > 0 {
recommendedOptions.Admission.DisablePlugins = disableAdmissionPlugins
}
// Customize the admission plugins to avoid using the new ValidatingAdmissionPolicy plugin.
plog.Warning("could not find ValidatingAdmissionPolicy resource on this Kubernetes cluster " +
"(which is normal for clusters older than Kubernetes 1.30); " +
"disabling ValidatingAdmissionPolicy admission plugins for all Pinniped aggregated API resource types")
mutateOptionsToUseOldStyleAdmissionPlugins(recommendedOptions)
return nil
}
@@ -100,26 +126,3 @@ func k8sAPIServerHasValidatingAdmissionPolicyResource(discoveryClient discovery.
// Didn't findValidatingAdmissionPolicy on this cluster.
return false, nil
}
func mutateOptionsToUseOldStyleAdmissionPlugins(recommendedOptions *options.RecommendedOptions) {
plugins := admission.NewPlugins()
// These lines are copied from server.RegisterAllAdmissionPlugins in k8s.io/apiserver@v0.30.0/pkg/server/plugins.go.
lifecycle.Register(plugins)
validating.Register(plugins)
mutating.Register(plugins)
// Note that we are not adding this one:
// validatingadmissionpolicy.Register(newAdmissionPlugins)
// This list is copied from the implementation of NewAdmissionOptions() in k8s.io/apiserver@v0.30.0/pkg/server/options/admission.go
recommendedOptions.Admission.RecommendedPluginOrder = []string{
lifecycle.PluginName,
mutating.PluginName,
// Again, note that we are not adding this one:
// validatingadmissionpolicy.PluginName,
validating.PluginName,
}
// Overwrite the registered plugins with our new, smaller list.
recommendedOptions.Admission.Plugins = plugins
}

View File

@@ -1,11 +1,10 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2024-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package admissionpluginconfig
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/require"
@@ -15,7 +14,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/discovery"
kubernetesfake "k8s.io/client-go/kubernetes/fake"
@@ -23,17 +21,66 @@ import (
kubetesting "k8s.io/client-go/testing"
)
func TestValidateAdmissionPluginNames(t *testing.T) {
tests := []struct {
name string
pluginNames []string
wantErr string
}{
{
name: "empty",
pluginNames: []string{},
},
{
name: "all current valid values (this list may change in future versions of Kubernetes packages)",
pluginNames: []string{
"NamespaceLifecycle",
"MutatingAdmissionWebhook",
"ValidatingAdmissionPolicy",
"ValidatingAdmissionWebhook",
},
},
{
name: "one invalid value",
pluginNames: []string{
"NamespaceLifecycle",
"MutatingAdmissionWebhook",
"ValidatingAdmissionPolicy",
"foobar",
"ValidatingAdmissionWebhook",
},
wantErr: "admission plugin names not recognized: [foobar] (each must be one of [NamespaceLifecycle MutatingAdmissionWebhook ValidatingAdmissionPolicy ValidatingAdmissionWebhook])",
},
{
name: "multiple invalid values",
pluginNames: []string{
"NamespaceLifecycle",
"MutatingAdmissionWebhook",
"foobat",
"ValidatingAdmissionPolicy",
"foobar",
"ValidatingAdmissionWebhook",
"foobaz",
},
wantErr: "admission plugin names not recognized: [foobat foobar foobaz] (each must be one of [NamespaceLifecycle MutatingAdmissionWebhook ValidatingAdmissionPolicy ValidatingAdmissionWebhook])",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := ValidateAdmissionPluginNames(tt.pluginNames)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestConfigureAdmissionPlugins(t *testing.T) {
defaultPlugins := admission.NewPlugins()
defaultPlugins.Register("fake-plugin1", func(config io.Reader) (admission.Interface, error) { return nil, nil })
defaultPlugins.Register("fake-plugin2", func(config io.Reader) (admission.Interface, error) { return nil, nil })
defaultPluginsRegistered := []string{"fake-plugin1", "fake-plugin2"}
defaultRecommendedPluginOrder := []string{"fake-plugin2", "fake-plugin1"}
customOldStylePluginsRegistered := []string{"MutatingAdmissionWebhook", "NamespaceLifecycle", "ValidatingAdmissionWebhook"}
customOldStyleRecommendedPluginOrder := []string{"NamespaceLifecycle", "MutatingAdmissionWebhook", "ValidatingAdmissionWebhook"}
coreResources := &metav1.APIResourceList{
GroupVersion: corev1.SchemeGroupVersion.String(),
APIResources: []metav1.APIResource{
@@ -73,49 +120,75 @@ func TestConfigureAdmissionPlugins(t *testing.T) {
}
tests := []struct {
name string
availableAPIResources []*metav1.APIResourceList
discoveryErr error
wantErr string
wantRegisteredPlugins []string
wantRecommendedPluginOrder []string
name string
disabledPlugins []string
availableAPIResources []*metav1.APIResourceList
discoveryErr error
wantErr string
wantDisabledPlugins []string
}{
{
name: "when there is a ValidatingAdmissionPolicy resource, then we do not change the plugin configuration",
name: "when there is a ValidatingAdmissionPolicy resource and nil disabled list, then we do not change the plugin configuration",
availableAPIResources: []*metav1.APIResourceList{
coreResources,
newStyleAdmissionResourcesWithValidatingAdmissionPolicies,
appsResources,
},
wantRegisteredPlugins: defaultPluginsRegistered,
wantRecommendedPluginOrder: defaultRecommendedPluginOrder,
disabledPlugins: nil,
wantDisabledPlugins: nil,
},
{
name: "when there is no ValidatingAdmissionPolicy resource, as there would not be in an old Kubernetes cluster, then we change the plugin configuration to be more like it was for old versions of Kubernetes",
name: "when there is a ValidatingAdmissionPolicy resource and empty disabled list, then we do not change the plugin configuration",
availableAPIResources: []*metav1.APIResourceList{
coreResources,
newStyleAdmissionResourcesWithValidatingAdmissionPolicies,
appsResources,
},
disabledPlugins: []string{},
wantDisabledPlugins: nil,
},
{
name: "when there is no ValidatingAdmissionPolicy resource, as there would not be in an old Kubernetes cluster, then we disable that admission plugin",
availableAPIResources: []*metav1.APIResourceList{
coreResources,
oldStyleAdmissionResourcesWithoutValidatingAdmissionPolicies,
appsResources,
},
wantRegisteredPlugins: customOldStylePluginsRegistered,
wantRecommendedPluginOrder: customOldStyleRecommendedPluginOrder,
disabledPlugins: nil,
wantDisabledPlugins: []string{"ValidatingAdmissionPolicy"},
},
{
name: "when there is only an older version of ValidatingAdmissionPolicy resource, as there would be in an old Kubernetes cluster with the feature flag enabled, then we change the plugin configuration to be more like it was for old versions of Kubernetes (because the admission code wants to watch v1)",
name: "when there is only an older version of ValidatingAdmissionPolicy resource, as there would be in an old Kubernetes cluster with the feature flag enabled, then we disable that plugin (because the admission code wants to watch v1)",
availableAPIResources: []*metav1.APIResourceList{
coreResources,
newStyleAdmissionResourcesWithValidatingAdmissionPoliciesAtOlderAPIVersion,
appsResources,
},
wantRegisteredPlugins: customOldStylePluginsRegistered,
wantRecommendedPluginOrder: customOldStyleRecommendedPluginOrder,
disabledPlugins: []string{},
wantDisabledPlugins: []string{"ValidatingAdmissionPolicy"},
},
{
name: "when there is a total error returned by discovery",
discoveryErr: errors.New("total error from API discovery client"),
wantErr: "failed looking up availability of ValidatingAdmissionPolicy resource: failed to perform k8s API discovery: total error from API discovery client",
wantRegisteredPlugins: defaultPluginsRegistered,
wantRecommendedPluginOrder: defaultRecommendedPluginOrder,
name: "when there is no ValidatingAdmissionPolicy resource, and the ValidatingAdmissionPolicy plugin was explicitly disabled, then do not perform discovery, and just disable it",
availableAPIResources: []*metav1.APIResourceList{},
discoveryErr: errors.New("total error from API discovery client"),
disabledPlugins: []string{"MutatingAdmissionWebhook", "ValidatingAdmissionPolicy"},
wantDisabledPlugins: []string{"MutatingAdmissionWebhook", "ValidatingAdmissionPolicy"},
},
{
name: "when there is no ValidatingAdmissionPolicy resource, and the ValidatingAdmissionPolicy plugin was not explicitly disabled, still disable it",
availableAPIResources: []*metav1.APIResourceList{
coreResources,
oldStyleAdmissionResourcesWithoutValidatingAdmissionPolicies,
appsResources,
},
disabledPlugins: []string{"MutatingAdmissionWebhook", "NamespaceLifecycle"},
wantDisabledPlugins: []string{"MutatingAdmissionWebhook", "NamespaceLifecycle", "ValidatingAdmissionPolicy"},
},
{
name: "when there is a total error returned by discovery",
discoveryErr: errors.New("total error from API discovery client"),
wantErr: "failed looking up availability of ValidatingAdmissionPolicy resource: failed to perform k8s API discovery: total error from API discovery client",
wantDisabledPlugins: nil,
},
{
name: "when there is a partial error returned by discovery which does include the group of interest, then we cannot ignore the error, because we could not discover anything about that group",
@@ -128,9 +201,8 @@ func TestConfigureAdmissionPlugins(t *testing.T) {
schema.GroupVersion{Group: "someGroup", Version: "v1"}: errors.New("fake error for someGroup"),
schema.GroupVersion{Group: "admissionregistration.k8s.io", Version: "v1"}: errors.New("fake error for admissionregistration"),
}},
wantErr: "failed looking up availability of ValidatingAdmissionPolicy resource: unable to retrieve the complete list of server APIs: admissionregistration.k8s.io/v1: fake error for admissionregistration, someGroup/v1: fake error for someGroup",
wantRegisteredPlugins: defaultPluginsRegistered,
wantRecommendedPluginOrder: defaultRecommendedPluginOrder,
wantErr: "failed looking up availability of ValidatingAdmissionPolicy resource: unable to retrieve the complete list of server APIs: admissionregistration.k8s.io/v1: fake error for admissionregistration, someGroup/v1: fake error for someGroup",
wantDisabledPlugins: nil,
},
{
name: "when there is a partial error returned by discovery on an new-style cluster which does not include the group of interest, then we can ignore the error and use the default plugins",
@@ -143,8 +215,7 @@ func TestConfigureAdmissionPlugins(t *testing.T) {
schema.GroupVersion{Group: "someGroup", Version: "v1"}: errors.New("fake error for someGroup"),
schema.GroupVersion{Group: "someOtherGroup", Version: "v1"}: errors.New("fake error for someOtherGroup"),
}},
wantRegisteredPlugins: defaultPluginsRegistered,
wantRecommendedPluginOrder: defaultRecommendedPluginOrder,
wantDisabledPlugins: nil,
},
{
name: "when there is a partial error returned by discovery on an old-style cluster which does not include the group of interest, then we can ignore the error and customize the plugins",
@@ -157,8 +228,7 @@ func TestConfigureAdmissionPlugins(t *testing.T) {
schema.GroupVersion{Group: "someGroup", Version: "v1"}: errors.New("fake error for someGroup"),
schema.GroupVersion{Group: "someOtherGroup", Version: "v1"}: errors.New("fake error for someOtherGroup"),
}},
wantRegisteredPlugins: customOldStylePluginsRegistered,
wantRecommendedPluginOrder: customOldStyleRecommendedPluginOrder,
wantDisabledPlugins: []string{"ValidatingAdmissionPolicy"},
},
}
@@ -185,18 +255,13 @@ func TestConfigureAdmissionPlugins(t *testing.T) {
}
opts := &options.RecommendedOptions{
Admission: &options.AdmissionOptions{
Plugins: defaultPlugins,
RecommendedPluginOrder: defaultRecommendedPluginOrder,
},
Admission: options.NewAdmissionOptions(),
}
// Sanity checks on opts before we use it.
require.Equal(t, defaultPlugins, opts.Admission.Plugins)
require.Equal(t, defaultPluginsRegistered, opts.Admission.Plugins.Registered())
require.Equal(t, defaultRecommendedPluginOrder, opts.Admission.RecommendedPluginOrder)
require.Empty(t, opts.Admission.DisablePlugins)
// Call the function under test.
err := configureAdmissionPlugins(discoveryClient, opts)
err := configureAdmissionPlugins(discoveryClient, opts, tt.disabledPlugins)
if tt.wantErr == "" {
require.NoError(t, err)
@@ -205,8 +270,7 @@ func TestConfigureAdmissionPlugins(t *testing.T) {
}
// Check the expected side effects of the function under test, if any.
require.Equal(t, tt.wantRegisteredPlugins, opts.Admission.Plugins.Registered())
require.Equal(t, tt.wantRecommendedPluginOrder, opts.Admission.RecommendedPluginOrder)
require.Equal(t, tt.wantDisabledPlugins, opts.Admission.DisablePlugins)
})
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package server is the command line entry point for pinniped-concierge.
@@ -214,6 +214,7 @@ func (a *App) runServer(ctx context.Context) error {
identityGV,
auditLogger,
tokenClient,
cfg.AggregatedAPIServerDisableAdmissionPlugins,
)
if err != nil {
return fmt.Errorf("could not configure aggregated API server: %w", err)
@@ -243,6 +244,7 @@ func getAggregatedAPIServerConfig(
loginConciergeGroupVersion, identityConciergeGroupVersion schema.GroupVersion,
auditLogger plog.AuditLogger,
tokenClient *tokenclient.TokenClient,
disableAdmissionPlugins []string,
) (*apiserver.Config, error) {
codecs := serializer.NewCodecFactory(scheme)
@@ -259,7 +261,7 @@ func getAggregatedAPIServerConfig(
// This port is configurable. It should be safe to cast because the config reader already validated it.
recommendedOptions.SecureServing.BindPort = int(aggregatedAPIServerPort)
err := admissionpluginconfig.ConfigureAdmissionPlugins(recommendedOptions)
err := admissionpluginconfig.ConfigureAdmissionPlugins(recommendedOptions, disableAdmissionPlugins)
if err != nil {
return nil, fmt.Errorf("failed to configure admission plugins on recommended options: %w", err)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package concierge contains functionality to load/store Config's from/to
@@ -14,6 +14,7 @@ import (
"k8s.io/utils/ptr"
"sigs.k8s.io/yaml"
"go.pinniped.dev/internal/admissionpluginconfig"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/crypto/ptls"
"go.pinniped.dev/internal/groupsuffix"
@@ -68,6 +69,10 @@ func FromPath(ctx context.Context, path string, setAllowedCiphers ptls.SetAllowe
return nil, fmt.Errorf("validate apiGroupSuffix: %w", err)
}
if err := admissionpluginconfig.ValidateAdmissionPluginNames(config.AggregatedAPIServerDisableAdmissionPlugins); err != nil {
return nil, fmt.Errorf("validate aggregatedAPIServerDisableAdmissionPlugins: %w", err)
}
if err := validateServerPort(config.AggregatedAPIServerPort); err != nil {
return nil, fmt.Errorf("validate aggregatedAPIServerPort: %w", err)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package concierge
@@ -36,6 +36,11 @@ func TestFromPath(t *testing.T) {
renewBeforeSeconds: 2400
apiGroupSuffix: some.suffix.com
aggregatedAPIServerPort: 12345
aggregatedAPIServerDisableAdmissionPlugins:
- NamespaceLifecycle
- MutatingAdmissionWebhook
- ValidatingAdmissionPolicy
- ValidatingAdmissionWebhook
impersonationProxyServerPort: 4242
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
@@ -80,8 +85,14 @@ func TestFromPath(t *testing.T) {
RenewBeforeSeconds: ptr.To[int64](2400),
},
},
APIGroupSuffix: ptr.To("some.suffix.com"),
AggregatedAPIServerPort: ptr.To[int64](12345),
APIGroupSuffix: ptr.To("some.suffix.com"),
AggregatedAPIServerPort: ptr.To[int64](12345),
AggregatedAPIServerDisableAdmissionPlugins: []string{
"NamespaceLifecycle",
"MutatingAdmissionWebhook",
"ValidatingAdmissionPolicy",
"ValidatingAdmissionWebhook",
},
ImpersonationProxyServerPort: ptr.To[int64](4242),
NamesConfig: NamesConfigSpec{
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
@@ -134,6 +145,11 @@ func TestFromPath(t *testing.T) {
renewBeforeSeconds: 2400
apiGroupSuffix: some.suffix.com
aggregatedAPIServerPort: 12345
aggregatedAPIServerDisableAdmissionPlugins:
- NamespaceLifecycle
- MutatingAdmissionWebhook
- ValidatingAdmissionPolicy
- ValidatingAdmissionWebhook
impersonationProxyServerPort: 4242
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
@@ -173,8 +189,14 @@ func TestFromPath(t *testing.T) {
RenewBeforeSeconds: ptr.To[int64](2400),
},
},
APIGroupSuffix: ptr.To("some.suffix.com"),
AggregatedAPIServerPort: ptr.To[int64](12345),
APIGroupSuffix: ptr.To("some.suffix.com"),
AggregatedAPIServerPort: ptr.To[int64](12345),
AggregatedAPIServerDisableAdmissionPlugins: []string{
"NamespaceLifecycle",
"MutatingAdmissionWebhook",
"ValidatingAdmissionPolicy",
"ValidatingAdmissionWebhook",
},
ImpersonationProxyServerPort: ptr.To[int64](4242),
NamesConfig: NamesConfigSpec{
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
@@ -298,6 +320,9 @@ func TestFromPath(t *testing.T) {
Image: ptr.To("debian:latest"),
},
Audit: AuditSpec{LogUsernamesAndGroups: ""},
AggregatedAPIServerDisableAdmissionPlugins: nil,
TLS: TLSSpec{},
Log: plog.LogSpec{},
},
},
{
@@ -615,6 +640,22 @@ func TestFromPath(t *testing.T) {
`),
wantError: "validate apiGroupSuffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
},
{
name: "Invalid aggregatedAPIServerDisableAdmissionPlugins",
yaml: here.Doc(`
---
aggregatedAPIServerDisableAdmissionPlugins: [foobar, ValidatingAdmissionWebhook, foobaz]
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
credentialIssuer: pinniped-config
apiService: pinniped-api
impersonationLoadBalancerService: impersonationLoadBalancerService-value
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
impersonationCACertificateSecret: impersonationCACertificateSecret-value
impersonationSignerSecret: impersonationSignerSecret-value
`),
wantError: "validate aggregatedAPIServerDisableAdmissionPlugins: admission plugin names not recognized: [foobar foobaz] (each must be one of [NamespaceLifecycle MutatingAdmissionWebhook ValidatingAdmissionPolicy ValidatingAdmissionWebhook])",
},
{
name: "returns setAllowedCiphers errors",
yaml: here.Doc(`

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package concierge
@@ -12,17 +12,18 @@ const (
// Config contains knobs to set up an instance of the Pinniped Concierge.
type Config struct {
DiscoveryInfo DiscoveryInfoSpec `json:"discovery"`
APIConfig APIConfigSpec `json:"api"`
APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"`
AggregatedAPIServerPort *int64 `json:"aggregatedAPIServerPort"`
ImpersonationProxyServerPort *int64 `json:"impersonationProxyServerPort"`
NamesConfig NamesConfigSpec `json:"names"`
KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"`
Labels map[string]string `json:"labels"`
Log plog.LogSpec `json:"log"`
TLS TLSSpec `json:"tls"`
Audit AuditSpec `json:"audit"`
DiscoveryInfo DiscoveryInfoSpec `json:"discovery"`
APIConfig APIConfigSpec `json:"api"`
APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"`
AggregatedAPIServerPort *int64 `json:"aggregatedAPIServerPort"`
AggregatedAPIServerDisableAdmissionPlugins []string `json:"aggregatedAPIServerDisableAdmissionPlugins"`
ImpersonationProxyServerPort *int64 `json:"impersonationProxyServerPort"`
NamesConfig NamesConfigSpec `json:"names"`
KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"`
Labels map[string]string `json:"labels"`
Log plog.LogSpec `json:"log"`
TLS TLSSpec `json:"tls"`
Audit AuditSpec `json:"audit"`
}
type AuditUsernamesAndGroups string

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package supervisor contains functionality to load/store Config's from/to
@@ -15,6 +15,7 @@ import (
"k8s.io/utils/ptr"
"sigs.k8s.io/yaml"
"go.pinniped.dev/internal/admissionpluginconfig"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/crypto/ptls"
"go.pinniped.dev/internal/groupsuffix"
@@ -57,6 +58,10 @@ func FromPath(ctx context.Context, path string, setAllowedCiphers ptls.SetAllowe
return nil, fmt.Errorf("validate apiGroupSuffix: %w", err)
}
if err := admissionpluginconfig.ValidateAdmissionPluginNames(config.AggregatedAPIServerDisableAdmissionPlugins); err != nil {
return nil, fmt.Errorf("validate aggregatedAPIServerDisableAdmissionPlugins: %w", err)
}
maybeSetAggregatedAPIServerPortDefaults(&config.AggregatedAPIServerPort)
if err := validateServerPort(config.AggregatedAPIServerPort); err != nil {

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package supervisor
@@ -46,6 +46,11 @@ func TestFromPath(t *testing.T) {
level: info
format: json
aggregatedAPIServerPort: 12345
aggregatedAPIServerDisableAdmissionPlugins:
- NamespaceLifecycle
- MutatingAdmissionWebhook
- ValidatingAdmissionPolicy
- ValidatingAdmissionWebhook
tls:
onedottwo:
allowedCiphers:
@@ -80,6 +85,12 @@ func TestFromPath(t *testing.T) {
Format: plog.FormatJSON,
},
AggregatedAPIServerPort: ptr.To[int64](12345),
AggregatedAPIServerDisableAdmissionPlugins: []string{
"NamespaceLifecycle",
"MutatingAdmissionWebhook",
"ValidatingAdmissionPolicy",
"ValidatingAdmissionWebhook",
},
TLS: TLSSpec{
OneDotTwo: TLSProtocolSpec{
AllowedCiphers: []string{
@@ -134,6 +145,9 @@ func TestFromPath(t *testing.T) {
LogInternalPaths: "",
LogUsernamesAndGroups: "",
},
AggregatedAPIServerDisableAdmissionPlugins: nil,
TLS: TLSSpec{},
Log: plog.LogSpec{},
},
},
{
@@ -346,6 +360,16 @@ func TestFromPath(t *testing.T) {
allowedCiphersError: fmt.Errorf("some error from setAllowedCiphers"),
wantError: "validate tls: some error from setAllowedCiphers",
},
{
name: "invalid aggregatedAPIServerDisableAdmissionPlugins",
yaml: here.Doc(`
---
names:
defaultTLSCertificateSecret: my-secret-name
aggregatedAPIServerDisableAdmissionPlugins: [foobar, ValidatingAdmissionWebhook, foobaz]
`),
wantError: "validate aggregatedAPIServerDisableAdmissionPlugins: admission plugin names not recognized: [foobar foobaz] (each must be one of [NamespaceLifecycle MutatingAdmissionWebhook ValidatingAdmissionPolicy ValidatingAdmissionWebhook])",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package supervisor
@@ -14,14 +14,15 @@ const (
// Config contains knobs to set up an instance of the Pinniped Supervisor.
type Config struct {
APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"`
Labels map[string]string `json:"labels"`
NamesConfig NamesConfigSpec `json:"names"`
Log plog.LogSpec `json:"log"`
Endpoints *Endpoints `json:"endpoints"`
AggregatedAPIServerPort *int64 `json:"aggregatedAPIServerPort"`
TLS TLSSpec `json:"tls"`
Audit AuditSpec `json:"audit"`
APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"`
Labels map[string]string `json:"labels"`
NamesConfig NamesConfigSpec `json:"names"`
Log plog.LogSpec `json:"log"`
Endpoints *Endpoints `json:"endpoints"`
AggregatedAPIServerPort *int64 `json:"aggregatedAPIServerPort"`
AggregatedAPIServerDisableAdmissionPlugins []string `json:"aggregatedAPIServerDisableAdmissionPlugins"`
TLS TLSSpec `json:"tls"`
Audit AuditSpec `json:"audit"`
}
type AuditInternalPaths string

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package credentialrequest provides REST functionality for the CredentialRequest resource.
@@ -224,10 +224,6 @@ func validateRequest(ctx context.Context, obj runtime.Object, createValidation r
}
// let dynamic admission webhooks have a chance to validate (but not mutate) as well
// TODO Since we are an aggregated API, we should investigate to see if the kube API server is already invoking admission hooks for us.
// Even if it is, its okay to call it again here. However, if the kube API server is already calling the webhooks and passing
// the token, then there is probably no reason for us to avoid passing the token when we call the webhooks here, since
// they already got the token.
if createValidation != nil {
requestForValidation := obj.DeepCopyObject()
requestForValidation.(*loginapi.TokenCredentialRequest).Spec.Token = ""

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package server defines the entrypoint for the Pinniped Supervisor server.
@@ -530,6 +530,7 @@ func runSupervisor(ctx context.Context, podInfo *downward.PodInfo, cfg *supervis
client.PinnipedSupervisor.ConfigV1alpha1().OIDCClients(serverInstallationNamespace),
serverInstallationNamespace,
auditLogger,
cfg.AggregatedAPIServerDisableAdmissionPlugins,
)
if err != nil {
return fmt.Errorf("could not configure aggregated API server: %w", err)
@@ -641,6 +642,7 @@ func getAggregatedAPIServerConfig(
oidcClients v1alpha1.OIDCClientInterface,
serverInstallationNamespace string,
auditLogger plog.AuditLogger,
disableAdmissionPlugins []string,
) (*apiserver.Config, error) {
codecs := serializer.NewCodecFactory(scheme)
@@ -657,7 +659,7 @@ func getAggregatedAPIServerConfig(
// This port is configurable. It should be safe to cast because the config reader already validated it.
recommendedOptions.SecureServing.BindPort = int(aggregatedAPIServerPort)
err := admissionpluginconfig.ConfigureAdmissionPlugins(recommendedOptions)
err := admissionpluginconfig.ConfigureAdmissionPlugins(recommendedOptions, disableAdmissionPlugins)
if err != nil {
return nil, fmt.Errorf("failed to configure admission plugins on recommended options: %w", err)
}