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

Run node authorizer in kops-controller #7780

Closed

Conversation

justinsb
Copy link
Member

We create a GRPC interface and use it to serve the kubelet bootstrap
certificate; we consume it directly from nodeup.

Based heavily on the existing AWS node-authorizer implementation.

@k8s-ci-robot k8s-ci-robot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Oct 14, 2019
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: justinsb

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

The pull request process is described 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

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Oct 14, 2019
@justinsb justinsb added this to the 1.17 milestone Oct 14, 2019
@justinsb justinsb force-pushed the authorizer_in_kops_controller branch from 4d941f2 to 7da8906 Compare November 29, 2019 14:55
@justinsb justinsb force-pushed the authorizer_in_kops_controller branch from 7da8906 to 6eac1ff Compare December 22, 2019 17:39
@justinsb justinsb force-pushed the authorizer_in_kops_controller branch 2 times, most recently from 8e42c70 to 9476cbf Compare January 26, 2020 16:13
@johngmyers
Copy link
Member

This should update docs/architecture/kops-controller.md to document the new service provided by kops-controller.

@justinsb justinsb force-pushed the authorizer_in_kops_controller branch 2 times, most recently from 77b7233 to 88c7f96 Compare February 16, 2020 20:25
@justinsb justinsb changed the title WIP: Run node authorizer in kops-controller Run node authorizer in kops-controller Feb 16, 2020
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Feb 16, 2020
@justinsb
Copy link
Member Author

Thanks for the suggestion @johngmyers - added a small section to the architecture doc. As we add rules, we might want to fill in the details of how node validation is performed.

Right now, there is no node validation, and therefore this is very much not enabled by default. Nonetheless, I'm marking not-WIP because I think it is the first step to enabling a lot of the other functionality (a lot of my other WIP PRs).

@justinsb justinsb added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Feb 16, 2020
Copy link
Member

@johngmyers johngmyers left a comment

Choose a reason for hiding this comment

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

In general, I am against adding "trust everything" approvers. They are, in the best case, useless. How can we be confident we have the machinery to do a reasonable validation without some code that can validate something?

func (c *nodeBootstrapClient) Close() error {
if c.connection != nil {
if err := c.connection.Close(); err != nil {
return fmt.Errorf("error closing GRPC connection: %v", err)
Copy link
Member

Choose a reason for hiding this comment

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

Why is the style to use this instead of errors.Wrap()?

Copy link
Member Author

@justinsb justinsb Feb 17, 2020

Choose a reason for hiding this comment

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

Yeah - xerrors is now looking good, so maybe we should do that. I've started using it in new go projects, but I'm not sure whether it's time to put it into bigger existing projects (like kops) or whether we should wait for official support in go.

Path: filepath.Join(pkiDir, "server.key"),
Contents: fi.NewBytesResource(pkiutil.EncodePrivateKeyPEM(serverKey)),
Type: nodetasks.FileType_File,
Mode: s("0600"),
Copy link
Member

Choose a reason for hiding this comment

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

Do you need to specify the file owner?

Copy link
Member

Choose a reason for hiding this comment

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

Looks like it's fixed.

Server string `json:"server"`

// CACertificate is the CA certificate for the GRPC server
CACertificate []byte `json:"caCertificate"`
Copy link
Member

Choose a reason for hiding this comment

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

Consider making this a list of CAs certificates, to permit rotation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually looks like it already does :-)

But renaming to CACertificates to make this more self-documenting.

We should probably try to plumb this through the rest of the system though - we haven't totally ignored it, but I'm reasonably sure there are a few places where we likely drop the ball.

}

func (c *nodeBootstrapClient) CreateKubeletBootstrapToken(ctx context.Context) (pb.Token, error) {
request := &pb.CreateKubeletBootstrapTokenRequest{}
Copy link
Member

Choose a reason for hiding this comment

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

I don't see how the server would be able to authorize anything without some sort of cloudprovider-specific credential in the request.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right - so we have NodeInfo which is empty today, but is where I think we can put this information.

The ideal is TPM, which GCE has, but AFAIK no other clouds do. But pkg/authorizers/aws/aws.go has some good checks based on the AWS identity document, and we can also cross-check against the IP address.

Copy link
Member

Choose a reason for hiding this comment

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

One cloud provider is enough; doesn't matter which. We should at least have the structure for where the provider-specific code goes.


if response.Token == nil || response.Token.BearerToken == "" {
return pb.Token{}, fmt.Errorf("created bootstrap token, but response was empty")
}
Copy link
Member

Choose a reason for hiding this comment

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

I was hoping this would also return a TLS server certificate for the kubelet, but I guess that's another PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

So the process here is sort of tricky - we're creating a bootstrap token and then the kubelet exchanges that for a kubelet server certificate.

I did debate just generating the kubelet server certificate; a few things made me not do it:

  1. It's different; so we'd have to think more carefully about our failure scenarios. Here we're just doing the normal bootstrap flow - arguably a more secure version of it because we can use shorter-lived tokens.
  2. I wasn't sure if kubelet certiificate rotation worked if we didn't start with a bootstrap token.
  3. This way kops-controller doesn't need the CA keypair. (Although we could always do a CSR flow to avoid that)

We can always generate a kubelet certificate directly in future, though - it wouldn't be a huge change!

Copy link
Member

Choose a reason for hiding this comment

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

We could do a CSR flow with a pre-approved request, to allow for external or hardware-key signing controllers.

return nil, err
}

// @step: add the secret to the namespace
Copy link
Member

Choose a reason for hiding this comment

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

Why is this storing the created token in a Secret? I'm concerned a compromised node could impersonate a node in a more privileged instancegroup. Or a pod with permission to read these could escalate to impersonating a node.

Copy link
Member Author

Choose a reason for hiding this comment

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

Answering narrowly, this is how bootstrap tokens work.

Broadly, I do think your concerns are valid. We can make our bootstrap tokens short lived, we can use the NodeAuthorizer to reduce impact, but it's still a concern. The k8s secret also has all the information, whereas presumably it would be safer to store only the hash of the token (requiring an attacker to find a hash collision in a relatively short TTL).

Perhaps then we should move to just generating the kubelet TLS certificate, and trying to figure out what happens during rotation... WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

Short lived doesn't help, as the compromised node could presumably set a watch for kube-system secrets and snatch it almost as soon as it's created.

But yes, this weakness appears to be designed in upstream of us. Just generating the TLS certificate might be simpler.


---

# permits the node access to create a CSR
Copy link
Member

Choose a reason for hiding this comment

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

What is using these three ClusterRoleBindings?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ugh .. good point on the RoleBinding to kops-controller - we should always have those events permissions in kops-controller, at least.

On the ClusterRoleBindings, these are the magic ClusterRoleBindings that (1) allow a kubelet bootstrap token to create a CSR request, and (2) tell kube-controller-manager to auto-approve CSR requests and (3) tell kube-controller-manager to auto approve CSR renewal requests. Some docs are here: https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet-tls-bootstrapping/#approval

If the user/group isn't specified, don't try to change it.
kops-controller now exposes a GRPC service, which is used to more
securely provide nodes their kubelet bootstrap tokens.
@justinsb justinsb force-pushed the authorizer_in_kops_controller branch from 88c7f96 to 37a4960 Compare February 16, 2020 22:25
NodeInfo node_info = 1;
}

// NodeInfo allows a node to provide any secrets etc that can help verify its identity
Copy link
Member

Choose a reason for hiding this comment

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

A better name would be NodeCredential

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 change - thank you!

klog.Warningf("Unhandled type %T in Service::GetDependencies: %v", v, v)
deps = append(deps, v)
switch fmt.Sprintf("%T", v) {
case "*model.KubeletBootstrapKubeconfigTask":
Copy link
Member

Choose a reason for hiding this comment

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

Why not add this type to the outer switch? And why test the type by converting to string?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll add a comment, but it's because of go packages not allowing circular references..

@johngmyers
Copy link
Member

/test pull-kops-verify-staticcheck

@justinsb
Copy link
Member Author

I can certainly work on adding more trust code. We do have the AWS trust code already from the node-authorizer - it's based on the instance identity document. GCE has vTPM which ideally we would use. But we also have some similar logic when we map the instance to a Node.

In general I think we would only turn this on for platforms where we could make a reasonable case

For clouds that will be based on cross-checking with the control plane and verifying some form of cloud-provided identifier or authentication.

For metal I imagine it'll ideally be based on TPM, but will probably also be based on some pre-shared per-host key (maybe even the SSH hostkey, or at least something similar?)

I can certainly work on implementing some of these authorizers e.g. for AWS. I'll do that in a separate PR that builds on this.

Honestly, your concerns about bootstrap tokens are well made and are more concerning in my mind :-) I'm going to look at generating the kubelet certificate directly.

@justinsb
Copy link
Member Author

I had a go at using client-certificates instead of bootstrap tokens and it does work: #8580

I also was able to validate certificate rotation by setting kubeControllerManager: experimentalClusterSigningDuration: 10m0s. For kubelet to rotate its certificates using the CSR flow, we must also pass --rotate-certificates to kubelet. (I added #8581 to map that flag, regardless of whether we need it here) It looks like the CSR request uses the system:certificates.k8s.io:certificatesigningrequests:selfnodeclient, which is much more narrowly scoped than system:certificates.k8s.io:certificatesigningrequests:nodeclient. I think we would not need the system:certificates.k8s.io:certificatesigningrequests:nodeclient binding.

This does raise questions though:

  • Should we use the CSR flow when generating certificates, so we don't need to map the ca.key into kops-controller (Answer: probably, as pointed out by @johngmyers )
  • Do we want to do the kubelet bootstrap & rotation flows at all? Should nodeup just write the kubelet kubeconfig and be done with it? We would presumably need another controller to terminate any nodes that have been up for e.g. 360 days, assuming their cert is going to run out in 365 days. This controller might be a nice thing to have - I know some people deliberately rotate their nodes every week; it seems like a good idea if your workloads can tolerate it.
  • An alternative would be to make nodeup kick-in before the year was up, but I don't know that we need a (semi) persistent nodeup for anything else.

@johngmyers
Copy link
Member

Some people might want to reduce their cert lifetimes to be less than a year. But it is an interesting observation that the node lifetime should probably be less than the cert lifetime.

Any node expiration mechanism should respect the concurrency and cluster validation limitations that rolling update uses. This is the primary motivation for #8272—to coordinate node expiration with rolling updates for other reasons by having the node expiration controller signal to the rolling update controller to remove the nodes.

I would also like to issue kubelet a TLS server certificate. The rotation approver won't approve server certs because it doesn't have the cloud-provider-specific information to validate the domains. kops-controller could reasonably do so.

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Feb 17, 2020
@k8s-ci-robot
Copy link
Contributor

@justinsb: PR needs rebase.

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.

@justinsb justinsb modified the milestones: v1.17, v1.19 Apr 10, 2020
@johngmyers
Copy link
Member

@justinsb can this be closed?

@k8s-ci-robot
Copy link
Contributor

@justinsb: The following tests failed, say /retest to rerun all failed tests:

Test name Commit Details Rerun command
pull-kops-verify-staticcheck 37a4960 link /test pull-kops-verify-staticcheck
pull-kops-e2e-cni-calico 37a4960 link /test pull-kops-e2e-cni-calico
pull-kops-e2e-cni-cilium 37a4960 link /test pull-kops-e2e-cni-cilium
pull-kops-e2e-cni-amazonvpc 37a4960 link /test pull-kops-e2e-cni-amazonvpc
pull-kops-e2e-cni-flannel 37a4960 link /test pull-kops-e2e-cni-flannel
pull-kops-e2e-cni-weave 37a4960 link /test pull-kops-e2e-cni-weave
pull-kops-e2e-cni-kuberouter 37a4960 link /test pull-kops-e2e-cni-kuberouter
pull-kops-verify-cloudformation 37a4960 link /test pull-kops-verify-cloudformation
pull-kops-verify-hashes 37a4960 link /test pull-kops-verify-hashes
pull-kops-e2e-k8s-containerd 37a4960 link /test pull-kops-e2e-k8s-containerd

Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR.

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.

@olemarkus
Copy link
Member

I think we can call a timeout on this one :)

/close

@k8s-ci-robot
Copy link
Contributor

@olemarkus: Closed this PR.

In response to this:

I think we can call a timeout on this one :)

/close

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. 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.

4 participants