├── token ├── OWNERS ├── api │ ├── doc.go │ └── types.go ├── jws │ ├── jws_test.go │ └── jws.go └── util │ ├── helpers.go │ └── helpers_test.go ├── code-of-conduct.md ├── .github └── PULL_REQUEST_TEMPLATE.md ├── OWNERS ├── SECURITY_CONTACTS ├── doc.go ├── CONTRIBUTING.md ├── util ├── tokens │ └── tokens.go └── secrets │ ├── secrets.go │ └── secrets_test.go ├── README.md ├── go.mod ├── go.sum └── LICENSE /token/OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - luxas 5 | emeritus_approvers: 6 | - jbeda 7 | reviewers: [] 8 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Sorry, we do not accept changes directly against this repository. Please see 2 | CONTRIBUTING.md for information on where and how to contribute instead. 3 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - sig-cluster-lifecycle-leads 5 | reviewers: 6 | - sig-cluster-lifecycle-leads 7 | labels: 8 | - sig/cluster-lifecycle 9 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | cjcullen 14 | joelsmith 15 | liggitt 16 | philips 17 | tallclair 18 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes 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 clusterbootstrap 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | Do not open pull requests directly against this repository, they will be ignored. Instead, please open pull requests against [kubernetes/kubernetes](https://git.k8s.io/kubernetes/). Please follow the same [contributing guide](https://git.k8s.io/kubernetes/CONTRIBUTING.md) you would follow for any other pull request made to kubernetes/kubernetes. 4 | 5 | This repository is published from [kubernetes/kubernetes/staging/src/k8s.io/cluster-bootstrap](https://git.k8s.io/kubernetes/staging/src/k8s.io/cluster-bootstrap) by the [kubernetes publishing-bot](https://git.k8s.io/publishing-bot). 6 | 7 | Please see [Staging Directory and Publishing](https://git.k8s.io/community/contributors/devel/sig-architecture/staging.md) for more information 8 | -------------------------------------------------------------------------------- /token/api/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Kubernetes 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 api (k8s.io/cluster-bootstrap/token/api) contains constants and types needed for 18 | // bootstrap tokens as maintained by the BootstrapSigner and TokenCleaner 19 | // controllers (in k8s.io/kubernetes/pkg/controller/bootstrap) 20 | package api 21 | -------------------------------------------------------------------------------- /util/tokens/tokens.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 tokens 18 | 19 | import ( 20 | "fmt" 21 | "regexp" 22 | 23 | "k8s.io/cluster-bootstrap/token/api" 24 | ) 25 | 26 | var ( 27 | bootstrapTokenRe = regexp.MustCompile(api.BootstrapTokenPattern) 28 | ) 29 | 30 | // ParseToken tries and parse a valid token from a string. 31 | // A token ID and token secret are returned in case of success, an error otherwise. 32 | func ParseToken(s string) (tokenID, tokenSecret string, err error) { 33 | split := bootstrapTokenRe.FindStringSubmatch(s) 34 | if len(split) != 3 { 35 | return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, api.BootstrapTokenPattern) 36 | } 37 | return split[1], split[2], nil 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ **This is an automatically published [staged repository](https://git.k8s.io/kubernetes/staging#external-repository-staging-area) for Kubernetes**. 2 | > Contributions, including issues and pull requests, should be made to the main Kubernetes repository: [https://github.com/kubernetes/kubernetes](https://github.com/kubernetes/kubernetes). 3 | > This repository is read-only for importing, and not used for direct contributions. 4 | > See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. 5 | 6 | # cluster-bootstrap 7 | 8 | Set of constants and helpers in support of 9 | https://github.com/kubernetes/design-proposals-archive/blob/main/cluster-lifecycle/bootstrap-discovery.md 10 | 11 | 12 | ## Purpose 13 | 14 | Current user is kubeadm, the controller that cleans up the tokens, and the bootstrap authenticator. 15 | 16 | 17 | ## Where does it come from? 18 | 19 | `cluster-bootstrap` is synced from https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/cluster-bootstrap. 20 | Code changes are made in that location, merged into `k8s.io/kubernetes` and later synced here. 21 | 22 | 23 | ## Things you should *NOT* do 24 | 25 | 1. Add API types to this repo. This is for the helpers, not for the types. 26 | 2. Directly modify any files under `token` in this repo. Those are driven from `k8s.io/kubernetes/staging/src/k8s.io/cluster-bootstrap`. 27 | 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // This is a generated file. Do not edit directly. 2 | 3 | module k8s.io/cluster-bootstrap 4 | 5 | go 1.25.0 6 | 7 | godebug default=go1.25 8 | 9 | require ( 10 | github.com/stretchr/testify v1.11.1 11 | gopkg.in/go-jose/go-jose.v2 v2.6.3 12 | k8s.io/api v0.0.0-20251204222646-382014e64b8e 13 | k8s.io/apimachinery v0.0.0-20251204222123-56aa7d5cc8bb 14 | k8s.io/klog/v2 v2.130.1 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 20 | github.com/go-logr/logr v1.4.3 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/kr/text v0.2.0 // indirect 23 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 24 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/x448/float16 v0.8.4 // indirect 27 | go.yaml.in/yaml/v2 v2.4.3 // indirect 28 | golang.org/x/crypto v0.45.0 // indirect 29 | golang.org/x/net v0.47.0 // indirect 30 | golang.org/x/text v0.31.0 // indirect 31 | gopkg.in/inf.v0 v0.9.1 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect 34 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect 35 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect 36 | sigs.k8s.io/randfill v1.0.0 // indirect 37 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /token/jws/jws_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes 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 jws 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | const ( 26 | content = "Hello from the other side. I must have called a thousand times." 27 | secret = "my voice is my passcode" 28 | id = "joshua" 29 | ) 30 | 31 | func TestComputeDetachedSignature(t *testing.T) { 32 | sig, err := ComputeDetachedSignature(content, id, secret) 33 | assert.NoError(t, err, "Error when computing signature: %v", err) 34 | assert.Equal( 35 | t, 36 | "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..VShe2taLd-YTrmWuRkcL_8QTNDHYxQIEBsAYYiIj1_8", 37 | sig, 38 | "Wrong signature. Got: %v", sig) 39 | 40 | // Try with null content 41 | sig, err = ComputeDetachedSignature("", id, secret) 42 | assert.NoError(t, err, "Error when computing signature: %v", err) 43 | assert.Equal( 44 | t, 45 | "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..7Ui1ALizW4jXphVUB7xUqC9vLYLL9RZeOFfVLoB7Tgk", 46 | sig, 47 | "Wrong signature. Got: %v", sig) 48 | 49 | // Try with no secret 50 | sig, err = ComputeDetachedSignature(content, id, "") 51 | assert.NoError(t, err, "Error when computing signature: %v", err) 52 | assert.Equal( 53 | t, 54 | "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..UfkqvDGiIFxrMnFseDj9LYJOLNrvjW8aHhF71mvvAs8", 55 | sig, 56 | "Wrong signature. Got: %v", sig) 57 | } 58 | 59 | func TestDetachedTokenIsValid(t *testing.T) { 60 | // Valid detached JWS token and valid inputs should succeed 61 | sig := "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..VShe2taLd-YTrmWuRkcL_8QTNDHYxQIEBsAYYiIj1_8" 62 | assert.True(t, DetachedTokenIsValid(sig, content, id, secret), 63 | "Content %q and token \"%s:%s\" should equal signature: %q", content, id, secret, sig) 64 | 65 | // Invalid detached JWS token and valid inputs should fail 66 | sig2 := sig + "foo" 67 | assert.False(t, DetachedTokenIsValid(sig2, content, id, secret), 68 | "Content %q and token \"%s:%s\" should not equal signature: %q", content, id, secret, sig) 69 | } 70 | -------------------------------------------------------------------------------- /token/jws/jws.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes 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 jws 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | jose "gopkg.in/go-jose/go-jose.v2" 24 | ) 25 | 26 | // ComputeDetachedSignature takes content and token details and computes a detached 27 | // JWS signature. This is described in Appendix F of RFC 7515. Basically, this 28 | // is a regular JWS with the content part of the signature elided. 29 | func ComputeDetachedSignature(content, tokenID, tokenSecret string) (string, error) { 30 | jwk := &jose.JSONWebKey{ 31 | Key: []byte(tokenSecret), 32 | KeyID: tokenID, 33 | } 34 | 35 | opts := &jose.SignerOptions{ 36 | // Since this is a symmetric key, go-jose doesn't automatically include 37 | // the KeyID as part of the protected header. We have to pass it here 38 | // explicitly. 39 | ExtraHeaders: map[jose.HeaderKey]interface{}{ 40 | "kid": tokenID, 41 | }, 42 | } 43 | 44 | signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: jwk}, opts) 45 | if err != nil { 46 | return "", fmt.Errorf("can't make a HS256 signer from the given token: %v", err) 47 | } 48 | 49 | jws, err := signer.Sign([]byte(content)) 50 | if err != nil { 51 | return "", fmt.Errorf("can't HS256-sign the given token: %v", err) 52 | } 53 | 54 | fullSig, err := jws.CompactSerialize() 55 | if err != nil { 56 | return "", fmt.Errorf("can't serialize the given token: %v", err) 57 | } 58 | return stripContent(fullSig) 59 | } 60 | 61 | // stripContent will remove the content part of a compact JWS 62 | // 63 | // The `go-jose` library doesn't support generating signatures with "detached" 64 | // content. To make up for this we take the full compact signature, break it 65 | // apart and put it back together without the content section. 66 | func stripContent(fullSig string) (string, error) { 67 | parts := strings.Split(fullSig, ".") 68 | if len(parts) != 3 { 69 | return "", fmt.Errorf("compact JWS format must have three parts") 70 | } 71 | 72 | return parts[0] + ".." + parts[2], nil 73 | } 74 | 75 | // DetachedTokenIsValid checks whether a given detached JWS-encoded token matches JWS output of the given content and token 76 | func DetachedTokenIsValid(detachedToken, content, tokenID, tokenSecret string) bool { 77 | newToken, err := ComputeDetachedSignature(content, tokenID, tokenSecret) 78 | if err != nil { 79 | return false 80 | } 81 | return detachedToken == newToken 82 | } 83 | -------------------------------------------------------------------------------- /util/secrets/secrets.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 secrets 18 | 19 | import ( 20 | "regexp" 21 | "strings" 22 | "time" 23 | 24 | v1 "k8s.io/api/core/v1" 25 | "k8s.io/apimachinery/pkg/util/sets" 26 | "k8s.io/cluster-bootstrap/token/api" 27 | legacyutil "k8s.io/cluster-bootstrap/token/util" 28 | "k8s.io/klog/v2" 29 | ) 30 | 31 | var ( 32 | secretNameRe = regexp.MustCompile(`^` + regexp.QuoteMeta(api.BootstrapTokenSecretPrefix) + `([a-z0-9]{6})$`) 33 | ) 34 | 35 | // GetData returns the string value for the given key in the specified Secret 36 | // If there is an error or if the key doesn't exist, an empty string is returned. 37 | func GetData(secret *v1.Secret, key string) string { 38 | if secret.Data == nil { 39 | return "" 40 | } 41 | if val, ok := secret.Data[key]; ok { 42 | return string(val) 43 | } 44 | return "" 45 | } 46 | 47 | // HasExpired will identify whether the secret expires 48 | func HasExpired(secret *v1.Secret, currentTime time.Time) bool { 49 | _, expired := GetExpiration(secret, currentTime) 50 | 51 | return expired 52 | } 53 | 54 | // GetExpiration checks if the secret expires 55 | // isExpired indicates if the secret is already expired. 56 | // timeRemaining indicates how long until it does expire. 57 | // if the secret has no expiration timestamp, returns 0, false. 58 | // if there is an error parsing the secret's expiration timestamp, returns 0, true. 59 | func GetExpiration(secret *v1.Secret, currentTime time.Time) (timeRemaining time.Duration, isExpired bool) { 60 | expiration := GetData(secret, api.BootstrapTokenExpirationKey) 61 | if len(expiration) == 0 { 62 | return 0, false 63 | } 64 | expTime, err := time.Parse(time.RFC3339, expiration) 65 | if err != nil { 66 | klog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.", 67 | expiration, secret.Namespace, secret.Name, err) 68 | return 0, true 69 | } 70 | 71 | timeRemaining = expTime.Sub(currentTime) 72 | if timeRemaining <= 0 { 73 | klog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v", 74 | secret.Namespace, secret.Name, expiration) 75 | return 0, true 76 | } 77 | return timeRemaining, false 78 | } 79 | 80 | // ParseName parses the name of the secret to extract the secret ID. 81 | func ParseName(name string) (secretID string, ok bool) { 82 | r := secretNameRe.FindStringSubmatch(name) 83 | if r == nil { 84 | return "", false 85 | } 86 | return r[1], true 87 | } 88 | 89 | // GetGroups loads and validates the bootstrapapi.BootstrapTokenExtraGroupsKey 90 | // key from the bootstrap token secret, returning a list of group names or an 91 | // error if any of the group names are invalid. 92 | func GetGroups(secret *v1.Secret) ([]string, error) { 93 | // always include the default group 94 | groups := sets.NewString(api.BootstrapDefaultGroup) 95 | 96 | // grab any extra groups and if there are none, return just the default 97 | extraGroupsString := GetData(secret, api.BootstrapTokenExtraGroupsKey) 98 | if extraGroupsString == "" { 99 | return groups.List(), nil 100 | } 101 | 102 | // validate the names of the extra groups 103 | for _, group := range strings.Split(extraGroupsString, ",") { 104 | if err := legacyutil.ValidateBootstrapGroupName(group); err != nil { 105 | return nil, err 106 | } 107 | groups.Insert(group) 108 | } 109 | 110 | // return the result as a deduplicated, sorted list 111 | return groups.List(), nil 112 | } 113 | -------------------------------------------------------------------------------- /util/secrets/secrets_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes 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 secrets 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | bootstrapapi "k8s.io/cluster-bootstrap/token/api" 26 | ) 27 | 28 | func TestGetSecretString(t *testing.T) { 29 | var tests = []struct { 30 | name string 31 | secret *v1.Secret 32 | key string 33 | expectedVal string 34 | }{ 35 | { 36 | name: "existing key", 37 | secret: &v1.Secret{ 38 | ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 39 | Data: map[string][]byte{ 40 | "foo": []byte("bar"), 41 | }, 42 | }, 43 | key: "foo", 44 | expectedVal: "bar", 45 | }, 46 | { 47 | name: "non-existing key", 48 | secret: &v1.Secret{ 49 | ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 50 | Data: map[string][]byte{ 51 | "foo": []byte("bar"), 52 | }, 53 | }, 54 | key: "baz", 55 | expectedVal: "", 56 | }, 57 | { 58 | name: "no data", 59 | secret: &v1.Secret{ 60 | ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 61 | }, 62 | key: "foo", 63 | expectedVal: "", 64 | }, 65 | } 66 | for _, rt := range tests { 67 | t.Run(rt.name, func(t *testing.T) { 68 | actual := GetData(rt.secret, rt.key) 69 | if actual != rt.expectedVal { 70 | t.Errorf( 71 | "failed getSecretString:\n\texpected: %s\n\t actual: %s", 72 | rt.expectedVal, 73 | actual, 74 | ) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func TestParseSecretName(t *testing.T) { 81 | tokenID, ok := ParseName("bootstrap-token-abc123") 82 | if !ok { 83 | t.Error("ParseName should accept valid name") 84 | } 85 | if tokenID != "abc123" { 86 | t.Error("ParseName should return token ID") 87 | } 88 | 89 | _, ok = ParseName("") 90 | if ok { 91 | t.Error("ParseName should reject blank name") 92 | } 93 | 94 | _, ok = ParseName("abc123") 95 | if ok { 96 | t.Error("ParseName should reject with no prefix") 97 | } 98 | 99 | _, ok = ParseName("bootstrap-token-") 100 | if ok { 101 | t.Error("ParseName should reject no token ID") 102 | } 103 | 104 | _, ok = ParseName("bootstrap-token-abc") 105 | if ok { 106 | t.Error("ParseName should reject short token ID") 107 | } 108 | 109 | _, ok = ParseName("bootstrap-token-abc123ghi") 110 | if ok { 111 | t.Error("ParseName should reject long token ID") 112 | } 113 | 114 | _, ok = ParseName("bootstrap-token-ABC123") 115 | if ok { 116 | t.Error("ParseName should reject invalid token ID") 117 | } 118 | } 119 | 120 | func TestGetGroups(t *testing.T) { 121 | tests := []struct { 122 | name string 123 | secret *v1.Secret 124 | expectResult []string 125 | expectError bool 126 | }{ 127 | { 128 | name: "not set", 129 | secret: &v1.Secret{ 130 | ObjectMeta: metav1.ObjectMeta{Name: "test"}, 131 | Data: map[string][]byte{}, 132 | }, 133 | expectResult: []string{"system:bootstrappers"}, 134 | }, 135 | { 136 | name: "set to empty value", 137 | secret: &v1.Secret{ 138 | ObjectMeta: metav1.ObjectMeta{Name: "test"}, 139 | Data: map[string][]byte{ 140 | bootstrapapi.BootstrapTokenExtraGroupsKey: []byte(""), 141 | }, 142 | }, 143 | expectResult: []string{"system:bootstrappers"}, 144 | }, 145 | { 146 | name: "invalid prefix", 147 | secret: &v1.Secret{ 148 | ObjectMeta: metav1.ObjectMeta{Name: "test"}, 149 | Data: map[string][]byte{ 150 | bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("foo"), 151 | }, 152 | }, 153 | expectError: true, 154 | }, 155 | { 156 | name: "valid", 157 | secret: &v1.Secret{ 158 | ObjectMeta: metav1.ObjectMeta{Name: "test"}, 159 | Data: map[string][]byte{ 160 | bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("system:bootstrappers:foo,system:bootstrappers:bar,system:bootstrappers:bar"), 161 | }, 162 | }, 163 | // expect the results in deduplicated, sorted order 164 | expectResult: []string{ 165 | "system:bootstrappers", 166 | "system:bootstrappers:bar", 167 | "system:bootstrappers:foo", 168 | }, 169 | }, 170 | } 171 | for _, test := range tests { 172 | result, err := GetGroups(test.secret) 173 | if test.expectError { 174 | if err == nil { 175 | t.Errorf("test %q expected an error, but didn't get one (result: %#v)", test.name, result) 176 | } 177 | continue 178 | } 179 | if err != nil { 180 | t.Errorf("test %q return an unexpected error: %v", test.name, err) 181 | continue 182 | } 183 | if !reflect.DeepEqual(result, test.expectResult) { 184 | t.Errorf("test %q expected %#v, got %#v", test.name, test.expectResult, result) 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /token/api/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Kubernetes 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 api 18 | 19 | import ( 20 | "k8s.io/api/core/v1" 21 | ) 22 | 23 | const ( 24 | // BootstrapTokenSecretPrefix is the prefix for bootstrap token names. 25 | // Bootstrap tokens secrets must be named in the form 26 | // `bootstrap-token-`. This is the prefix to be used before the 27 | // token ID. 28 | BootstrapTokenSecretPrefix = "bootstrap-token-" 29 | 30 | // SecretTypeBootstrapToken is used during the automated bootstrap process (first 31 | // implemented by kubeadm). It stores tokens that are used to sign well known 32 | // ConfigMaps. They may also eventually be used for authentication. 33 | SecretTypeBootstrapToken v1.SecretType = "bootstrap.kubernetes.io/token" 34 | 35 | // BootstrapTokenIDKey is the id of this token. This can be transmitted in the 36 | // clear and encoded in the name of the secret. It must be a random 6 character 37 | // string that matches the regexp `^([a-z0-9]{6})$`. Required. 38 | BootstrapTokenIDKey = "token-id" 39 | 40 | // BootstrapTokenSecretKey is the actual secret. It must be a random 16 character 41 | // string that matches the regexp `^([a-z0-9]{16})$`. Required. 42 | BootstrapTokenSecretKey = "token-secret" 43 | 44 | // BootstrapTokenExpirationKey is when this token should be expired and no 45 | // longer used. A controller will delete this resource after this time. This 46 | // is an absolute UTC time using RFC3339. If this cannot be parsed, the token 47 | // should be considered invalid. Optional. 48 | BootstrapTokenExpirationKey = "expiration" 49 | 50 | // BootstrapTokenDescriptionKey is a description in human-readable format that 51 | // describes what the bootstrap token is used for. Optional. 52 | BootstrapTokenDescriptionKey = "description" 53 | 54 | // BootstrapTokenExtraGroupsKey is a comma-separated list of group names. 55 | // The bootstrap token will authenticate as these groups in addition to the 56 | // "system:bootstrappers" group. 57 | BootstrapTokenExtraGroupsKey = "auth-extra-groups" 58 | 59 | // BootstrapTokenUsagePrefix is the prefix for the other usage constants that specifies different 60 | // functions of a bootstrap token 61 | BootstrapTokenUsagePrefix = "usage-bootstrap-" 62 | 63 | // BootstrapTokenUsageSigningKey signals that this token should be used to 64 | // sign configs as part of the bootstrap process. Value must be "true". Any 65 | // other value is assumed to be false. Optional. 66 | BootstrapTokenUsageSigningKey = "usage-bootstrap-signing" 67 | 68 | // BootstrapTokenUsageAuthentication signals that this token should be used 69 | // as a bearer token to authenticate against the Kubernetes API. The bearer 70 | // token takes the form "." and authenticates as the 71 | // user "system:bootstrap:" in the "system:bootstrappers" group 72 | // as well as any groups specified using BootstrapTokenExtraGroupsKey. 73 | // Value must be "true". Any other value is assumed to be false. Optional. 74 | BootstrapTokenUsageAuthentication = "usage-bootstrap-authentication" 75 | 76 | // ConfigMapClusterInfo defines the name for the ConfigMap where the information how to connect and trust the cluster exist 77 | ConfigMapClusterInfo = "cluster-info" 78 | 79 | // KubeConfigKey defines at which key in the Data object of the ConfigMap the KubeConfig object is stored 80 | KubeConfigKey = "kubeconfig" 81 | 82 | // JWSSignatureKeyPrefix defines what key prefix the JWS-signed tokens have 83 | JWSSignatureKeyPrefix = "jws-kubeconfig-" 84 | 85 | // BootstrapUserPrefix is the username prefix bootstrapping bearer tokens 86 | // authenticate as. The full username given is "system:bootstrap:". 87 | BootstrapUserPrefix = "system:bootstrap:" 88 | 89 | // BootstrapDefaultGroup is the default group for bootstrapping bearer 90 | // tokens (in addition to any groups from BootstrapTokenExtraGroupsKey). 91 | BootstrapDefaultGroup = "system:bootstrappers" 92 | 93 | // BootstrapGroupPattern is the valid regex pattern that all groups 94 | // assigned to a bootstrap token by BootstrapTokenExtraGroupsKey must match. 95 | // See also util.ValidateBootstrapGroupName() 96 | BootstrapGroupPattern = `\Asystem:bootstrappers:[a-z0-9:-]{0,255}[a-z0-9]\z` 97 | 98 | // BootstrapTokenPattern defines the {id}.{secret} regular expression pattern 99 | BootstrapTokenPattern = `\A([a-z0-9]{6})\.([a-z0-9]{16})\z` 100 | 101 | // BootstrapTokenIDPattern defines token's id regular expression pattern 102 | BootstrapTokenIDPattern = `\A([a-z0-9]{6})\z` 103 | 104 | // BootstrapTokenIDBytes defines the number of bytes used for the Bootstrap Token's ID field 105 | BootstrapTokenIDBytes = 6 106 | 107 | // BootstrapTokenSecretBytes defines the number of bytes used the Bootstrap Token's Secret field 108 | BootstrapTokenSecretBytes = 16 109 | ) 110 | 111 | // KnownTokenUsages specifies the known functions a token will get. 112 | var KnownTokenUsages = []string{"signing", "authentication"} 113 | -------------------------------------------------------------------------------- /token/util/helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Kubernetes 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 util 18 | 19 | import ( 20 | "crypto/rand" 21 | "fmt" 22 | "math/big" 23 | "regexp" 24 | "strings" 25 | 26 | "k8s.io/apimachinery/pkg/util/sets" 27 | "k8s.io/cluster-bootstrap/token/api" 28 | ) 29 | 30 | // TODO(dixudx): refactor this to util/secrets and util/tokens 31 | 32 | // validBootstrapTokenChars defines the characters a bootstrap token can consist of 33 | const validBootstrapTokenChars = "0123456789abcdefghijklmnopqrstuvwxyz" 34 | 35 | var ( 36 | // BootstrapTokenRegexp is a compiled regular expression of TokenRegexpString 37 | BootstrapTokenRegexp = regexp.MustCompile(api.BootstrapTokenPattern) 38 | // BootstrapTokenIDRegexp is a compiled regular expression of TokenIDRegexpString 39 | BootstrapTokenIDRegexp = regexp.MustCompile(api.BootstrapTokenIDPattern) 40 | // BootstrapGroupRegexp is a compiled regular expression of BootstrapGroupPattern 41 | BootstrapGroupRegexp = regexp.MustCompile(api.BootstrapGroupPattern) 42 | ) 43 | 44 | // GenerateBootstrapToken generates a new, random Bootstrap Token. 45 | func GenerateBootstrapToken() (string, error) { 46 | tokenID, err := randBytes(api.BootstrapTokenIDBytes) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | tokenSecret, err := randBytes(api.BootstrapTokenSecretBytes) 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | return TokenFromIDAndSecret(tokenID, tokenSecret), nil 57 | } 58 | 59 | // randBytes returns a random string consisting of the characters in 60 | // validBootstrapTokenChars, with the length customized by the parameter 61 | func randBytes(length int) (string, error) { 62 | var ( 63 | token = make([]byte, length) 64 | max = new(big.Int).SetUint64(uint64(len(validBootstrapTokenChars))) 65 | ) 66 | 67 | for i := range token { 68 | val, err := rand.Int(rand.Reader, max) 69 | if err != nil { 70 | return "", fmt.Errorf("could not generate random integer: %w", err) 71 | } 72 | // Use simple operations in constant-time to obtain a byte in the a-z,0-9 73 | // character range 74 | x := val.Uint64() 75 | res := x + 48 + (39 & ((9 - x) >> 8)) 76 | token[i] = byte(res) 77 | } 78 | 79 | return string(token), nil 80 | } 81 | 82 | // TokenFromIDAndSecret returns the full token which is of the form "{id}.{secret}" 83 | func TokenFromIDAndSecret(id, secret string) string { 84 | return fmt.Sprintf("%s.%s", id, secret) 85 | } 86 | 87 | // IsValidBootstrapToken returns whether the given string is valid as a Bootstrap Token. 88 | // Avoid using BootstrapTokenRegexp.MatchString(token) and instead perform constant-time 89 | // comparisons on the secret. 90 | func IsValidBootstrapToken(token string) bool { 91 | // Must be exactly two strings separated by "." 92 | t := strings.Split(token, ".") 93 | if len(t) != 2 { 94 | return false 95 | } 96 | 97 | // Validate the ID: t[0] 98 | // Using a Regexp for it is safe because the ID is public already 99 | if !BootstrapTokenIDRegexp.MatchString(t[0]) { 100 | return false 101 | } 102 | 103 | // Validate the secret with constant-time: t[1] 104 | secret := t[1] 105 | if len(secret) != api.BootstrapTokenSecretBytes { // Must be an exact size 106 | return false 107 | } 108 | for i := range secret { 109 | c := int(secret[i]) 110 | notDigit := (c < 48 || c > 57) // Character is not in the 0-9 range 111 | notLetter := (c < 97 || c > 122) // Character is not in the a-z range 112 | if notDigit && notLetter { 113 | return false 114 | } 115 | } 116 | return true 117 | } 118 | 119 | // IsValidBootstrapTokenID returns whether the given string is valid as a Bootstrap Token ID and 120 | // in other words satisfies the BootstrapTokenIDRegexp 121 | func IsValidBootstrapTokenID(tokenID string) bool { 122 | return BootstrapTokenIDRegexp.MatchString(tokenID) 123 | } 124 | 125 | // BootstrapTokenSecretName returns the expected name for the Secret storing the 126 | // Bootstrap Token in the Kubernetes API. 127 | func BootstrapTokenSecretName(tokenID string) string { 128 | return fmt.Sprintf("%s%s", api.BootstrapTokenSecretPrefix, tokenID) 129 | } 130 | 131 | // ValidateBootstrapGroupName checks if the provided group name is a valid 132 | // bootstrap group name. Returns nil if valid or a validation error if invalid. 133 | // TODO(dixudx): should be moved to util/secrets 134 | func ValidateBootstrapGroupName(name string) error { 135 | if BootstrapGroupRegexp.Match([]byte(name)) { 136 | return nil 137 | } 138 | return fmt.Errorf("bootstrap group %q is invalid (must match %s)", name, api.BootstrapGroupPattern) 139 | } 140 | 141 | // ValidateUsages validates that the passed in string are valid usage strings for bootstrap tokens. 142 | func ValidateUsages(usages []string) error { 143 | validUsages := sets.NewString(api.KnownTokenUsages...) 144 | invalidUsages := sets.NewString() 145 | for _, usage := range usages { 146 | if !validUsages.Has(usage) { 147 | invalidUsages.Insert(usage) 148 | } 149 | } 150 | if len(invalidUsages) > 0 { 151 | return fmt.Errorf("invalid bootstrap token usage string: %s, valid usage options: %s", strings.Join(invalidUsages.List(), ","), strings.Join(api.KnownTokenUsages, ",")) 152 | } 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 6 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 7 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 8 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 9 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 10 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 11 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 12 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 13 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 14 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 15 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 16 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 17 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 18 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 20 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 21 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 22 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 23 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 27 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 28 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 29 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 30 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 31 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 32 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 33 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 34 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 35 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 36 | go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 37 | go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 38 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 39 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 40 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 41 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 42 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 43 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 46 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 47 | gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= 48 | gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= 49 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 50 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 51 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 52 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | k8s.io/api v0.0.0-20251204222646-382014e64b8e h1:pfnI6xGQHUXwuFKumPQjgzDYkBa5RKWJbWfF89vbrS4= 54 | k8s.io/api v0.0.0-20251204222646-382014e64b8e/go.mod h1:OjSG925L2Qod5yd066wDJ6HLaA8lNCJRx5eAtCw5rOM= 55 | k8s.io/apimachinery v0.0.0-20251204222123-56aa7d5cc8bb h1:yb4KUdenOGnO2tWDaMQFR74DE6bnjtSNCE3+InuoO6g= 56 | k8s.io/apimachinery v0.0.0-20251204222123-56aa7d5cc8bb/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= 57 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 58 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 59 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= 60 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 61 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= 62 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 63 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= 64 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 65 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 66 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 67 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= 68 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 69 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 70 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 71 | -------------------------------------------------------------------------------- /token/util/helpers_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Kubernetes 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 util 18 | 19 | import ( 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestGenerateBootstrapToken(t *testing.T) { 25 | token, err := GenerateBootstrapToken() 26 | if err != nil { 27 | t.Fatalf("GenerateBootstrapToken returned an unexpected error: %+v", err) 28 | } 29 | if !IsValidBootstrapToken(token) { 30 | t.Errorf("GenerateBootstrapToken didn't generate a valid token: %q", token) 31 | } 32 | } 33 | 34 | func TestRandBytes(t *testing.T) { 35 | var randTest = []int{ 36 | 0, 37 | 1, 38 | 2, 39 | 3, 40 | 100, 41 | } 42 | 43 | for _, rt := range randTest { 44 | actual, err := randBytes(rt) 45 | if err != nil { 46 | t.Errorf("failed randBytes: %v", err) 47 | } 48 | if len(actual) != rt { 49 | t.Errorf("failed randBytes:\n\texpected: %d\n\t actual: %d\n", rt, len(actual)) 50 | } 51 | } 52 | } 53 | 54 | func TestTokenFromIDAndSecret(t *testing.T) { 55 | var tests = []struct { 56 | id string 57 | secret string 58 | expected string 59 | }{ 60 | {"foo", "bar", "foo.bar"}, // should use default 61 | {"abcdef", "abcdef0123456789", "abcdef.abcdef0123456789"}, 62 | {"h", "b", "h.b"}, 63 | } 64 | for _, rt := range tests { 65 | actual := TokenFromIDAndSecret(rt.id, rt.secret) 66 | if actual != rt.expected { 67 | t.Errorf( 68 | "failed TokenFromIDAndSecret:\n\texpected: %s\n\t actual: %s", 69 | rt.expected, 70 | actual, 71 | ) 72 | } 73 | } 74 | } 75 | 76 | func TestIsValidBootstrapToken(t *testing.T) { 77 | var tests = []struct { 78 | token string 79 | expected bool 80 | }{ 81 | {token: "", expected: false}, 82 | {token: ".", expected: false}, 83 | {token: "1234567890123456789012", expected: false}, // invalid parcel size 84 | {token: "12345.1234567890123456", expected: false}, // invalid parcel size 85 | {token: ".1234567890123456", expected: false}, // invalid parcel size 86 | {token: "123456.", expected: false}, // invalid parcel size 87 | {token: "123456:1234567890.123456", expected: false}, // invalid separation 88 | {token: "abcdef:1234567890123456", expected: false}, // invalid separation 89 | {token: "Abcdef.1234567890123456", expected: false}, // invalid token id 90 | {token: "123456.AABBCCDDEEFFGGHH", expected: false}, // invalid token secret 91 | {token: "123456.AABBCCD-EEFFGGHH", expected: false}, // invalid character 92 | {token: "abc*ef.1234567890123456", expected: false}, // invalid character 93 | {token: "abcdef.1234567890123456", expected: true}, 94 | {token: "123456.aabbccddeeffgghh", expected: true}, 95 | {token: "ABCDEF.abcdef0123456789", expected: false}, 96 | {token: "abcdef.abcdef0123456789", expected: true}, 97 | {token: "123456.1234560123456789", expected: true}, 98 | } 99 | for _, rt := range tests { 100 | actual := IsValidBootstrapToken(rt.token) 101 | if actual != rt.expected { 102 | t.Errorf( 103 | "failed IsValidBootstrapToken for the token %q\n\texpected: %t\n\t actual: %t", 104 | rt.token, 105 | rt.expected, 106 | actual, 107 | ) 108 | } 109 | } 110 | } 111 | 112 | func TestIsValidBootstrapTokenID(t *testing.T) { 113 | var tests = []struct { 114 | tokenID string 115 | expected bool 116 | }{ 117 | {tokenID: "", expected: false}, 118 | {tokenID: "1234567890123456789012", expected: false}, 119 | {tokenID: "12345", expected: false}, 120 | {tokenID: "Abcdef", expected: false}, 121 | {tokenID: "ABCDEF", expected: false}, 122 | {tokenID: "abcdef.", expected: false}, 123 | {tokenID: "abcdef", expected: true}, 124 | {tokenID: "123456", expected: true}, 125 | } 126 | for _, rt := range tests { 127 | actual := IsValidBootstrapTokenID(rt.tokenID) 128 | if actual != rt.expected { 129 | t.Errorf( 130 | "failed IsValidBootstrapTokenID for the token %q\n\texpected: %t\n\t actual: %t", 131 | rt.tokenID, 132 | rt.expected, 133 | actual, 134 | ) 135 | } 136 | } 137 | } 138 | 139 | func TestBootstrapTokenSecretName(t *testing.T) { 140 | var tests = []struct { 141 | tokenID string 142 | expected string 143 | }{ 144 | {"foo", "bootstrap-token-foo"}, 145 | {"bar", "bootstrap-token-bar"}, 146 | {"", "bootstrap-token-"}, 147 | {"abcdef", "bootstrap-token-abcdef"}, 148 | } 149 | for _, rt := range tests { 150 | actual := BootstrapTokenSecretName(rt.tokenID) 151 | if actual != rt.expected { 152 | t.Errorf( 153 | "failed BootstrapTokenSecretName:\n\texpected: %s\n\t actual: %s", 154 | rt.expected, 155 | actual, 156 | ) 157 | } 158 | } 159 | } 160 | 161 | func TestValidateBootstrapGroupName(t *testing.T) { 162 | tests := []struct { 163 | name string 164 | input string 165 | valid bool 166 | }{ 167 | {"valid", "system:bootstrappers:foo", true}, 168 | {"valid nested", "system:bootstrappers:foo:bar:baz", true}, 169 | {"valid with dashes and number", "system:bootstrappers:foo-bar-42", true}, 170 | {"invalid uppercase", "system:bootstrappers:Foo", false}, 171 | {"missing prefix", "foo", false}, 172 | {"prefix with no body", "system:bootstrappers:", false}, 173 | {"invalid spaces", "system:bootstrappers: ", false}, 174 | {"invalid asterisk", "system:bootstrappers:*", false}, 175 | {"trailing colon", "system:bootstrappers:foo:", false}, 176 | {"trailing dash", "system:bootstrappers:foo-", false}, 177 | {"script tags", "system:bootstrappers:", false}, 178 | {"too long", "system:bootstrappers:" + strings.Repeat("x", 300), false}, 179 | } 180 | for _, test := range tests { 181 | err := ValidateBootstrapGroupName(test.input) 182 | if err != nil && test.valid { 183 | t.Errorf("test %q: ValidateBootstrapGroupName(%q) returned unexpected error: %v", test.name, test.input, err) 184 | } 185 | if err == nil && !test.valid { 186 | t.Errorf("test %q: ValidateBootstrapGroupName(%q) was supposed to return an error but didn't", test.name, test.input) 187 | } 188 | } 189 | } 190 | 191 | func TestValidateUsages(t *testing.T) { 192 | tests := []struct { 193 | name string 194 | input []string 195 | valid bool 196 | }{ 197 | {"valid of signing", []string{"signing"}, true}, 198 | {"valid of authentication", []string{"authentication"}, true}, 199 | {"all valid", []string{"authentication", "signing"}, true}, 200 | {"single invalid", []string{"authentication", "foo"}, false}, 201 | {"all invalid", []string{"foo", "bar"}, false}, 202 | } 203 | 204 | for _, test := range tests { 205 | err := ValidateUsages(test.input) 206 | if err != nil && test.valid { 207 | t.Errorf("test %q: ValidateUsages(%v) returned unexpected error: %v", test.name, test.input, err) 208 | } 209 | if err == nil && !test.valid { 210 | t.Errorf("test %q: ValidateUsages(%v) was supposed to return an error but didn't", test.name, test.input) 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------