mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 12:43:23 +00:00
featuregate UID in RequestHeader authenticator
This commit is contained in:
parent
2b472fe469
commit
a051b067cd
@ -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"
|
||||
@ -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
|
||||
@ -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 {
|
||||
|
@ -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},
|
||||
},
|
||||
|
@ -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
|
||||
@ -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},
|
||||
},
|
||||
|
@ -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"
|
||||
@ -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)
|
||||
}
|
||||
@ -84,10 +83,6 @@ 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")
|
||||
}
|
||||
@ -95,6 +90,20 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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.")
|
||||
|
@ -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)
|
||||
@ -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})
|
||||
|
@ -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"
|
||||
|
||||
@ -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"
|
||||
@ -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,
|
||||
@ -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"},
|
||||
@ -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"},
|
||||
@ -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 {
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user