diff --git a/cmd/k3s/main.go b/cmd/k3s/main.go index f6db6e37aa..cbe996703a 100644 --- a/cmd/k3s/main.go +++ b/cmd/k3s/main.go @@ -37,6 +37,7 @@ func main() { return } + tokenCommand := internalCLIAction(version.Program+"-"+cmds.TokenCommand, dataDir, os.Args) etcdsnapshotCommand := internalCLIAction(version.Program+"-"+cmds.EtcdSnapshotCommand, dataDir, os.Args) secretsencryptCommand := internalCLIAction(version.Program+"-"+cmds.SecretsEncryptCommand, dataDir, os.Args) certCommand := internalCLIAction(version.Program+"-"+cmds.CertCommand, dataDir, os.Args) @@ -51,6 +52,12 @@ func main() { cmds.NewCRICTL(externalCLIAction("crictl", dataDir)), cmds.NewCtrCommand(externalCLIAction("ctr", dataDir)), cmds.NewCheckConfigCommand(externalCLIAction("check-config", dataDir)), + cmds.NewTokenCommands( + tokenCommand, + tokenCommand, + tokenCommand, + tokenCommand, + ), cmds.NewEtcdSnapshotCommands( etcdsnapshotCommand, etcdsnapshotCommand, diff --git a/cmd/server/main.go b/cmd/server/main.go index 0f0ca4a3c2..bf82d9c305 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -17,6 +17,7 @@ import ( "github.com/k3s-io/k3s/pkg/cli/kubectl" "github.com/k3s-io/k3s/pkg/cli/secretsencrypt" "github.com/k3s-io/k3s/pkg/cli/server" + "github.com/k3s-io/k3s/pkg/cli/token" "github.com/k3s-io/k3s/pkg/configfilearg" "github.com/k3s-io/k3s/pkg/containerd" ctr2 "github.com/k3s-io/k3s/pkg/ctr" @@ -48,6 +49,12 @@ func main() { cmds.NewKubectlCommand(kubectl.Run), cmds.NewCRICTL(crictl.Run), cmds.NewCtrCommand(ctr.Run), + cmds.NewTokenCommands( + token.Create, + token.Delete, + token.Generate, + token.List, + ), cmds.NewEtcdSnapshotCommands( etcdsnapshot.Run, etcdsnapshot.Delete, diff --git a/cmd/token/main.go b/cmd/token/main.go new file mode 100644 index 0000000000..26d069fc92 --- /dev/null +++ b/cmd/token/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "errors" + "os" + + "github.com/k3s-io/k3s/pkg/cli/cmds" + "github.com/k3s-io/k3s/pkg/cli/token" + "github.com/k3s-io/k3s/pkg/configfilearg" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +func main() { + app := cmds.NewApp() + app.Commands = []cli.Command{ + cmds.NewTokenCommands( + token.Create, + token.Delete, + token.Generate, + token.List, + ), + } + + if err := app.Run(configfilearg.MustParse(os.Args)); err != nil && !errors.Is(err, context.Canceled) { + logrus.Fatal(err) + } +} diff --git a/go.mod b/go.mod index ec071d673f..ac16da7943 100644 --- a/go.mod +++ b/go.mod @@ -152,6 +152,7 @@ require ( k8s.io/apiserver v0.26.1 k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible k8s.io/cloud-provider v0.26.1 + k8s.io/cluster-bootstrap v0.0.0 k8s.io/component-base v0.26.1 k8s.io/component-helpers v0.26.1 k8s.io/cri-api v0.26.1 @@ -396,7 +397,6 @@ require ( honnef.co/go/tools v0.2.2 // indirect k8s.io/apiextensions-apiserver v0.25.4 // indirect k8s.io/cli-runtime v0.22.2 // indirect - k8s.io/cluster-bootstrap v0.0.0 // indirect k8s.io/code-generator v0.25.4 // indirect k8s.io/controller-manager v0.25.4 // indirect k8s.io/csi-translation-lib v0.0.0 // indirect diff --git a/pkg/cli/cmds/token.go b/pkg/cli/cmds/token.go new file mode 100644 index 0000000000..2e3cfe574b --- /dev/null +++ b/pkg/cli/cmds/token.go @@ -0,0 +1,97 @@ +package cmds + +import ( + "time" + + "github.com/urfave/cli" +) + +const TokenCommand = "token" + +// Config holds CLI values for the token subcommands +type Token struct { + Description string + Kubeconfig string + Token string + Output string + Groups cli.StringSlice + Usages cli.StringSlice + TTL time.Duration +} + +var ( + TokenConfig = Token{} + TokenFlags = []cli.Flag{ + DataDirFlag, + cli.StringFlag{ + Name: "kubeconfig", + Usage: "(cluster) Server to connect to", + EnvVar: "KUBECONFIG", + Destination: &TokenConfig.Kubeconfig, + }, + } +) + +func NewTokenCommands(create, delete, generate, list func(ctx *cli.Context) error) cli.Command { + return cli.Command{ + Name: TokenCommand, + Usage: "Manage bootstrap tokens", + SkipFlagParsing: false, + SkipArgReorder: true, + Subcommands: []cli.Command{ + { + Name: "create", + Usage: "Create bootstrap tokens on the server", + Flags: append(TokenFlags, &cli.StringFlag{ + Name: "description", + Usage: "A human friendly description of how this token is used", + Destination: &TokenConfig.Description, + }, &cli.StringSliceFlag{ + Name: "groups", + Usage: "Extra groups that this token will authenticate as when used for authentication", + Value: &TokenConfig.Groups, + }, &cli.DurationFlag{ + Name: "ttl", + Usage: "The duration before the token is automatically deleted (e.g. 1s, 2m, 3h). If set to '0', the token will never expire", + Value: time.Hour * 24, + Destination: &TokenConfig.TTL, + }, &cli.StringSliceFlag{ + Name: "usages", + Usage: "Describes the ways in which this token can be used.", + Value: &TokenConfig.Usages, + }), + SkipFlagParsing: false, + SkipArgReorder: true, + Action: create, + }, + { + Name: "delete", + Usage: "Delete bootstrap tokens on the server", + Flags: TokenFlags, + SkipFlagParsing: false, + SkipArgReorder: true, + Action: delete, + }, + { + Name: "generate", + Usage: "Generate and print a bootstrap token, but do not create it on the server", + Flags: TokenFlags, + SkipFlagParsing: false, + SkipArgReorder: true, + Action: generate, + }, + { + Name: "list", + Usage: "List bootstrap tokens on the server", + Flags: append(TokenFlags, &cli.StringFlag{ + Name: "output,o", + Value: "text", + Destination: &TokenConfig.Output, + }), + SkipFlagParsing: false, + SkipArgReorder: true, + Action: list, + }, + }, + } +} diff --git a/pkg/cli/token/token.go b/pkg/cli/token/token.go new file mode 100644 index 0000000000..44e7eb99ba --- /dev/null +++ b/pkg/cli/token/token.go @@ -0,0 +1,224 @@ +package token + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "text/tabwriter" + "time" + + "github.com/k3s-io/k3s/pkg/cli/cmds" + "github.com/k3s-io/k3s/pkg/clientaccess" + "github.com/k3s-io/k3s/pkg/kubeadm" + "github.com/k3s-io/k3s/pkg/util" + "github.com/pkg/errors" + "github.com/urfave/cli" + "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/client-go/tools/clientcmd" + bootstrapapi "k8s.io/cluster-bootstrap/token/api" + bootstraputil "k8s.io/cluster-bootstrap/token/util" +) + +func Create(app *cli.Context) error { + if err := cmds.InitLogging(); err != nil { + return err + } + return create(app, &cmds.TokenConfig) +} + +func create(app *cli.Context, cfg *cmds.Token) error { + if err := kubeadm.SetDefaults(app, cfg); err != nil { + return err + } + + cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig) + client, err := util.GetClientSet(cfg.Kubeconfig) + if err != nil { + return err + } + + restConfig, err := clientcmd.BuildConfigFromFlags("", cfg.Kubeconfig) + if err != nil { + return err + } + + if len(restConfig.TLSClientConfig.CAData) == 0 && restConfig.TLSClientConfig.CAFile != "" { + restConfig.TLSClientConfig.CAData, err = os.ReadFile(restConfig.TLSClientConfig.CAFile) + if err != nil { + return err + } + } + + bts, err := kubeadm.NewBootstrapTokenString(cfg.Token) + if err != nil { + return err + } + + bt := kubeadm.BootstrapToken{ + Token: bts, + Description: cfg.Description, + TTL: &metav1.Duration{Duration: cfg.TTL}, + Usages: cfg.Usages, + Groups: cfg.Groups, + } + + secretName := bootstraputil.BootstrapTokenSecretName(bt.Token.ID) + if secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(context.TODO(), secretName, metav1.GetOptions{}); secret != nil && err == nil { + return fmt.Errorf("a token with id %q already exists", bt.Token.ID) + } + + secret := kubeadm.BootstrapTokenToSecret(&bt) + if _, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil { + return err + } + + token, err := clientaccess.FormatTokenBytes(bt.Token.String(), restConfig.TLSClientConfig.CAData) + if err != nil { + return err + } + + fmt.Println(token) + return nil +} + +func Delete(app *cli.Context) error { + if err := cmds.InitLogging(); err != nil { + return err + } + return delete(app, &cmds.TokenConfig) +} + +func delete(app *cli.Context, cfg *cmds.Token) error { + args := app.Args() + if len(args) < 1 { + return errors.New("missing argument; 'token delete' is missing token") + } + + cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig) + client, err := util.GetClientSet(cfg.Kubeconfig) + if err != nil { + return err + } + + for _, token := range args { + if !bootstraputil.IsValidBootstrapTokenID(token) { + bts, err := kubeadm.NewBootstrapTokenString(cfg.Token) + if err != nil { + return fmt.Errorf("given token didn't match pattern %q or %q", bootstrapapi.BootstrapTokenIDPattern, bootstrapapi.BootstrapTokenIDPattern) + } + token = bts.ID + } + secretName := bootstraputil.BootstrapTokenSecretName(token) + if err := client.CoreV1().Secrets(metav1.NamespaceSystem).Delete(context.TODO(), secretName, metav1.DeleteOptions{}); err != nil { + return errors.Wrapf(err, "failed to delete bootstrap token %q", err) + } + + fmt.Printf("bootstrap token %q deleted\n", token) + } + return nil +} + +func Generate(app *cli.Context) error { + if err := cmds.InitLogging(); err != nil { + return err + } + return generate(app, &cmds.TokenConfig) +} + +func generate(app *cli.Context, cfg *cmds.Token) error { + token, err := bootstraputil.GenerateBootstrapToken() + if err != nil { + return err + } + fmt.Println(token) + return nil +} + +func List(app *cli.Context) error { + if err := cmds.InitLogging(); err != nil { + return err + } + return list(app, &cmds.TokenConfig) +} + +func list(app *cli.Context, cfg *cmds.Token) error { + if err := kubeadm.SetDefaults(app, cfg); err != nil { + return err + } + + cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig) + client, err := util.GetClientSet(cfg.Kubeconfig) + if err != nil { + return err + } + + tokenSelector := fields.SelectorFromSet( + map[string]string{ + "type": string(bootstrapapi.SecretTypeBootstrapToken), + }, + ) + listOptions := metav1.ListOptions{ + FieldSelector: tokenSelector.String(), + } + + secrets, err := client.CoreV1().Secrets(metav1.NamespaceSystem).List(context.TODO(), listOptions) + if err != nil { + return errors.Wrapf(err, "failed to list bootstrap tokens") + } + + tokens := make([]*kubeadm.BootstrapToken, len(secrets.Items)) + for i, secret := range secrets.Items { + token, err := kubeadm.BootstrapTokenFromSecret(&secret) + if err != nil { + fmt.Printf("%v", err) + continue + } + tokens[i] = token + } + + switch cfg.Output { + case "json": + if err := json.NewEncoder(os.Stdout).Encode(tokens); err != nil { + return err + } + return nil + case "yaml": + if err := yaml.NewEncoder(os.Stdout).Encode(tokens); err != nil { + return err + } + return nil + default: + format := "%s\t%s\t%s\t%s\t%s\t%s\n" + w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0) + defer w.Flush() + + fmt.Fprintf(w, format, "TOKEN", "TTL", "EXPIRES", "USAGES", "DESCRIPTION", "EXTRA GROUPS") + for _, token := range tokens { + ttl := "" + expires := "" + if token.Expires != nil { + ttl = duration.ShortHumanDuration(token.Expires.Sub(time.Now())) + expires = token.Expires.Format(time.RFC3339) + } + + fmt.Fprintf(w, format, token.Token.ID, ttl, expires, joinOrNone(token.Usages...), joinOrNone(token.Description), joinOrNone(token.Groups...)) + } + } + + return nil +} + +// joinOrNone joins strings with a comma. If the resulting output is an empty string, +// it instead returns the replacement string "" +func joinOrNone(s ...string) string { + j := strings.Join(s, ",") + if j == "" { + return "" + } + return j +} diff --git a/pkg/clientaccess/token.go b/pkg/clientaccess/token.go index b0031b5143..a6090deaa2 100644 --- a/pkg/clientaccess/token.go +++ b/pkg/clientaccess/token.go @@ -369,7 +369,7 @@ func put(u string, body []byte, client *http.Client, username, password string) return nil } -// FormatToken takes a username:password string, and a path to a certificate bundle, and +// FormatToken takes a username:password string or join token, and a path to a certificate bundle, and // returns a string containing the full K10 format token string. If the credentials are // empty, an empty token is returned. If the certificate bundle does not exist or does not // contain a valid bundle, an error is returned. @@ -382,6 +382,15 @@ func FormatToken(creds, certFile string) (string, error) { if err != nil { return "", err } + return FormatTokenBytes(creds, b) +} + +// FormatTokenBytes has the same interface as FormatToken, but accepts a byte slice instead +// of file path. +func FormatTokenBytes(creds string, b []byte) (string, error) { + if len(creds) == 0 { + return "", nil + } digest, err := hashCA(b) if err != nil { diff --git a/pkg/daemons/control/server.go b/pkg/daemons/control/server.go index a4e6b52d39..c7685070d0 100644 --- a/pkg/daemons/control/server.go +++ b/pkg/daemons/control/server.go @@ -90,6 +90,7 @@ func Server(ctx context.Context, cfg *config.Control) error { func controllerManager(ctx context.Context, cfg *config.Control) error { runtime := cfg.Runtime argsMap := map[string]string{ + "controllers": "*,tokencleaner", "feature-gates": "JobTrackingWithFinalizers=true", "kubeconfig": runtime.KubeConfigController, "authorization-kubeconfig": runtime.KubeConfigController, @@ -117,7 +118,7 @@ func controllerManager(ctx context.Context, cfg *config.Control) error { } if !cfg.DisableCCM { argsMap["configure-cloud-routes"] = "false" - argsMap["controllers"] = "*,-service,-route,-cloud-node-lifecycle" + argsMap["controllers"] = argsMap["controllers"] + ",-service,-route,-cloud-node-lifecycle" } args := config.GetArgs(argsMap, cfg.ExtraControllerArgs) @@ -158,6 +159,7 @@ func apiServer(ctx context.Context, cfg *config.Control) error { argsMap["cert-dir"] = certDir argsMap["allow-privileged"] = "true" + argsMap["enable-bootstrap-token-auth"] = "true" argsMap["authorization-mode"] = strings.Join([]string{modes.ModeNode, modes.ModeRBAC}, ",") argsMap["service-account-signing-key-file"] = runtime.ServiceCurrentKey argsMap["service-cluster-ip-range"] = util.JoinIPNets(cfg.ServiceIPRanges) diff --git a/pkg/kubeadm/token.go b/pkg/kubeadm/token.go new file mode 100644 index 0000000000..b176eb9b94 --- /dev/null +++ b/pkg/kubeadm/token.go @@ -0,0 +1,52 @@ +package kubeadm + +import ( + "github.com/k3s-io/k3s/pkg/cli/cmds" + "github.com/k3s-io/k3s/pkg/version" + "github.com/pkg/errors" + "github.com/urfave/cli" + bootstrapapi "k8s.io/cluster-bootstrap/token/api" + bootstraputil "k8s.io/cluster-bootstrap/token/util" +) + +var ( + NodeBootstrapTokenAuthGroup = "system:bootstrappers:" + version.Program + ":default-node-token" +) + +// SetDefaults ensures that the default values are set on the token configuration. +// These are set here, rather than in the default Token struct, to avoid +// importing the cluster-bootstrap packages into the CLI. +func SetDefaults(clx *cli.Context, cfg *cmds.Token) error { + if !clx.IsSet("groups") { + cfg.Groups = []string{NodeBootstrapTokenAuthGroup} + } + + if !clx.IsSet("usages") { + cfg.Usages = bootstrapapi.KnownTokenUsages + } + + if cfg.Output == "" { + cfg.Output = "text" + } else { + switch cfg.Output { + case "text", "json", "yaml": + default: + return errors.New("invalid output format: " + cfg.Output) + } + } + + args := clx.Args() + if len(args) > 0 { + cfg.Token = args[0] + } + + if cfg.Token == "" { + var err error + cfg.Token, err = bootstraputil.GenerateBootstrapToken() + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/kubeadm/types.go b/pkg/kubeadm/types.go new file mode 100644 index 0000000000..444e80778d --- /dev/null +++ b/pkg/kubeadm/types.go @@ -0,0 +1,45 @@ +package kubeadm + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// kubeadm bootstrap token types cribbed from: +// https://github.com/kubernetes/kubernetes/blob/v1.25.4/cmd/kubeadm/app/apis/bootstraptoken/v1/types.go +// Copying these instead of importing from kubeadm saves about 4mb of binary size. + +// BootstrapToken describes one bootstrap token, stored as a Secret in the cluster +type BootstrapToken struct { + // Token is used for establishing bidirectional trust between nodes and control-planes. + // Used for joining nodes in the cluster. + Token *BootstrapTokenString `json:"token" datapolicy:"token"` + // Description sets a human-friendly message why this token exists and what it's used + // for, so other administrators can know its purpose. + // +optional + Description string `json:"description,omitempty"` + // TTL defines the time to live for this token. Defaults to 24h. + // Expires and TTL are mutually exclusive. + // +optional + TTL *metav1.Duration `json:"ttl,omitempty"` + // Expires specifies the timestamp when this token expires. Defaults to being set + // dynamically at runtime based on the TTL. Expires and TTL are mutually exclusive. + // +optional + Expires *metav1.Time `json:"expires,omitempty"` + // Usages describes the ways in which this token can be used. Can by default be used + // for establishing bidirectional trust, but that can be changed here. + // +optional + Usages []string `json:"usages,omitempty"` + // Groups specifies the extra groups that this token will authenticate as when/if + // used for authentication + // +optional + Groups []string `json:"groups,omitempty"` +} + +// BootstrapTokenString is a token of the format abcdef.abcdef0123456789 that is used +// for both validation of the identity of the API server from a joining node's point +// of view and as an authentication method for the node. This token is and should be +// short-lived. +type BootstrapTokenString struct { + ID string `json:"-"` + Secret string `json:"-" datapolicy:"token"` +} diff --git a/pkg/kubeadm/utils.go b/pkg/kubeadm/utils.go new file mode 100644 index 0000000000..0fec03e87d --- /dev/null +++ b/pkg/kubeadm/utils.go @@ -0,0 +1,173 @@ +package kubeadm + +import ( + "sort" + "strings" + "time" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + bootstrapapi "k8s.io/cluster-bootstrap/token/api" + bootstraputil "k8s.io/cluster-bootstrap/token/util" + bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets" +) + +// kubeadm bootstrap token utilities cribbed from: +// https://github.com/kubernetes/kubernetes/blob/v1.25.4/cmd/kubeadm/app/apis/bootstraptoken/v1/utils.go +// Copying these instead of importing from kubeadm saves about 4mb of binary size. + +// String returns the string representation of the BootstrapTokenString +func (bts BootstrapTokenString) String() string { + if len(bts.ID) > 0 && len(bts.Secret) > 0 { + return bootstraputil.TokenFromIDAndSecret(bts.ID, bts.Secret) + } + return "" +} + +// NewBootstrapTokenString converts the given Bootstrap Token as a string +// to the BootstrapTokenString object used for serialization/deserialization +// and internal usage. It also automatically validates that the given token +// is of the right format +func NewBootstrapTokenString(token string) (*BootstrapTokenString, error) { + substrs := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch(token) + if len(substrs) != 3 { + return nil, errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern) + } + + return &BootstrapTokenString{ID: substrs[1], Secret: substrs[2]}, nil +} + +// NewBootstrapTokenStringFromIDAndSecret is a wrapper around NewBootstrapTokenString +// that allows the caller to specify the ID and Secret separately +func NewBootstrapTokenStringFromIDAndSecret(id, secret string) (*BootstrapTokenString, error) { + return NewBootstrapTokenString(bootstraputil.TokenFromIDAndSecret(id, secret)) +} + +// BootstrapTokenToSecret converts the given BootstrapToken object to its Secret representation that +// may be submitted to the API Server in order to be stored. +func BootstrapTokenToSecret(bt *BootstrapToken) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: bootstraputil.BootstrapTokenSecretName(bt.Token.ID), + Namespace: metav1.NamespaceSystem, + }, + Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken), + Data: encodeTokenSecretData(bt, time.Now()), + } +} + +// encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret +// now is passed in order to be able to used in unit testing +func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]byte { + data := map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(token.Token.ID), + bootstrapapi.BootstrapTokenSecretKey: []byte(token.Token.Secret), + } + + if len(token.Description) > 0 { + data[bootstrapapi.BootstrapTokenDescriptionKey] = []byte(token.Description) + } + + // If for some strange reason both token.TTL and token.Expires would be set + // (they are mutually exclusive in validation so this shouldn't be the case), + // token.Expires has higher priority, as can be seen in the logic here. + if token.Expires != nil { + // Format the expiration date accordingly + // TODO: This maybe should be a helper function in bootstraputil? + expirationString := token.Expires.Time.UTC().Format(time.RFC3339) + data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) + + } else if token.TTL != nil && token.TTL.Duration > 0 { + // Only if .Expires is unset, TTL might have an effect + // Get the current time, add the specified duration, and format it accordingly + expirationString := now.Add(token.TTL.Duration).UTC().Format(time.RFC3339) + data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) + } + + for _, usage := range token.Usages { + data[bootstrapapi.BootstrapTokenUsagePrefix+usage] = []byte("true") + } + + if len(token.Groups) > 0 { + data[bootstrapapi.BootstrapTokenExtraGroupsKey] = []byte(strings.Join(token.Groups, ",")) + } + return data +} + +// BootstrapTokenFromSecret returns a BootstrapToken object from the given Secret +func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { + // Get the Token ID field from the Secret data + tokenID := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey) + if len(tokenID) == 0 { + return nil, errors.Errorf("bootstrap Token Secret has no token-id data: %s", secret.Name) + } + + // Enforce the right naming convention + if secret.Name != bootstraputil.BootstrapTokenSecretName(tokenID) { + return nil, errors.Errorf("bootstrap token name is not of the form '%s(token-id)'. Actual: %q. Expected: %q", + bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID)) + } + + tokenSecret := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey) + if len(tokenSecret) == 0 { + return nil, errors.Errorf("bootstrap Token Secret has no token-secret data: %s", secret.Name) + } + + // Create the BootstrapTokenString object based on the ID and Secret + bts, err := NewBootstrapTokenStringFromIDAndSecret(tokenID, tokenSecret) + if err != nil { + return nil, errors.Wrap(err, "bootstrap Token Secret is invalid and couldn't be parsed") + } + + // Get the description (if any) from the Secret + description := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenDescriptionKey) + + // Expiration time is optional, if not specified this implies the token + // never expires. + secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey) + var expires *metav1.Time + if len(secretExpiration) > 0 { + expTime, err := time.Parse(time.RFC3339, secretExpiration) + if err != nil { + return nil, errors.Wrapf(err, "can't parse expiration time of bootstrap token %q", secret.Name) + } + expires = &metav1.Time{Time: expTime} + } + + // Build an usages string slice from the Secret data + var usages []string + for k, v := range secret.Data { + // Skip all fields that don't include this prefix + if !strings.HasPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix) { + continue + } + // Skip those that don't have this usage set to true + if string(v) != "true" { + continue + } + usages = append(usages, strings.TrimPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix)) + } + // Only sort the slice if defined + if usages != nil { + sort.Strings(usages) + } + + // Get the extra groups information from the Secret + // It's done this way to make .Groups be nil in case there is no items, rather than an + // empty slice or an empty slice with a "" string only + var groups []string + groupsString := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExtraGroupsKey) + g := strings.Split(groupsString, ",") + if len(g) > 0 && len(g[0]) > 0 { + groups = g + } + + return &BootstrapToken{ + Token: bts, + Description: description, + Expires: expires, + Usages: usages, + Groups: groups, + }, nil +} diff --git a/pkg/server/router.go b/pkg/server/router.go index b1105de399..160575908d 100644 --- a/pkg/server/router.go +++ b/pkg/server/router.go @@ -32,6 +32,7 @@ import ( "k8s.io/apimachinery/pkg/util/json" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + bootstrapapi "k8s.io/cluster-bootstrap/token/api" ) const ( @@ -44,7 +45,7 @@ func router(ctx context.Context, config *Config, cfg *cmds.Server) http.Handler prefix := "/v1-" + version.Program authed := mux.NewRouter().SkipClean(true) - authed.Use(authMiddleware(serverConfig, version.Program+":agent")) + authed.Use(authMiddleware(serverConfig, version.Program+":agent", user.NodesGroup, bootstrapapi.BootstrapDefaultGroup)) authed.Path(prefix + "/serving-kubelet.crt").Handler(servingKubeletCert(serverConfig, serverConfig.Runtime.ServingKubeletKey, nodeAuth)) authed.Path(prefix + "/client-kubelet.crt").Handler(clientKubeletCert(serverConfig, serverConfig.Runtime.ClientKubeletKey, nodeAuth)) authed.Path(prefix + "/client-kube-proxy.crt").Handler(fileHandler(serverConfig.Runtime.ClientKubeProxyCert, serverConfig.Runtime.ClientKubeProxyKey)) diff --git a/scripts/build b/scripts/build index 71d8a00647..fb538350d0 100755 --- a/scripts/build +++ b/scripts/build @@ -92,6 +92,7 @@ fi rm -f \ bin/k3s-agent \ bin/k3s-server \ + bin/k3s-token \ bin/k3s-etcd-snapshot \ bin/k3s-secrets-encrypt \ bin/k3s-certificate \ @@ -127,6 +128,7 @@ echo Building k3s CGO_ENABLED=1 "${GO}" build -tags "$TAGS" -gcflags="all=${GCFLAGS}" -ldflags "$VERSIONFLAGS $LDFLAGS $STATIC" -o bin/k3s ./cmd/server/main.go ln -s k3s ./bin/k3s-agent ln -s k3s ./bin/k3s-server +ln -s k3s ./bin/k3s-token ln -s k3s ./bin/k3s-etcd-snapshot ln -s k3s ./bin/k3s-secrets-encrypt ln -s k3s ./bin/k3s-certificate diff --git a/scripts/package-cli b/scripts/package-cli index 4dc2c80db1..2c71e851d1 100755 --- a/scripts/package-cli +++ b/scripts/package-cli @@ -7,7 +7,7 @@ cd $(dirname $0)/.. GO=${GO-go} -for i in crictl kubectl k3s-agent k3s-server k3s-etcd-snapshot k3s-secrets-encrypt k3s-certificate k3s-completion; do +for i in crictl kubectl k3s-agent k3s-server k3s-token k3s-etcd-snapshot k3s-secrets-encrypt k3s-certificate k3s-completion; do rm -f bin/$i ln -s k3s bin/$i done