Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use kubeadm's bootstrap token types #5425

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading