Skip to content

Commit

Permalink
Use kubeadm's bootstrap token types
Browse files Browse the repository at this point in the history
Kubeadm has proper types for bootstrap tokens. Use those types instead
of the current ad-hoc solution. This includes the token generation,
which had a bias for certain values before.

Signed-off-by: Tom Wieczorek <[email protected]>
  • Loading branch information
twz123 committed Jan 10, 2025
1 parent 3ca4e93 commit be96a3a
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 @@ -90,12 +95,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 @@ -216,54 +221,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 @@ -87,10 +88,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 @@ -102,13 +103,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 @@ -123,7 +124,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 @@ -133,7 +134,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 be96a3a

Please sign in to comment.