Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ PoC: Implement user warrants #3156

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Conversation

sttts
Copy link
Member

@sttts sttts commented Aug 22, 2024

Summary

Needs kcp-dev/kubernetes#145.

From Slack:

🧵 long ago @sur presented a problem (if I remember right) in our current auth model of serviceaccounts only being valid locally, and with that the inability to create workspaces through an APIExport virtual workspace. The steps are these:

  1. a user U creates a workspace W2 in logicalcluster L1 through an APIExport virtual workspace (that has a permission claim on Workspaces).
  2. the VW forwards the requests by impersonating its system:masters user with a fake system:kcp:admin serviceaccount (S1) for L1 named system:serviceaccount:default:rest.
  3. the forwarded requests hits kcp and kcp authorizes the workspace creation with that S1 serviceaccount. It uses that identity to authorize the use of workspace types. By giving use permission to everybody, this goes through.
  4. workspace admission sticks S1 as owner on the workspace object, to be used by initialization later.
  5. the workspace scheduler creates a logicalclusters L2 for W2 and gives S1 admin permissions through a clusterrole+binding.
  6. the initializers kick in and access the initializer VW for L2 where W2 has been scheduled to. Their requests are impersonated as S1 by the VW.
  7. these requests hit kcp and the authorizer denies access because S1 is a foreign service account 💥

Now assume a modified system that

  • understands serviceaccounts from other workspaces, but does not give them permissions by default.
  • does not use a fake serviceaccount S1 identity to pass through the APIExport VW requests, but preserves the actual controller user and only attaches a warrant that gives the requires access of the claim.

The steps from above would turn into:

  1. a user U creates a workspace W2 in logicalcluster L1 through an APIExport virtual workspace (that has a permission claim on Workspaces).
  2. the VW forwards the requests by impersonating its system:masters user as U with a warrant for a fake system:kcp:admin serviceaccount (S1) for L1 named system:serviceaccount:default:rest.
  3. the forwarded requests hits kcp and kcp authorizes the workspace creation with that warrant S1 serviceaccount because U is not enough. It uses the U identity to authorize the use of workspace types. By giving use permission to everybody, tThis goes through.
  4. workspace admission sticks U with warrant S1 as owner on the workspace object, to be used by initialization later.
  5. the workspace scheduler creates a logicalclusters L2 for W2 and gives U with warrant S1 admin permissions through a clusterrole+binding.
  6. the initializers kick in and access the initializer VW for L2 where W2 has been scheduled to. Their requests are impersonated as U with warrant S1 by the VW.
  7. these requests hit kcp and the authorizer denies access because S1 is a foreign service account grants access because U with warrant S1 is admin in L2.

Now assume in addition that U is actually a controller serviceaccount S0 in workspace root. Step (7) would 💥 because S0 is foreign for L2. We further modify the system to authenticate serviceaccount as system:kcp:serviceaccount:<logicalcluster>:<ns>:<name> with a warrant for system:serviceaccount:<ns>:<name> restricted to their defining logicalcluster.

Now, U becomes system:kcp:serviceaccount:root:default:controller with warrant system:serviceaccount:default:controller restricted to root. With that (4) becomes

  1. workspace admission sticks system:kcp:serviceaccount:root:default:controller with warrant system:serviceaccount:default:controller restricted to root with warrant S1 as owner on the workspace object, to be used by initialization later.

I.e. the user has two warrants. With that (7) becomes:

  1. these requests hit kcp and the authorizer grants access because system:kcp:serviceaccount:root:default:controller with the TWO warrants including S1 is admin in L2.

Related issue(s)

Fixes #

Release Notes

Pass through original identity of controllers accessing a logical cluster through the APIExport virtual workspace. To get the required permissions, a warrant mechanism is added through user extra fields that attaches secondary user identities purely used for authorization.

@kcp-ci-bot kcp-ci-bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. dco-signoff: yes Indicates the PR's author has signed the DCO. kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Aug 22, 2024
@sttts sttts changed the title ✨ PoC: Implement user warrents ✨ PoC: Implement user warrants Aug 22, 2024
@kcp-ci-bot kcp-ci-bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Aug 22, 2024
@kcp-ci-bot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please ask for approval from sttts. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

