Skip to content

Commit 46b1c0c

Browse files
committed
prevent workspace authentication from returning system groups or names
On-behalf-of: @SAP [email protected]
1 parent 1c11747 commit 46b1c0c

File tree

8 files changed

+200
-46
lines changed

8 files changed

+200
-46
lines changed

pkg/authentication/authenticators.go

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -26,51 +26,39 @@ import (
2626
"github.com/kcp-dev/kcp/pkg/proxy/lookup"
2727
)
2828

29-
type workspaceAuthenticator struct{}
30-
3129
func NewWorkspaceAuthenticator() authenticator.Request {
32-
return &workspaceAuthenticator{}
33-
}
34-
35-
func (a *workspaceAuthenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
36-
reqAuthenticator, ok := WorkspaceAuthenticatorFrom(req.Context())
37-
if !ok {
38-
return nil, false, nil
39-
}
40-
41-
return reqAuthenticator.AuthenticateRequest(req)
42-
}
30+
return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
31+
reqAuthenticator, ok := WorkspaceAuthenticatorFrom(req.Context())
32+
if !ok {
33+
return nil, false, nil
34+
}
4335

44-
type clusterScoper struct {
45-
delegate authenticator.Request
36+
return reqAuthenticator.AuthenticateRequest(req)
37+
})
4638
}
4739

4840
func withClusterScope(delegate authenticator.Request) authenticator.Request {
49-
return &clusterScoper{
50-
delegate: delegate,
51-
}
52-
}
53-
54-
func (a *clusterScoper) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
55-
response, authenticated, ok := a.delegate.AuthenticateRequest(req)
56-
if authenticated {
57-
cluster := lookup.ClusterNameFrom(req.Context())
58-
59-
extra := response.User.GetExtra()
60-
if extra == nil {
61-
extra = map[string][]string{}
62-
}
63-
if true {
64-
extra["authentication.kcp.io/scopes"] = append(extra["authentication.kcp.io/scopes"], fmt.Sprintf("cluster:%s", cluster))
65-
}
66-
67-
response.User = &user.DefaultInfo{
68-
Name: response.User.GetName(),
69-
UID: response.User.GetUID(),
70-
Groups: response.User.GetGroups(),
71-
Extra: extra,
41+
return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
42+
response, authenticated, ok := delegate.AuthenticateRequest(req)
43+
if authenticated {
44+
cluster := lookup.ClusterNameFrom(req.Context())
45+
46+
extra := response.User.GetExtra()
47+
if extra == nil {
48+
extra = map[string][]string{}
49+
}
50+
if true {
51+
extra["authentication.kcp.io/scopes"] = append(extra["authentication.kcp.io/scopes"], fmt.Sprintf("cluster:%s", cluster))
52+
}
53+
54+
response.User = &user.DefaultInfo{
55+
Name: response.User.GetName(),
56+
UID: response.User.GetUID(),
57+
Groups: response.User.GetGroups(),
58+
Extra: extra,
59+
}
7260
}
73-
}
7461

75-
return response, authenticated, ok
62+
return response, authenticated, ok
63+
})
7664
}
File renamed without changes.
File renamed without changes.

pkg/authentication/index.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,14 @@ func (c *state) Lookup(wsType logicalcluster.Path) (authenticator.Request, bool)
235235
return nil, false
236236
}
237237

238-
return authenticatorunion.New(authenticators...), true
238+
authenticator := authenticatorunion.New(authenticators...)
239+
240+
// ensure that per-workspace auth cannot be used to become a system: user/group
241+
authenticator = ForbidSystemUsernames(authenticator)
242+
filtered := &GroupFilter{
243+
Authenticator: authenticator,
244+
DropGroupPrefixes: []string{"system:"},
245+
}
246+
247+
return filtered, true
239248
}

pkg/authentication/usernames.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
Copyright 2022 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package authentication
18+
19+
import (
20+
"errors"
21+
"net/http"
22+
"strings"
23+
24+
"k8s.io/apiserver/pkg/authentication/authenticator"
25+
)
26+
27+
// ForbidSystemUsernames wraps an authenticator and prevents it from returning
28+
// an internal system username (anything beginning with "system:"). This is so
29+
// per-workspace authenticators cannot impersonate low-level system accounts or
30+
// serviceaccounts.
31+
// This filter should be used together with the GroupsFilter to also strip
32+
// system groups.
33+
func ForbidSystemUsernames(delegate authenticator.Request) authenticator.Request {
34+
return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
35+
result, authenticated, err := delegate.AuthenticateRequest(req)
36+
if err == nil {
37+
if strings.HasPrefix(result.User.GetName(), "system:") {
38+
return nil, false, errors.New("system usernames are not admitted")
39+
}
40+
}
41+
42+
return result, authenticated, err
43+
})
44+
}

pkg/proxy/options/authentication.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import (
3636
kcpkubernetesinformers "github.com/kcp-dev/client-go/informers"
3737
kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes"
3838

39+
kcpauthentication "github.com/kcp-dev/kcp/pkg/authentication"
3940
"github.com/kcp-dev/kcp/pkg/authorization/bootstrap"
40-
kcpauthentication "github.com/kcp-dev/kcp/pkg/proxy/authentication"
4141
)
4242

4343
const resyncPeriod = 10 * time.Hour

test/e2e/authentication/mockoidc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func startMockOIDC(t *testing.T, server kcptestingserver.RunningServer) (*mockoi
8686
return m, ca
8787
}
8888

89-
func mockJWTAuthenticator(t *testing.T, m *mockoidc.MockOIDC, ca *crypto.CA) tenancyv1alpha1.JWTAuthenticator {
89+
func mockJWTAuthenticator(t *testing.T, m *mockoidc.MockOIDC, ca *crypto.CA, userPrefix, groupPrefix string) tenancyv1alpha1.JWTAuthenticator {
9090
cfg := m.Config()
9191

9292
caCert, _, err := ca.Config.GetPEMBytes()
@@ -101,11 +101,11 @@ func mockJWTAuthenticator(t *testing.T, m *mockoidc.MockOIDC, ca *crypto.CA) ten
101101
ClaimMappings: tenancyv1alpha1.ClaimMappings{
102102
Username: tenancyv1alpha1.PrefixedClaimOrExpression{
103103
Claim: "email",
104-
Prefix: ptr.To("oidc:"),
104+
Prefix: ptr.To(userPrefix),
105105
},
106106
Groups: tenancyv1alpha1.PrefixedClaimOrExpression{
107107
Claim: "groups",
108-
Prefix: ptr.To("oidc:"),
108+
Prefix: ptr.To(groupPrefix),
109109
},
110110
},
111111
}

test/e2e/authentication/workspace_test.go

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes"
3535
"github.com/kcp-dev/logicalcluster/v3"
3636

37+
"github.com/kcp-dev/kcp/pkg/authorization/bootstrap"
3738
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
3839
kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster"
3940
kcptesting "github.com/kcp-dev/kcp/sdk/testing"
@@ -205,6 +206,18 @@ func TestWorkspaceOIDC(t *testing.T) {
205206
teamCPath: true,
206207
},
207208
},
209+
{
210+
name: "impersonating system groups is not allowed and those groups will be stripped",
211+
username: "hacker",
212+
213+
groups: []string{bootstrap.SystemKcpAdminGroup, "system:masters"},
214+
mock: mockB,
215+
workspaceAccess: map[logicalcluster.Path]bool{
216+
teamAPath: false,
217+
teamBPath: false,
218+
teamCPath: false,
219+
},
220+
},
208221
}
209222

210223
for _, testcase := range testcases {
@@ -306,6 +319,106 @@ func TestUserScope(t *testing.T) {
306319
require.Equal(t, user.Extra["authentication.kcp.io/scopes"], authenticationv1.ExtraValue{"cluster:" + teamWs.Spec.Cluster})
307320
}
308321

322+
func TestForbiddenSystemAccess(t *testing.T) {
323+
framework.Suite(t, "control-plane")
324+
325+
ctx := context.Background()
326+
327+
// start kcp and setup clients
328+
server := kcptesting.SharedKcpServer(t)
329+
330+
baseWsPath, _ := kcptesting.NewWorkspaceFixture(t, server, logicalcluster.NewPath("root"), kcptesting.WithNamePrefix("oidc-scope"))
331+
332+
kcpConfig := server.BaseConfig(t)
333+
kubeClusterClient, err := kcpkubernetesclientset.NewForConfig(kcpConfig)
334+
require.NoError(t, err)
335+
kcpClusterClient, err := kcpclientset.NewForConfig(kcpConfig)
336+
require.NoError(t, err)
337+
338+
mock, ca := startMockOIDC(t, server)
339+
340+
// create an evil AuthConfig that would not prefix OIDC-provided groups, theoretically allowing
341+
// users to become part of system groups.
342+
// setup a new workspace auth config that uses mockoidc's server
343+
authConfig := &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{
344+
ObjectMeta: metav1.ObjectMeta{
345+
Name: "evil-oidc",
346+
},
347+
Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{
348+
JWT: []tenancyv1alpha1.JWTAuthenticator{
349+
mockJWTAuthenticator(t, mock, ca, "", ""),
350+
},
351+
},
352+
}
353+
354+
t.Logf("Creating WorkspaceAuthenticationConfguration %s...", authConfig.Name)
355+
_, err = kcpClusterClient.Cluster(baseWsPath).TenancyV1alpha1().WorkspaceAuthenticationConfigurations().Create(ctx, authConfig, metav1.CreateOptions{})
356+
require.NoError(t, err)
357+
358+
wsType := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfig.Name)
359+
360+
// create a new workspace with our new type
361+
t.Log("Creating Workspaces...")
362+
teamPath, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-a"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsType)))
363+
364+
// give a dummy user access
365+
grantWorkspaceAccess(t, ctx, kubeClusterClient, teamPath, []rbacv1.Subject{{
366+
Kind: "User",
367+
368+
}})
369+
370+
// wait until the authenticator is ready
371+
token := createOIDCToken(t, mock, "dummy", "[email protected]", nil)
372+
373+
client, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken(token, kcpConfig))
374+
require.NoError(t, err)
375+
376+
t.Log("Waiting for authenticator to be ready...")
377+
require.Eventually(t, func() bool {
378+
_, err := client.Cluster(teamPath).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
379+
380+
return err == nil
381+
}, wait.ForeverTestTimeout, 500*time.Millisecond)
382+
383+
// Now that we know that the authenticator is ready, run the actual tests that ensure we do NOT
384+
// gain access based on our system names / groups.
385+
386+
testcases := []struct {
387+
name string
388+
username string
389+
email string
390+
groups []string
391+
}{
392+
{
393+
name: fmt.Sprintf("%s should not give workspace access", bootstrap.SystemKcpAdminGroup),
394+
username: "al",
395+
396+
groups: []string{bootstrap.SystemKcpAdminGroup},
397+
},
398+
{
399+
name: "shard-admin should not be admitted",
400+
username: "al",
401+
email: "shard-admin",
402+
groups: nil,
403+
},
404+
}
405+
406+
t.Log("Testing tokens...")
407+
for _, testcase := range testcases {
408+
t.Run(testcase.name, func(t *testing.T) {
409+
t.Parallel()
410+
411+
token := createOIDCToken(t, mock, testcase.username, testcase.email, testcase.groups)
412+
413+
client, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken(token, kcpConfig))
414+
require.NoError(t, err)
415+
416+
_, err = client.Cluster(teamPath).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
417+
require.Error(t, err, "user should have no access")
418+
})
419+
}
420+
}
421+
309422
func createWorkspaceAuthentication(t *testing.T, ctx context.Context, client kcpclientset.ClusterInterface, workspace logicalcluster.Path, mock *mockoidc.MockOIDC, ca *crypto.CA) string {
310423
name := fmt.Sprintf("mockoidc-%d", rand.Int())
311424

@@ -316,7 +429,7 @@ func createWorkspaceAuthentication(t *testing.T, ctx context.Context, client kcp
316429
},
317430
Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{
318431
JWT: []tenancyv1alpha1.JWTAuthenticator{
319-
mockJWTAuthenticator(t, mock, ca),
432+
mockJWTAuthenticator(t, mock, ca, "oidc:", "oidc:"),
320433
},
321434
},
322435
}

0 commit comments

Comments
 (0)