-
Notifications
You must be signed in to change notification settings - Fork 40k
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
Add configurable groups to bootstrap tokens. #50933
Changes from all commits
33e02af
fd5c00b
77f1b72
c7996a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ import ( | |
"k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/fields" | ||
"k8s.io/apimachinery/pkg/util/sets" | ||
clientset "k8s.io/client-go/kubernetes" | ||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" | ||
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" | ||
|
@@ -87,6 +88,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { | |
"dry-run", dryRun, "Whether to enable dry-run mode or not") | ||
|
||
var usages []string | ||
var extraGroups []string | ||
var tokenDuration time.Duration | ||
var description string | ||
createCmd := &cobra.Command{ | ||
|
@@ -114,14 +116,17 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { | |
fmt.Fprintln(errW, "[kubeadm] WARNING: starting in 1.8, tokens expire after 24 hours by default (if you require a non-expiring token use --ttl 0)") | ||
} | ||
|
||
err = RunCreateToken(out, client, token, tokenDuration, usages, description) | ||
err = RunCreateToken(out, client, token, tokenDuration, usages, extraGroups, description) | ||
kubeadmutil.CheckErr(err) | ||
}, | ||
} | ||
createCmd.Flags().DurationVar(&tokenDuration, | ||
"ttl", kubeadmconstants.DefaultTokenDuration, "The duration before the token is automatically deleted (e.g. 1s, 2m, 3h). 0 means 'never expires'.") | ||
createCmd.Flags().StringSliceVar(&usages, | ||
"usages", kubeadmconstants.DefaultTokenUsages, "The ways in which this token can be used. Valid options: [signing,authentication].") | ||
createCmd.Flags().StringSliceVar(&extraGroups, | ||
"groups", []string{}, | ||
fmt.Sprintf("Extra groups that this token will authenticate as when used for authentication. Must match %q.", bootstrapapi.BootstrapGroupPattern)) | ||
createCmd.Flags().StringVar(&description, | ||
"description", "", "A human friendly description of how this token is used.") | ||
tokenCmd.AddCommand(createCmd) | ||
|
@@ -192,7 +197,7 @@ func NewCmdTokenGenerate(out io.Writer) *cobra.Command { | |
} | ||
|
||
// RunCreateToken generates a new bootstrap token and stores it as a secret on the server. | ||
func RunCreateToken(out io.Writer, client clientset.Interface, token string, tokenDuration time.Duration, usages []string, description string) error { | ||
func RunCreateToken(out io.Writer, client clientset.Interface, token string, tokenDuration time.Duration, usages []string, extraGroups []string, description string) error { | ||
|
||
if len(token) == 0 { | ||
var err error | ||
|
@@ -207,8 +212,22 @@ func RunCreateToken(out io.Writer, client clientset.Interface, token string, tok | |
} | ||
} | ||
|
||
// adding groups only makes sense for authentication | ||
var usagesSet sets.String | ||
usagesSet.Insert(usages...) | ||
if len(extraGroups) > 0 && !usagesSet.Has("authentication") { | ||
return fmt.Errorf("--groups cannot be specified unless --usages includes \"authentication\"") | ||
} | ||
|
||
// validate any extra group names | ||
for _, group := range extraGroups { | ||
if err := bootstrapapi.ValidateBootstrapGroupName(group); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
// TODO: Validate usages here so we don't allow something unsupported | ||
err := tokenphase.CreateNewToken(client, token, tokenDuration, usages, description) | ||
err := tokenphase.CreateNewToken(client, token, tokenDuration, usages, extraGroups, description) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -246,7 +265,7 @@ func RunListTokens(out io.Writer, errW io.Writer, client clientset.Interface) er | |
} | ||
|
||
w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0) | ||
fmt.Fprintln(w, "TOKEN\tTTL\tEXPIRES\tUSAGES\tDESCRIPTION") | ||
fmt.Fprintln(w, "TOKEN\tTTL\tEXPIRES\tUSAGES\tDESCRIPTION\tEXTRA GROUPS") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: column headers shouldn't have whitespace in the titles |
||
for _, secret := range secrets.Items { | ||
tokenId := getSecretString(&secret, bootstrapapi.BootstrapTokenIDKey) | ||
if len(tokenId) == 0 { | ||
|
@@ -304,7 +323,12 @@ func RunListTokens(out io.Writer, errW io.Writer, client clientset.Interface) er | |
if len(description) == 0 { | ||
description = "<none>" | ||
} | ||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", tokenutil.BearerToken(td), ttl, expires, usageString, description) | ||
|
||
groups := getSecretString(&secret, bootstrapapi.BootstrapTokenExtraGroupsKey) | ||
if len(groups) == 0 { | ||
groups = "<none>" | ||
} | ||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", tokenutil.BearerToken(td), ttl, expires, usageString, description, groups) | ||
} | ||
w.Flush() | ||
return nil | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package api | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
) | ||
|
||
var bootstrapGroupRegexp = regexp.MustCompile(`\A` + BootstrapGroupPattern + `\z`) | ||
|
||
// ValidateBootstrapGroupName checks if the provided group name is a valid | ||
// bootstrap group name. Returns nil if valid or a validation error if invalid. | ||
// TODO(mattmoyer): this validation should migrate out to client-go (see https://github.com/kubernetes/client-go/issues/114) | ||
func ValidateBootstrapGroupName(name string) error { | ||
if bootstrapGroupRegexp.Match([]byte(name)) { | ||
return nil | ||
} | ||
return fmt.Errorf("bootstrap group %q is invalid (must match %s)", name, BootstrapGroupPattern) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package api | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestValidateBootstrapGroupName(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
input string | ||
valid bool | ||
}{ | ||
{"valid", "system:bootstrappers:foo", true}, | ||
{"valid nested", "system:bootstrappers:foo:bar:baz", true}, | ||
{"valid with dashes and number", "system:bootstrappers:foo-bar-42", true}, | ||
{"invalid uppercase", "system:bootstrappers:Foo", false}, | ||
{"missing prefix", "foo", false}, | ||
{"prefix with no body", "system:bootstrappers:", false}, | ||
{"invalid spaces", "system:bootstrappers: ", false}, | ||
{"invalid asterisk", "system:bootstrappers:*", false}, | ||
{"trailing colon", "system:bootstrappers:foo:", false}, | ||
{"trailing dash", "system:bootstrappers:foo-", false}, | ||
{"script tags", "system:bootstrappers:<script> alert(\"scary?!\") </script>", false}, | ||
{"too long", "system:bootstrappers:" + strings.Repeat("x", 300), false}, | ||
} | ||
for _, test := range tests { | ||
err := ValidateBootstrapGroupName(test.input) | ||
if err != nil && test.valid { | ||
t.Errorf("test %q: ValidateBootstrapGroupName(%q) returned unexpected error: %v", test.name, test.input, err) | ||
} | ||
if err == nil && !test.valid { | ||
t.Errorf("test %q: ValidateBootstrapGroupName(%q) was supposed to return an error but didn't", test.name, test.input) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,11 @@ const ( | |
// describes what the bootstrap token is used for. Optional. | ||
BootstrapTokenDescriptionKey = "description" | ||
|
||
// BootstrapTokenExtraGroupsKey is a comma-separated list of group names. | ||
// The bootstrap token will authenticate as these groups in addition to the | ||
// "system:bootstrappers" group. | ||
BootstrapTokenExtraGroupsKey = "auth-extra-groups" | ||
|
||
// BootstrapTokenUsagePrefix is the prefix for the other usage constants that specifies different | ||
// functions of a bootstrap token | ||
BootstrapTokenUsagePrefix = "usage-bootstrap-" | ||
|
@@ -63,7 +68,8 @@ const ( | |
// BootstrapTokenUsageAuthentication signals that this token should be used | ||
// as a bearer token to authenticate against the Kubernetes API. The bearer | ||
// token takes the form "<token-id>.<token-secret>" and authenticates as the | ||
// user "system:bootstrap:<token-id>" in the group "system:bootstrappers". | ||
// user "system:bootstrap:<token-id>" in the "system:bootstrappers" group | ||
// as well as any groups specified using BootstrapTokenExtraGroupsKey. | ||
// Value must be "true". Any other value is assumed to be false. Optional. | ||
BootstrapTokenUsageAuthentication = "usage-bootstrap-authentication" | ||
|
||
|
@@ -80,6 +86,12 @@ const ( | |
// authenticate as. The full username given is "system:bootstrap:<token-id>". | ||
BootstrapUserPrefix = "system:bootstrap:" | ||
|
||
// BootstrapGroup is the group bootstrapping bearer tokens authenticate in. | ||
BootstrapGroup = "system:bootstrappers" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you looked up and replaced all occurances of this const? I think there should be some more places... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was all I could find. |
||
// BootstrapGroupPattern is the valid regex pattern that all groups | ||
// assigned to a bootstrap token by BootstrapTokenExtraGroupsKey must match. | ||
// See also ValidateBootstrapGroupName(). | ||
BootstrapGroupPattern = "system:bootstrappers:[a-z0-9:-]{0,255}[a-z0-9]" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: usually for stuff like this we use a DNS-ish type of regexp. That means that any segment shouldn't start with a number or -. It is a bit strange to allow But I don't see any real problem to be honest so it is just a nit. |
||
|
||
// BootstrapDefaultGroup is the default group for bootstrapping bearer | ||
// tokens (in addition to any groups from BootstrapTokenExtraGroupsKey). | ||
BootstrapDefaultGroup = "system:bootstrappers" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default kubeadm token should have a prefix of
system:bootstrappers:kubeadm:default-node
or something.Please add that to constants.go
We should also migrate the RBAC-creation code to create a ClusterRoleBinding depending on version
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you okay doing that in a followup PR? I wanted to make sure we're careful about upgrading the cluster role binding since it's possible that an upgraded cluster would have nodes using tokens created without the new default group.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems like you're using a group to represent an individual token... each token already has its own unique username... why not use that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's a slight difference where you might have a single logical node pool (maybe
system:bootstrappers:high-integrity-nodes
). That group name might be used in a custom CSR autoapprover to assign a particular class of node identity to those nodes.You might rotate through multiple tokens for different nodes within that pool, e.g., a token you used to bootstrap an initial set of nodes, and then another token you use to bootstrap additional nodes later on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is for the default token group, sorry for not making that clear.
ClusterRoleBindings will be bound to this group; and the default token will be added to this group by default.