docs/content/concepts/authorization.md Outdated Show resolved Hide resolved
docs/content/concepts/authorization.md Outdated Show resolved Hide resolved
docs/content/concepts/authorization.md Outdated Show resolved Hide resolved
@kcp-ci-bot kcp-ci-bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Aug 23, 2024
@sttts sttts force-pushed the sttts-warrants branch 4 times, most recently from 4b188ca to 2ca96c3 Compare August 26, 2024 09:29
@kcp-ci-bot kcp-ci-bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Sep 13, 2024
Comment on lines +86 to +87
logger.V(4).Info("authorization step",
"reason", reason,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this not be auditReasonMsg ? We add this into annotations but remove from logs? Intentional?


// withWarrantsInChainCallingDelegateWithOriginalUser could be named shorter, but then it would
// look innocent which it isn't. It produces one level of the chain with the wrapper called
// for the base and all warrents. The delegate though is called only once with the original user.
Copy link
Contributor

Choose a reason for hiding this comment

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

s/warrents/warrants

Groups: []string{bootstrap.SystemKcpAdminGroup},
// Add a warrant of a fake local service account giving full access
warrant := validation.Warrant{
User: "system:serviceaccount:default:rest",
Copy link
Contributor

Choose a reason for hiding this comment

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

Just of interest, what is somebody creates an account like this?

Copy link
Member Author

Choose a reason for hiding this comment

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

totally possible. Same as today: you can create any kind of client cert faking a service account or oidc user or whatever you like. In other words, control over an authn provider is always critical.

Copy link
Contributor

Choose a reason for hiding this comment

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

But don't think you can impersonate this in any way.

Copy link
Member Author

Choose a reason for hiding this comment

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

Impersonation is a good point. Please double check that this is properly e2e tested. Escalating permissions due to impersonation in a workspace must not be possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

deal. will try to spin some tests scenarios for this manually too.

@mjudeikis
Copy link
Contributor

In general just a questions. looks good,

@@ -44,7 +44,7 @@ const (

// NewMaximalPermissionPolicyAuthorizer returns an authorizer that first checks if the request is for a
// bound resource or not. If the resource is bound it checks the maximal permission policy of the underlying API export.
func NewMaximalPermissionPolicyAuthorizer(kubeInformers, globalKubeInformers kcpkubernetesinformers.SharedInformerFactory, kcpInformers, globalKcpInformers kcpinformers.SharedInformerFactory, delegate authorizer.Authorizer) authorizer.Authorizer {
func NewMaximalPermissionPolicyAuthorizer(kubeInformers, globalKubeInformers kcpkubernetesinformers.SharedInformerFactory, kcpInformers, globalKcpInformers kcpinformers.SharedInformerFactory) func(delegate authorizer.Authorizer) authorizer.Authorizer {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just mental note:

func NewMaximalPermissionPolicyAuthorizer(
	kubeInformers, globalKubeInformers kcpkubernetesinformers.SharedInformerFactory,
	kcpInformers, globalKcpInformers kcpinformers.SharedInformerFactory,
) func(delegate authorizer.Authorizer) authorizer.Authorizer {

easier to read. Took me a second to absorb the signature.

@kcp-ci-bot kcp-ci-bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Dec 2, 2024
impersonatedClient, err := kcpkubernetesclientset.NewForConfig(impersonationConfig)
require.NoError(t, err)

t.Log("As user-2 with system:masters, we should be able to read secrets")
Copy link
Contributor

Choose a reason for hiding this comment

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

As user-1 with impersonation of user-2 with system:masters, we should be able to read secrets ?


t.Logf("Check SelfSubjectReview as user-2 with system:masters")
req := &authenticationv1.SelfSubjectReview{}
resp, err := impersonatedClient.Cluster(wsPath).AuthenticationV1().SelfSubjectReviews().Create(ctx, req, metav1.CreateOptions{})
Copy link
Contributor

Choose a reason for hiding this comment

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

Would impersonation not return system:masters group? Why its not expecting it?

@@ -44,25 +44,27 @@ const (
WorkspaceAccessNotPermittedReason = "workspace access not permitted"
)

func NewWorkspaceContentAuthorizer(localInformers, globalInformers kcpkubernetesinformers.SharedInformerFactory, localLogicalClusterLister, globalLogicalClusterLister corev1alpha1listers.LogicalClusterClusterLister, delegate authorizer.Authorizer) authorizer.Authorizer {
Copy link
Contributor

Choose a reason for hiding this comment

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

Im starting to wonder if we should duplicate this code (2 content authorizer implementations) and make all this warrant code feature-flag gated? But might be a lot of if-else statements, not sure if this worth the effort.
My inner me - says - lets just go.
But OSS me - says - better safe than sorry.


// protect status updates to apiexport and apibinding
systemCRDAuth := authz.NewSystemCRDAuthorizer(maxPermissionPolicyAuth)
systemCRDAuth = authz.NewDecorator("03-systemcrd", systemCRDAuth).AddAuditLogging().AddAnonymization().AddReasonAnnotation()
chain = withWarrantsInChainCallingDelegateWithOriginalUser(authz.NewSystemCRDAuthorizer, chain)
Copy link
Member Author

Choose a reason for hiding this comment

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

@mjudeikis to feature gate it, we could have a noop withWarrantsInChainCallingDelegateWithOriginalUser.

@kcp-ci-bot kcp-ci-bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Dec 11, 2024
@kcp-ci-bot kcp-ci-bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Dec 12, 2024
@kcp-ci-bot kcp-ci-bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Dec 18, 2024
Copy link
Contributor

@mjudeikis mjudeikis left a comment

Choose a reason for hiding this comment

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

maybe service accounts could go into separate pr too?

Comment on lines +217 to +302
Service accounts from foreign workspaces are authenticated as user `system:kcp:serviceaccount:<logicalcluster>:<ns>:<serviceaccount>`
and under that name can be granted access to workspaces and other permissions. The original service account name is
appended as a warrant (see next section) in order to match the same (cluster) role bindings:
Copy link
Contributor

Choose a reason for hiding this comment

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

mental note:
this could be nice test too in e2e stack.

// WithServiceAccountRewrite replaces the user info of the context for service
// accounts by representing them as globally valid kcp service account representation
// with a warrant in the old format (to match kube-like (cluster) role bindings).
func WithServiceAccountRewrite(handler http.Handler) http.Handler {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can service accounts be split too to separate PR? Just enable service account for cross-workspaces?

Copy link
Member Author

Choose a reason for hiding this comment

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

Will try. Have started the rebase now.

case !isUser && !isServiceAccountFromCluster:
// service accounts from other workspaces cannot access
case isServiceAccount && isForeign:
// Service accounts from other workspaces might conflict with local service accounts by name.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still true if we rewrite service account names to be globally unique?

sttts added 11 commits December 21, 2024 16:34
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
…for the target workspace

Signed-off-by: Dr. Stefan Schimanski <[email protected]>
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
@kcp-ci-bot kcp-ci-bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Dec 21, 2024
@kcp-ci-bot
Copy link
Contributor

@sttts: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-kcp-verify d853530 link true /test pull-kcp-verify
pull-kcp-verify-codegen d853530 link true /test pull-kcp-verify-codegen
pull-kcp-test-e2e-shared d853530 link true /test pull-kcp-test-e2e-shared
pull-kcp-lint d853530 link true /test pull-kcp-lint
pull-kcp-test-e2e-sharded d853530 link true /test pull-kcp-test-e2e-sharded
pull-kcp-test-e2e d853530 link true /test pull-kcp-test-e2e
pull-kcp-test-e2e-multiple-runs d853530 link true /test pull-kcp-test-e2e-multiple-runs

Full PR test history

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dco-signoff: yes Indicates the PR's author has signed the DCO. kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants