Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/miekg/dns v1.1.61
github.com/onsi/ginkgo/v2 v2.27.2
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250804142706-7b3ab438a292
github.com/openshift/api v0.0.0-20260302174620-dcac36b908db
github.com/openshift/api v0.0.0-20260304205204-470851c945c9
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee
github.com/openshift/client-go v0.0.0-20260302182750-20813ce71ca6
github.com/openshift/library-go v0.0.0-20260303171201-5d9eb6295ff6
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250804142706-7b3ab438a292 h1:3athg6KQ+TaNfW4BWZDlGFt1ImSZEJWgzXtPC1VPITI=
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250804142706-7b3ab438a292/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M=
github.com/openshift/api v0.0.0-20260302174620-dcac36b908db h1:MOQ5JSIlbP4apwTrEdNpApT6PsnB0/1S6y9aKODp5Ks=
github.com/openshift/api v0.0.0-20260302174620-dcac36b908db/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo=
github.com/openshift/api v0.0.0-20260304205204-470851c945c9 h1:TXC7LRFZQU4Lq/DkCRiTw7bymyH/VI1aXPhlnm51E5Q=
github.com/openshift/api v0.0.0-20260304205204-470851c945c9/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo=
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+Sp5GGnjHDhT/a/nQ1xdp43UscBMr7G5wxsYotyhzJ4=
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE=
github.com/openshift/client-go v0.0.0-20260302182750-20813ce71ca6 h1:wJv4Ia+R4OxoaJcTUyvMtBc5rWFvfTiEA8d5f1MBPqI=
Expand Down
46 changes: 37 additions & 9 deletions pkg/operator/configobservation/auth/auth_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

configv1 "github.com/openshift/api/config/v1"
"github.com/openshift/api/features"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/operatorclient"
"github.com/openshift/library-go/pkg/operator/configobserver"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
"github.com/openshift/library-go/pkg/operator/events"
"github.com/openshift/library-go/pkg/operator/resourcesynccontroller"
)
Expand All @@ -21,17 +23,35 @@ const (
managedNamespace = "openshift-config-managed"
)

var (
topLevelMetadataFilePath = []string{"authConfig", "oauthMetadataFile"}
)
var topLevelMetadataFilePath = []string{"authConfig", "oauthMetadataFile"}

func NewOAuthMetadataObserver(featureGateAccessor featuregates.FeatureGateAccess) configobserver.ObserveConfigFunc {
return (&oauthMetadataObserver{
featureGateAccessor: featureGateAccessor,
}).ObserveAuthMetadata
}

type oauthMetadataObserver struct {
featureGateAccessor featuregates.FeatureGateAccess
}

// ObserveAuthMetadata fills in authConfig.OauthMetadataFile with the path for a configMap referenced by the authentication
// config.
func ObserveAuthMetadata(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, _ []error) {
func (o *oauthMetadataObserver) ObserveAuthMetadata(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, _ []error) {
defer func() {
ret = configobserver.Pruned(ret, topLevelMetadataFilePath)
}()

if !o.featureGateAccessor.AreInitialFeatureGatesObserved() {
// if we haven't observed featuregates yet, return the existing
return existingConfig, nil
}

featureGates, err := o.featureGateAccessor.CurrentFeatureGates()
if err != nil {
return existingConfig, []error{err}
}

listers := genericListers.(configobservation.Listers)
errs := []error{}
prevObservedConfig := map[string]interface{}{}
Expand Down Expand Up @@ -88,11 +108,19 @@ func ObserveAuthMetadata(genericListers configobserver.Listers, recorder events.
// in order to delete the configmap and unset oauthMetadataFile

case configv1.AuthenticationTypeOIDC:
if _, err := listers.ConfigmapLister_.ConfigMaps(operatorclient.TargetNamespace).Get(AuthConfigCMName); errors.IsNotFound(err) {
// auth-config does not exist in target namespace yet; do not remove oauth metadata until it's there
return prevObservedConfig, errs
} else if err != nil {
return prevObservedConfig, append(errs, err)
// When the ExternalOIDCExternalClaimsSourcing feature gate is not enabled the
// existing KAS configuration logic for External OIDC should take place, including
// waiting to remove the oauth metadata.
// We still shouldn't serve oauth metadata when this feature gate is enabled because
// the oauth-apiserver will just become a webhook authenticator and the oauth-server
// will still be removed.
if !featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) {
if _, err := listers.ConfigmapLister_.ConfigMaps(operatorclient.TargetNamespace).Get(AuthConfigCMName); errors.IsNotFound(err) {
// auth-config does not exist in target namespace yet; do not remove oauth metadata until it's there
return prevObservedConfig, errs
} else if err != nil {
return prevObservedConfig, append(errs, err)
}
}

// no oauth metadata is served; do not set anything as source
Expand Down
3 changes: 2 additions & 1 deletion pkg/operator/configobservation/auth/auth_metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
configv1 "github.com/openshift/api/config/v1"
configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
"github.com/openshift/library-go/pkg/operator/events"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
Expand Down Expand Up @@ -313,7 +314,7 @@ func TestObserveAuthMetadata(t *testing.T) {
ResourceSync: &mockResourceSyncer{t: t, synced: synced, error: tt.syncerError},
}

actualConfig, errs := ObserveAuthMetadata(listers, eventRecorder, tt.existingConfig)
actualConfig, errs := NewOAuthMetadataObserver(featuregates.NewHardcodedFeatureGateAccess(nil, nil))(listers, eventRecorder, tt.existingConfig)

if tt.expectErrors != (len(errs) > 0) {
t.Errorf("expected errors: %v; got %v", tt.expectErrors, errs)
Expand Down
8 changes: 8 additions & 0 deletions pkg/operator/configobservation/auth/external_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ func (o *externalOIDC) ObserveExternalOIDC(genericListers configobserver.Listers
return existingConfig, nil
}

// When the ExternalOIDCExternalClaimsSourcing feature gate is enabled, the kube-apiserver
// should not have the built-in Structured Authentication Configuration feature configured,
// which this controller handles.
// When this feature goes GA, this controller should be removed.
if featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) {
return existingConfig, nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear the old auth-config when external claims sourcing is enabled.

return existingConfig at Lines 66-71 keeps any previously observed apiServerArguments.authentication-config and also skips the SyncConfigMap(..., empty) cleanup path below. If a cluster enables ExternalOIDCExternalClaimsSourcing after already using external OIDC, the stale auth-config stays mounted even though this branch is supposed to disable the structured auth flow.

🧹 Suggested cleanup
 	if featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) {
-		return existingConfig, nil
+		if err := genericListers.ResourceSyncer().SyncConfigMap(
+			resourcesynccontroller.ResourceLocation{Namespace: operatorclient.TargetNamespace, Name: AuthConfigCMName},
+			resourcesynccontroller.ResourceLocation{Namespace: "", Name: ""},
+		); err != nil {
+			return existingConfig, []error{err}
+		}
+		return nil, nil
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// When the ExternalOIDCExternalClaimsSourcing feature gate is enabled, the kube-apiserver
// should not have the built-in Structured Authentication Configuration feature configured,
// which this controller handles.
// When this feature goes GA, this controller should be removed.
if featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) {
return existingConfig, nil
// When the ExternalOIDCExternalClaimsSourcing feature gate is enabled, the kube-apiserver
// should not have the built-in Structured Authentication Configuration feature configured,
// which this controller handles.
// When this feature goes GA, this controller should be removed.
if featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) {
if err := genericListers.ResourceSyncer().SyncConfigMap(
resourcesynccontroller.ResourceLocation{Namespace: operatorclient.TargetNamespace, Name: AuthConfigCMName},
resourcesynccontroller.ResourceLocation{Namespace: "", Name: ""},
); err != nil {
return existingConfig, []error{err}
}
return nil, nil
}

}

listers := genericListers.(configobservation.Listers)
auth, err := listers.AuthConfigLister.Get("cluster")
if errors.IsNotFound(err) {
Expand Down
78 changes: 65 additions & 13 deletions pkg/operator/configobservation/auth/webhook_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"

configv1 "github.com/openshift/api/config/v1"
"github.com/openshift/api/features"
"github.com/openshift/library-go/pkg/operator/configobserver"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
"github.com/openshift/library-go/pkg/operator/events"
"github.com/openshift/library-go/pkg/operator/resourcesynccontroller"

Expand All @@ -27,16 +29,36 @@ var (
webhookTokenAuthenticatorVersion = []interface{}{"v1"}
)

func NewObserveWebhookTokenAuthenticator(featureGateAccessor featuregates.FeatureGateAccess) configobserver.ObserveConfigFunc {
return (&webhookTokenAuthenticatorObserver{
featureGateAccessor: featureGateAccessor,
}).ObserveWebhookTokenAuthenticator
}

type webhookTokenAuthenticatorObserver struct {
featureGateAccessor featuregates.FeatureGateAccess
}

// ObserveWebhookTokenAuthenticator observes the webhookTokenAuthenticator field of
// the authentication.config/cluster resource and if kubeConfig secret reference is
// set it uses the contents of this secret as a webhhook token authenticator
// for the API server. It also takes care of synchronizing this secret to the
// openshift-kube-apiserver NS.
func ObserveWebhookTokenAuthenticator(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, _ []error) {
func (o *webhookTokenAuthenticatorObserver) ObserveWebhookTokenAuthenticator(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, _ []error) {
defer func() {
ret = configobserver.Pruned(ret, webhookTokenAuthenticatorPath, webhookTokenAuthenticatorVersionPath)
}()

if !o.featureGateAccessor.AreInitialFeatureGatesObserved() {
// if we haven't observed featuregates yet, return the existing
return existingConfig, nil
}

featureGates, err := o.featureGateAccessor.CurrentFeatureGates()
if err != nil {
return existingConfig, []error{err}
}

listers := genericListers.(configobservation.Listers)
resourceSyncer := genericListers.ResourceSyncer()

Expand Down Expand Up @@ -64,7 +86,11 @@ func ObserveWebhookTokenAuthenticator(genericListers configobserver.Listers, rec
}

observedWebhookConfigured := len(webhookSecretName) > 0
if observedWebhookConfigured && auth.Spec.Type != configv1.AuthenticationTypeOIDC {

// When the ExternalOIDCExternalClaimsSourcing feature gate is enabled, the oauth-apiserver
// will always be the webhook authenticator called by the kube-apiserver.
// This means this should _always_ sync the webhook authenticator secret.
if featureGates.Enabled(features.FeatureGateExternalOIDCExternalClaimsSourcing) {
// retrieve the secret from config and validate it, don't proceed on failure
kubeconfigSecret, err := listers.ConfigSecretLister().Secrets("openshift-config").Get(webhookSecretName)
if err != nil {
Expand All @@ -89,20 +115,46 @@ func ObserveWebhookTokenAuthenticator(genericListers configobserver.Listers, rec
resourcesynccontroller.ResourceLocation{Namespace: operatorclient.GlobalUserSpecifiedConfigNamespace, Name: webhookSecretName},
)
} else {
if auth.Spec.Type == configv1.AuthenticationTypeOIDC {
if _, err := listers.ConfigmapLister_.ConfigMaps(operatorclient.TargetNamespace).Get(AuthConfigCMName); errors.IsNotFound(err) {
// auth-config does not exist in target namespace yet; do not remove webhook until it's there
return existingConfig, errs
} else if err != nil {
if observedWebhookConfigured && auth.Spec.Type != configv1.AuthenticationTypeOIDC {
// retrieve the secret from config and validate it, don't proceed on failure
kubeconfigSecret, err := listers.ConfigSecretLister().Secrets("openshift-config").Get(webhookSecretName)
if err != nil {
return existingConfig, append(errs, fmt.Errorf("failed to get secret openshift-config/%s: %w", webhookSecretName, err))
}

if secretErrors := validateKubeconfigSecret(kubeconfigSecret); len(secretErrors) > 0 {
return existingConfig, append(errs,
fmt.Errorf("secret openshift-config/%s is invalid: %w", webhookSecretName, utilerrors.NewAggregate(secretErrors)))
}

if err := unstructured.SetNestedField(observedConfig, webhookTokenAuthenticatorVersion, webhookTokenAuthenticatorVersionPath...); err != nil {
return existingConfig, append(errs, err)
}
}

// don't sync anything and remove whatever we synced
resourceSyncer.SyncSecret(
resourcesynccontroller.ResourceLocation{Namespace: operatorclient.TargetNamespace, Name: "webhook-authenticator"},
resourcesynccontroller.ResourceLocation{Namespace: "", Name: ""},
)
if err := unstructured.SetNestedField(observedConfig, webhookTokenAuthenticatorFile, webhookTokenAuthenticatorPath...); err != nil {
return existingConfig, append(errs, err)
}

resourceSyncer.SyncSecret(
resourcesynccontroller.ResourceLocation{Namespace: operatorclient.TargetNamespace, Name: "webhook-authenticator"},
resourcesynccontroller.ResourceLocation{Namespace: operatorclient.GlobalUserSpecifiedConfigNamespace, Name: webhookSecretName},
)
} else {
if auth.Spec.Type == configv1.AuthenticationTypeOIDC {
if _, err := listers.ConfigmapLister_.ConfigMaps(operatorclient.TargetNamespace).Get(AuthConfigCMName); errors.IsNotFound(err) {
// auth-config does not exist in target namespace yet; do not remove webhook until it's there
return existingConfig, errs
} else if err != nil {
return existingConfig, append(errs, err)
}
}

// don't sync anything and remove whatever we synced
resourceSyncer.SyncSecret(
resourcesynccontroller.ResourceLocation{Namespace: operatorclient.TargetNamespace, Name: "webhook-authenticator"},
resourcesynccontroller.ResourceLocation{Namespace: "", Name: ""},
)
}
}

if observedWebhookConfigured != existingWebhookConfigured {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
configv1 "github.com/openshift/api/config/v1"
configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
"github.com/openshift/library-go/pkg/operator/events"
"github.com/openshift/library-go/pkg/operator/resourcesynccontroller"
"k8s.io/utils/clock"
Expand Down Expand Up @@ -174,7 +175,7 @@ func TestObserveWebhookTokenAuthenticator(t *testing.T) {

eventRecorder := events.NewInMemoryRecorder("webhookauthenticatortest", clock.RealClock{})

gotConfig, errs := ObserveWebhookTokenAuthenticator(listers, eventRecorder, tt.existingConfig)
gotConfig, errs := NewObserveWebhookTokenAuthenticator(featuregates.NewHardcodedFeatureGateAccess(nil, nil))(listers, eventRecorder, tt.existingConfig)
if !equality.Semantic.DeepEqual(tt.expectedConfig, gotConfig) {
t.Errorf("unexpected config diff: %s", diff.Diff(tt.expectedConfig, gotConfig))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ func NewConfigObserver(operatorClient v1helpers.StaticPodOperatorClient, kubeInf
apiserver.ObserveAdmissionPlugins,
apiserver.NewObserveEventTTL(featureGateAccessor),
libgoapiserver.ObserveTLSSecurityProfile,
auth.ObserveAuthMetadata,
auth.NewOAuthMetadataObserver(featureGateAccessor),
auth.ObserveServiceAccountIssuer,
auth.ObserveWebhookTokenAuthenticator,
auth.NewObserveWebhookTokenAuthenticator(featureGateAccessor),
auth.NewObserveExternalOIDC(featureGateAccessor),
auth.NewObservePodSecurityAdmissionEnforcementFunc(featureGateAccessor),
encryption.NewEncryptionConfigObserver(
Expand Down
14 changes: 14 additions & 0 deletions vendor/github.com/openshift/api/AGENTS.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions vendor/github.com/openshift/api/features.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading