Skip to content

Commit

Permalink
Merge pull request #5425 from twz123/use-kubeadm-bootstrap-token
Browse files Browse the repository at this point in the history
Use kubeadm's bootstrap token types
  • Loading branch information
twz123 authored Jan 14, 2025
2 parents d2c78a3 + be96a3a commit 2e0eefe
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 169 deletions.
61 changes: 33 additions & 28 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package api

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
Expand All @@ -26,6 +27,7 @@ import (
"os"
"path"
"path/filepath"
"slices"
"strings"
"time"

Expand All @@ -37,8 +39,11 @@ import (
"github.com/k0sproject/k0s/pkg/etcd"
kubeutil "github.com/k0sproject/k0s/pkg/kubernetes"

apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
tokenutil "k8s.io/cluster-bootstrap/token/util"
bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -92,12 +97,12 @@ func (c *command) start() (err error) {
// Only mount the etcd handler if we're running on internal etcd storage
// by default the mux will return 404 back which the caller should handle
mux.Handle(prefix+"/etcd/members", mw.AllowMethods(http.MethodPost)(
c.authMiddleware(c.etcdHandler(), "usage-controller-join")))
c.authMiddleware(c.etcdHandler(), "controller-join")))
}

if storage.IsJoinable() {
mux.Handle(prefix+"/ca", mw.AllowMethods(http.MethodGet)(
c.authMiddleware(c.caHandler(), "usage-controller-join")))
c.authMiddleware(c.caHandler(), "controller-join")))
}

srv := &http.Server{
Expand Down Expand Up @@ -218,54 +223,54 @@ func (c *command) caHandler() http.Handler {
// We need to validate:
// - that we find a secret with the ID
// - that the token matches whats inside the secret
func (c *command) isValidToken(ctx context.Context, token string, usage string) bool {
parts := strings.Split(token, ".")
logrus.Debugf("token parts: %v", parts)
if len(parts) != 2 {
func (c *command) isValidToken(ctx context.Context, rawTokenString string, usage string) bool {
tokenString, err := bootstraptokenv1.NewBootstrapTokenString(rawTokenString)
if err != nil {
return false
}

secretName := "bootstrap-token-" + parts[0]
secretName := tokenutil.BootstrapTokenSecretName(tokenString.ID)
secret, err := c.client.CoreV1().Secrets("kube-system").Get(ctx, secretName, metav1.GetOptions{})
if err != nil {
logrus.Errorf("failed to get bootstrap token: %s", err.Error())
if !apierrors.IsNotFound(err) {
logrus.WithError(err).Error("Failed to get bootstrap token with ID ", tokenString.ID)
}
return false
}

if string(secret.Data["token-secret"]) != parts[1] {
token, err := bootstraptokenv1.BootstrapTokenFromSecret(secret)
if err != nil {
logrus.WithError(err).Errorf("Bootstrap token with ID %s is malformed", tokenString.ID)
return false
}

usageValue, ok := secret.Data[usage]
if !ok || string(usageValue) != "true" {
if token.Expires != nil && !time.Now().Before(token.Expires.Time) {
return false
}

return true
if *token.Token != *tokenString {
return false
}

switch {
case slices.Contains(token.Usages, usage):
return true // usage found
case bytes.Equal(secret.Data["usage-"+usage], []byte("true")):
return true // usage found in its legacy form
default:
return false // usage not found
}
}

func (c *command) authMiddleware(next http.Handler, usage string) http.Handler {
unauthorizedErr := errors.New("go away")

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth == "" {
sendError(unauthorizedErr, w, http.StatusUnauthorized)
return
}

parts := strings.Split(auth, "Bearer ")
if len(parts) == 2 {
token := parts[1]
if !c.isValidToken(r.Context(), token, usage) {
sendError(unauthorizedErr, w, http.StatusUnauthorized)
return
}
token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
if ok && c.isValidToken(r.Context(), token, usage) {
next.ServeHTTP(w, r)
} else {
sendError(unauthorizedErr, w, http.StatusUnauthorized)
return
}

next.ServeHTTP(w, r)
})
}
6 changes: 4 additions & 2 deletions cmd/token/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func tokenListCmd() *cobra.Command {
return err
}

tokens, err := manager.List(cmd.Context(), listTokenRole)
tokens, err := manager.List(cmd.Context())
if err != nil {
return err
}
Expand All @@ -70,7 +70,9 @@ func tokenListCmd() *cobra.Command {
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true)
for _, t := range tokens {
table.Append(t.ToArray())
if listTokenRole == "" || listTokenRole == t.Role {
table.Append(t.ToArray())
}
}

table.Render()
Expand Down
13 changes: 7 additions & 6 deletions cmd/token/preshared.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/client-go/kubernetes/scheme"
bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1"

"github.com/k0sproject/k0s/internal/pkg/file"
"github.com/k0sproject/k0s/pkg/config"
Expand Down Expand Up @@ -90,10 +91,10 @@ func preSharedCmd() *cobra.Command {
return cmd
}

func createSecret(role string, validity time.Duration, outDir string) (string, error) {
func createSecret(role string, validity time.Duration, outDir string) (*bootstraptokenv1.BootstrapTokenString, error) {
secret, token, err := token.RandomBootstrapSecret(role, validity)
if err != nil {
return "", fmt.Errorf("failed to generate bootstrap secret: %w", err)
return nil, fmt.Errorf("failed to generate bootstrap secret: %w", err)
}

if err := file.WriteAtomically(filepath.Join(outDir, secret.Name+".yaml"), 0640, func(unbuffered io.Writer) error {
Expand All @@ -105,13 +106,13 @@ func createSecret(role string, validity time.Duration, outDir string) (string, e
}
return w.Flush()
}); err != nil {
return "", fmt.Errorf("failed to save bootstrap secret: %w", err)
return nil, fmt.Errorf("failed to save bootstrap secret: %w", err)
}

return token, nil
}

func createKubeConfig(tokenString, role, joinURL, certPath, outDir string) error {
func createKubeConfig(tok *bootstraptokenv1.BootstrapTokenString, role, joinURL, certPath, outDir string) error {
caCert, err := os.ReadFile(certPath)
if err != nil {
return fmt.Errorf("error reading certificate: %w", err)
Expand All @@ -126,7 +127,7 @@ func createKubeConfig(tokenString, role, joinURL, certPath, outDir string) error
default:
return fmt.Errorf("unknown role: %s", role)
}
kubeconfig, err := token.GenerateKubeconfig(joinURL, caCert, userName, tokenString)
kubeconfig, err := token.GenerateKubeconfig(joinURL, caCert, userName, tok)
if err != nil {
return fmt.Errorf("error generating kubeconfig: %w", err)
}
Expand All @@ -136,7 +137,7 @@ func createKubeConfig(tokenString, role, joinURL, certPath, outDir string) error
return fmt.Errorf("error encoding token: %w", err)
}

err = file.WriteContentAtomically(filepath.Join(outDir, "token_"+tokenString), []byte(encodedToken), 0640)
err = file.WriteContentAtomically(filepath.Join(outDir, "token_"+tok.ID), []byte(encodedToken), 0640)
if err != nil {
return fmt.Errorf("error writing kubeconfig: %w", err)
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ require (
k8s.io/cli-runtime v0.31.3
k8s.io/client-go v0.31.3
k8s.io/cloud-provider v0.31.3
k8s.io/cluster-bootstrap v0.31.3
k8s.io/component-base v0.31.3
k8s.io/component-helpers v0.31.3
k8s.io/cri-api v0.31.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,8 @@ k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4=
k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs=
k8s.io/cloud-provider v0.31.3 h1:7C3CHQUUwnv/HWWVIaibZH06iPg663RYQ6C6Zy4FnO8=
k8s.io/cloud-provider v0.31.3/go.mod h1:c7csKppoVb9Ej6upJ28AvHy4B3BtlRMzXfgezsDdPKw=
k8s.io/cluster-bootstrap v0.31.3 h1:O1Yxk1bLaxZvmQCXLaJjj5iJD+lVMfJdRUuKgbUHPlA=
k8s.io/cluster-bootstrap v0.31.3/go.mod h1:TI6TCsQQB4FfcryWgNO3SLXSKWBqHjx4DfyqSFwixj8=
k8s.io/component-base v0.31.3 h1:DMCXXVx546Rfvhj+3cOm2EUxhS+EyztH423j+8sOwhQ=
k8s.io/component-base v0.31.3/go.mod h1:xME6BHfUOafRgT0rGVBGl7TuSg8Z9/deT7qq6w7qjIU=
k8s.io/component-helpers v0.31.3 h1:0zGPD2PrekhFWgmz85XxlMEl7dfhlKC1tERZDe3onQc=
Expand Down
36 changes: 0 additions & 36 deletions internal/autopilot/pkg/random/random.go

This file was deleted.

36 changes: 0 additions & 36 deletions internal/pkg/random/random.go

This file was deleted.

12 changes: 7 additions & 5 deletions pkg/token/joinclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1"
"github.com/k0sproject/k0s/pkg/token"

bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1"

"github.com/cloudflare/cfssl/csr"
"github.com/cloudflare/cfssl/initca"
"github.com/stretchr/testify/assert"
Expand All @@ -42,13 +44,13 @@ func TestJoinClient_GetCA(t *testing.T) {

joinURL, certData := startFakeJoinServer(t, func(res http.ResponseWriter, req *http.Request) {
assert.Equal(t, "/some/sub/path/v1beta1/ca", req.RequestURI)
assert.Equal(t, []string{"Bearer the-token"}, req.Header["Authorization"])
assert.Equal(t, []string{"Bearer the-id.the-secret"}, req.Header["Authorization"])
_, err := res.Write([]byte("{}"))
assert.NoError(t, err)
})

joinURL.Path = "/some/sub/path"
kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, t.Name(), "the-token")
kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, t.Name(), &bootstraptokenv1.BootstrapTokenString{ID: "the-id", Secret: "the-secret"})
require.NoError(t, err)
tok, err := token.JoinEncode(bytes.NewReader(kubeconfig))
require.NoError(t, err)
Expand All @@ -66,7 +68,7 @@ func TestJoinClient_JoinEtcd(t *testing.T) {

joinURL, certData := startFakeJoinServer(t, func(res http.ResponseWriter, req *http.Request) {
assert.Equal(t, "/some/sub/path/v1beta1/etcd/members", req.RequestURI)
assert.Equal(t, []string{"Bearer the-token"}, req.Header["Authorization"])
assert.Equal(t, []string{"Bearer the-id.the-secret"}, req.Header["Authorization"])

if body, err := io.ReadAll(req.Body); assert.NoError(t, err) {
var data map[string]string
Expand All @@ -83,7 +85,7 @@ func TestJoinClient_JoinEtcd(t *testing.T) {
})

joinURL.Path = "/some/sub/path"
kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, t.Name(), "the-token")
kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, t.Name(), &bootstraptokenv1.BootstrapTokenString{ID: "the-id", Secret: "the-secret"})
require.NoError(t, err)
tok, err := token.JoinEncode(bytes.NewReader(kubeconfig))
require.NoError(t, err)
Expand Down Expand Up @@ -124,7 +126,7 @@ func TestJoinClient_Cancellation(t *testing.T) {
<-req.Context().Done() // block forever
})

kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, "", "")
kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, "", &bootstraptokenv1.BootstrapTokenString{})
require.NoError(t, err)
tok, err := token.JoinEncode(bytes.NewReader(kubeconfig))
require.NoError(t, err)
Expand Down
9 changes: 5 additions & 4 deletions pkg/token/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (

"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1"
)

const (
Expand Down Expand Up @@ -61,7 +62,7 @@ func CreateKubeletBootstrapToken(ctx context.Context, api *v1beta1.APISpec, k0sV
return JoinEncode(bytes.NewReader(kubeconfig))
}

func GenerateKubeconfig(joinURL string, caCert []byte, userName string, token string) ([]byte, error) {
func GenerateKubeconfig(joinURL string, caCert []byte, userName string, token *bootstraptokenv1.BootstrapTokenString) ([]byte, error) {
const k0sContextName = "k0s"
kubeconfig, err := clientcmd.Write(clientcmdapi.Config{
Clusters: map[string]*clientcmdapi.Cluster{k0sContextName: {
Expand All @@ -74,7 +75,7 @@ func GenerateKubeconfig(joinURL string, caCert []byte, userName string, token st
}},
CurrentContext: k0sContextName,
AuthInfos: map[string]*clientcmdapi.AuthInfo{userName: {
Token: token,
Token: token.String(),
}},
})
return kubeconfig, err
Expand All @@ -101,10 +102,10 @@ func loadCACert(k0sVars *config.CfgVars) ([]byte, error) {
return caCert, nil
}

func loadToken(ctx context.Context, k0sVars *config.CfgVars, role string, expiry time.Duration) (string, error) {
func loadToken(ctx context.Context, k0sVars *config.CfgVars, role string, expiry time.Duration) (*bootstraptokenv1.BootstrapTokenString, error) {
manager, err := NewManager(k0sVars.AdminKubeConfigPath)
if err != nil {
return "", err
return nil, err
}
return manager.Create(ctx, expiry, role)
}
Loading

0 comments on commit 2e0eefe

Please sign in to comment.