Skip to content

Commit

Permalink
featuregate UID in RequestHeader authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
stlaz committed Dec 4, 2024
1 parent 2b472fe commit a051b06
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
utilfeature "k8s.io/apiserver/pkg/util/feature"
corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
Expand Down Expand Up @@ -262,10 +264,13 @@ func getConfigMapDataFor(authenticationInfo ClusterAuthenticationInfo) (map[stri
if err != nil {
return nil, err
}
data["requestheader-uid-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUIDHeaders.Value())
if err != nil {
return nil, err
if utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) && len(authenticationInfo.RequestHeaderUIDHeaders.Value()) > 0 {
data["requestheader-uid-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUIDHeaders.Value())
if err != nil {
return nil, err
}
}

data["requestheader-group-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderGroupHeaders.Value())
if err != nil {
return nil, err
Expand Down Expand Up @@ -305,9 +310,12 @@ func getClusterAuthenticationInfoFor(data map[string]string) (ClusterAuthenticat
if err != nil {
return ClusterAuthenticationInfo{}, err
}
ret.RequestHeaderUIDHeaders, err = jsonDeserializeStringSlice(data["requestheader-uid-headers"])
if err != nil {
return ClusterAuthenticationInfo{}, err

if utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) {
ret.RequestHeaderUIDHeaders, err = jsonDeserializeStringSlice(data["requestheader-uid-headers"])
if err != nil {
return ClusterAuthenticationInfo{}, err
}
}

if caBundle := data["requestheader-client-ca-file"]; len(caBundle) > 0 {
Expand Down
4 changes: 4 additions & 0 deletions pkg/features/versioned_kube_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},

genericfeatures.RemoteRequestHeaderUID: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},

genericfeatures.ResilientWatchCacheInitialization: {
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
Expand Down
11 changes: 11 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/features/kube_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ const (
// to a chunking list request.
RemainingItemCount featuregate.Feature = "RemainingItemCount"

// owner: @stlaz
//
// Enable kube-apiserver to accept UIDs via request header authentication.
// This will also make the kube-apiserver's API aggregator add UIDs via standard
// headers when forwarding requests to the servers serving the aggregated API.
RemoteRequestHeaderUID featuregate.Feature = "RemoteRequestHeaderUID"

// owner: @wojtek-t
//
// Enables resilient watchcache initialization to avoid controlplane
Expand Down Expand Up @@ -359,6 +366,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},

RemoteRequestHeaderUID: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},

ResilientWatchCacheInitialization: {
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
Expand Down
25 changes: 17 additions & 8 deletions staging/src/k8s.io/apiserver/pkg/server/options/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import (
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
Expand Down Expand Up @@ -68,9 +70,6 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error {
if err := checkForWhiteSpaceOnly("requestheader-username-headers", s.UsernameHeaders...); err != nil {
allErrors = append(allErrors, err)
}
if err := checkForWhiteSpaceOnly("requestheader-uid-headers", s.UIDHeaders...); err != nil {
allErrors = append(allErrors, err)
}
if err := checkForWhiteSpaceOnly("requestheader-group-headers", s.GroupHeaders...); err != nil {
allErrors = append(allErrors, err)
}
Expand All @@ -84,17 +83,27 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error {
if len(s.UsernameHeaders) > 0 && !caseInsensitiveHas(s.UsernameHeaders, "X-Remote-User") {
klog.Warningf("--requestheader-username-headers is set without specifying the standard X-Remote-User header - API aggregation will not work")
}
if len(s.UIDHeaders) > 0 && !caseInsensitiveHas(s.UIDHeaders, "X-Remote-Uid") {
// this was added later and so we are able to error out
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers is set without specifying the standard X-Remote-Uid header - API aggregation will not work"))
}
if len(s.GroupHeaders) > 0 && !caseInsensitiveHas(s.GroupHeaders, "X-Remote-Group") {
klog.Warningf("--requestheader-group-headers is set without specifying the standard X-Remote-Group header - API aggregation will not work")
}
if len(s.ExtraHeaderPrefixes) > 0 && !caseInsensitiveHas(s.ExtraHeaderPrefixes, "X-Remote-Extra-") {
klog.Warningf("--requestheader-extra-headers-prefix is set without specifying the standard X-Remote-Extra- header prefix - API aggregation will not work")
}

if !utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) {
if len(s.UIDHeaders) > 0 {
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers requires the %q feature to be enabled", features.RemoteRequestHeaderUID))
}
} else {
if err := checkForWhiteSpaceOnly("requestheader-uid-headers", s.UIDHeaders...); err != nil {
allErrors = append(allErrors, err)
}
if len(s.UIDHeaders) > 0 && !caseInsensitiveHas(s.UIDHeaders, "X-Remote-Uid") {
// this was added later and so we are able to error out
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers is set without specifying the standard X-Remote-Uid header - API aggregation will not work"))
}
}

return allErrors
}

Expand Down Expand Up @@ -126,7 +135,7 @@ func (s *RequestHeaderAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
"List of request headers to inspect for usernames. X-Remote-User is common.")

fs.StringSliceVar(&s.UIDHeaders, "requestheader-uid-headers", s.UIDHeaders, ""+
"List of request headers to inspect for UIDs. X-Remote-Uid is suggested.")
"List of request headers to inspect for UIDs. X-Remote-Uid is suggested. Requires the RemoteRequestHeaderUID feature to be enabled.")

fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+
"List of request headers to inspect for groups. X-Remote-Group is suggested.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,12 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
proxyRoundTripper := handlingInfo.proxyRoundTripper
upgrade := httpstream.IsUpgradeRequest(req)

proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra(), proxyRoundTripper)
var userUID string
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.RemoteRequestHeaderUID) {
userUID = user.GetUID()
}

proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), userUID, user.GetGroups(), user.GetExtra(), proxyRoundTripper)

if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerTracing) && !upgrade {
tracingWrapper := tracing.WrapperFor(r.tracerProvider)
Expand All @@ -170,7 +175,7 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// NOT use the proxyRoundTripper. It's a direct dial that bypasses the proxyRoundTripper. This means that we have to
// attach the "correct" user headers to the request ahead of time.
if upgrade {
transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra())
transport.SetAuthProxyHeaders(newReq, user.GetName(), userUID, user.GetGroups(), user.GetExtra())
}

handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ import (
"sync/atomic"
"testing"

"github.com/google/go-cmp/cmp"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/client-go/transport"

Expand All @@ -53,8 +55,11 @@ import (
"k8s.io/apiserver/pkg/endpoints/filters"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/server/egressselector"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy"
"k8s.io/component-base/featuregate"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
Expand Down Expand Up @@ -130,6 +135,8 @@ func TestProxyHandler(t *testing.T) {
expectedBody string
expectedCalled bool
expectedHeaders map[string][]string

enableFeatureGates []featuregate.Feature
}{
"no target": {
expectedStatusCode: http.StatusNotFound,
Expand Down Expand Up @@ -174,6 +181,40 @@ func TestProxyHandler(t *testing.T) {
},
expectedStatusCode: http.StatusOK,
expectedCalled: true,
expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"},
"X-Forwarded-For": {"127.0.0.1"},
"X-Remote-User": {"username"},
"User-Agent": {"Go-http-client/1.1"},
"Accept-Encoding": {"gzip"},
"X-Remote-Group": {"one", "two"},
},
},
"[RemoteRequestHeaderUID] proxy with user, insecure": {
user: &user.DefaultInfo{
Name: "username",
UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78",
Groups: []string{"one", "two"},
},
path: "/request/path",
apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Port: pointer.Int32Ptr(443)},
Group: "foo",
Version: "v1",
InsecureSkipTLSVerify: true,
},
Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{
{Type: apiregistration.Available, Status: apiregistration.ConditionTrue},
},
},
},
enableFeatureGates: []featuregate.Feature{features.RemoteRequestHeaderUID},
expectedStatusCode: http.StatusOK,
expectedCalled: true,
expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"},
Expand Down Expand Up @@ -208,6 +249,40 @@ func TestProxyHandler(t *testing.T) {
},
expectedStatusCode: http.StatusOK,
expectedCalled: true,
expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"},
"X-Forwarded-For": {"127.0.0.1"},
"X-Remote-User": {"username"},
"User-Agent": {"Go-http-client/1.1"},
"Accept-Encoding": {"gzip"},
"X-Remote-Group": {"one", "two"},
},
},
"[RemoteRequestHeaderUID] proxy with user, cabundle": {
user: &user.DefaultInfo{
Name: "username",
UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78",
Groups: []string{"one", "two"},
},
path: "/request/path",
apiService: &apiregistration.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)},
Group: "foo",
Version: "v1",
CABundle: testCACrt,
},
Status: apiregistration.APIServiceStatus{
Conditions: []apiregistration.APIServiceCondition{
{Type: apiregistration.Available, Status: apiregistration.ConditionTrue},
},
},
},
enableFeatureGates: []featuregate.Feature{features.RemoteRequestHeaderUID},
expectedStatusCode: http.StatusOK,
expectedCalled: true,
expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"},
Expand Down Expand Up @@ -320,7 +395,11 @@ func TestProxyHandler(t *testing.T) {
target.Reset()
legacyregistry.Reset()

func() {
t.Run(name, func(t *testing.T) {
for _, f := range tc.enableFeatureGates {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, f, true)
}

targetServer := httptest.NewUnstartedServer(target)
serviceCert := tc.serviceCertOverride
if serviceCert == nil {
Expand Down Expand Up @@ -354,37 +433,37 @@ func TestProxyHandler(t *testing.T) {

resp, err := http.Get(server.URL + tc.path)
if err != nil {
t.Errorf("%s: %v", name, err)
t.Errorf("%v", err)
return
}
if e, a := tc.expectedStatusCode, resp.StatusCode; e != a {
body, _ := httputil.DumpResponse(resp, true)
t.Logf("%s: %v", name, string(body))
t.Errorf("%s: expected %v, got %v", name, e, a)
t.Logf("%v", string(body))
t.Errorf("expected %v, got %v", e, a)
return
}
bytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Errorf("%s: %v", name, err)
t.Errorf("%v", err)
return
}
if !strings.Contains(string(bytes), tc.expectedBody) {
t.Errorf("%s: expected %q, got %q", name, tc.expectedBody, string(bytes))
t.Errorf("expected %q, got %q", tc.expectedBody, string(bytes))
return
}

if e, a := tc.expectedCalled, target.called; e != a {
t.Errorf("%s: expected %v, got %v", name, e, a)
t.Errorf("expected %v, got %v", e, a)
return
}
// this varies every test
delete(target.headers, "X-Forwarded-Host")
if e, a := tc.expectedHeaders, target.headers; !reflect.DeepEqual(e, a) {
t.Errorf("%s: expected %v, got %v", name, e, a)
t.Errorf("expected != got %v", cmp.Diff(e, a))
return
}
if e, a := targetServer.Listener.Addr().String(), target.host; tc.expectedCalled && !reflect.DeepEqual(e, a) {
t.Errorf("%s: expected %v, got %v", name, e, a)
t.Errorf("expected %v, got %v", e, a)
return
}

Expand All @@ -397,7 +476,7 @@ func TestProxyHandler(t *testing.T) {
t.Errorf("expected the x509_missing_san_total to be 1, but it's %d", errorCounter)
}
}
}()
})
}
}

Expand Down

0 comments on commit a051b06

Please sign in to comment.