From c4c1851aff2060dd4d03461943b8c29bbc0cd76b Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Fri, 23 Jun 2023 00:53:03 +0400 Subject: [PATCH] Certificate-less bootstrap tokens (#93) --- internal/bootstraptoken/bootstraptoken.go | 72 +++++++++++-------- .../bootstraptoken/bootstraptoken_test.go | 17 +++-- internal/command/worker/run.go | 22 +++++- 3 files changed, 73 insertions(+), 38 deletions(-) diff --git a/internal/bootstraptoken/bootstraptoken.go b/internal/bootstraptoken/bootstraptoken.go index 8ecc66c..1dfefbf 100644 --- a/internal/bootstraptoken/bootstraptoken.go +++ b/internal/bootstraptoken/bootstraptoken.go @@ -30,10 +30,6 @@ type BootstrapToken struct { } func New(rawCertificate []byte, serviceAccountName string, serviceAccountToken string) (*BootstrapToken, error) { - if len(rawCertificate) == 0 { - return nil, fmt.Errorf("%w: empty certificate", ErrFailedToCreateBootstrapToken) - } - if serviceAccountName == "" { return nil, fmt.Errorf("%w: empty service account name", ErrFailedToCreateBootstrapToken) } @@ -42,17 +38,22 @@ func New(rawCertificate []byte, serviceAccountName string, serviceAccountToken s return nil, fmt.Errorf("%w: empty service account token", ErrFailedToCreateBootstrapToken) } - // Parse certificate - block, _ := pem.Decode(rawCertificate) - if block == nil { - return nil, fmt.Errorf("%w: failed to parse certificate: expected a PEM format", - ErrFailedToCreateBootstrapToken) - } + // Optionally parse a certificate + var certificate *x509.Certificate + var err error - certificate, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("%w: failed to parse certificate: %v", - ErrFailedToCreateBootstrapToken, err) + if len(rawCertificate) != 0 { + block, _ := pem.Decode(rawCertificate) + if block == nil { + return nil, fmt.Errorf("%w: failed to parse certificate: expected a PEM format", + ErrFailedToCreateBootstrapToken) + } + + certificate, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("%w: failed to parse certificate: %v", + ErrFailedToCreateBootstrapToken, err) + } } return &BootstrapToken{ @@ -78,10 +79,6 @@ func NewFromString(rawBootstrapToken string) (*BootstrapToken, error) { return nil, fmt.Errorf("%w: missing service account credentials", ErrInvalidBootstrapTokenFormat) } - if len(splits) < 4 { - return nil, fmt.Errorf("%w: missing certificate", ErrInvalidBootstrapTokenFormat) - } - if len(splits) > 4 { return nil, fmt.Errorf("%w: extraneous data", ErrInvalidBootstrapTokenFormat) } @@ -96,19 +93,25 @@ func NewFromString(rawBootstrapToken string) (*BootstrapToken, error) { return nil, fmt.Errorf("%w: failed to decode service account token: %v", ErrInvalidBootstrapTokenFormat, err) } - rawCertificate, err := encoding.DecodeString(splits[3]) - if err != nil { - return nil, fmt.Errorf("%w: failed to decode certificate: %v", - ErrInvalidBootstrapTokenFormat, err) - } - // Parse certificate - block, _ := pem.Decode(rawCertificate) + // Optionally parse the certificate + var certificate *x509.Certificate + var rawCertificate []byte - certificate, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("%w: failed to parse certificate: %v", - ErrFailedToCreateBootstrapToken, err) + if len(splits) == 4 { + rawCertificate, err = encoding.DecodeString(splits[3]) + if err != nil { + return nil, fmt.Errorf("%w: failed to decode certificate: %v", + ErrInvalidBootstrapTokenFormat, err) + } + + block, _ := pem.Decode(rawCertificate) + + certificate, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("%w: failed to parse certificate: %v", + ErrFailedToCreateBootstrapToken, err) + } } return &BootstrapToken{ @@ -121,12 +124,19 @@ func NewFromString(rawBootstrapToken string) (*BootstrapToken, error) { } func (bt *BootstrapToken) String() string { - return fmt.Sprintf("%s%d.%s.%s.%s", + var certificatePart string + + // Certificate is optional + if len(bt.rawCertificate) != 0 { + certificatePart = fmt.Sprintf(".%s", encoding.EncodeToString(bt.rawCertificate)) + } + + return fmt.Sprintf("%s%d.%s.%s%s", versionPrefix, version, encoding.EncodeToString([]byte(bt.serviceAccountName)), encoding.EncodeToString([]byte(bt.serviceAccountToken)), - encoding.EncodeToString(bt.rawCertificate), + certificatePart, ) } diff --git a/internal/bootstraptoken/bootstraptoken_test.go b/internal/bootstraptoken/bootstraptoken_test.go index 021ff87..9458e3e 100644 --- a/internal/bootstraptoken/bootstraptoken_test.go +++ b/internal/bootstraptoken/bootstraptoken_test.go @@ -10,9 +10,6 @@ import ( ) func TestBootstrapTokenTwoWay(t *testing.T) { - serviceAccountName := "admin" - serviceAccountToken := uuid.New().String() - tlsCert, err := controllercmd.GenerateSelfSignedControllerCertificate() require.NoError(t, err) @@ -22,7 +19,19 @@ func TestBootstrapTokenTwoWay(t *testing.T) { } certificatePEM := pem.EncodeToMemory(block) - bootstrapTokenOld, err := bootstraptoken.New(certificatePEM, serviceAccountName, serviceAccountToken) + bootstrapTokenOld, err := bootstraptoken.New(certificatePEM, uuid.NewString(), uuid.NewString()) + require.NoError(t, err) + + bootstrapTokenNew, err := bootstraptoken.NewFromString(bootstrapTokenOld.String()) + require.NoError(t, err) + + require.Equal(t, bootstrapTokenOld.ServiceAccountName(), bootstrapTokenNew.ServiceAccountName()) + require.Equal(t, bootstrapTokenOld.ServiceAccountToken(), bootstrapTokenNew.ServiceAccountToken()) + require.Equal(t, bootstrapTokenOld.Certificate(), bootstrapTokenNew.Certificate()) +} + +func TestBootstrapTokenTwoWayEmptyCertificate(t *testing.T) { + bootstrapTokenOld, err := bootstraptoken.New([]byte{}, uuid.NewString(), uuid.NewString()) require.NoError(t, err) bootstrapTokenNew, err := bootstraptoken.NewFromString(bootstrapTokenOld.String()) diff --git a/internal/command/worker/run.go b/internal/command/worker/run.go index 6cda779..637a3f8 100644 --- a/internal/command/worker/run.go +++ b/internal/command/worker/run.go @@ -21,6 +21,7 @@ var ErrBootstrapTokenNotProvided = errors.New("no bootstrap token provided") var bootstrapTokenRaw string var logFilePath string var stringToStringResources map[string]string +var noPKI bool func newRunCommand() *cobra.Command { cmd := &cobra.Command{ @@ -36,15 +37,22 @@ func newRunCommand() *cobra.Command { "optional path to a file where logs (up to 100 Mb) will be written.") cmd.PersistentFlags().StringToStringVar(&stringToStringResources, "resources", map[string]string{}, "resources that this worker provides") + cmd.PersistentFlags().BoolVar(&noPKI, "no-pki", false, + "do not use the host's root CA set and instead validate the Controller's presented "+ + "certificate using a bootstrap token (or manually via fingerprint, "+ + "if no bootstrap token is provided)") return cmd } func runWorker(cmd *cobra.Command, args []string) (err error) { + // Parse controller URL controllerURL, err := netconstants.NormalizeAddress(args[0]) if err != nil { return err } + + // Parse bootstrap token if bootstrapTokenRaw == "" { return ErrBootstrapTokenNotProvided } @@ -59,11 +67,19 @@ func runWorker(cmd *cobra.Command, args []string) (err error) { return fmt.Errorf("%w: %v", ErrRunFailed, err) } - controllerClient, err := client.New( + clientOpts := []client.Option{ client.WithAddress(controllerURL.String()), - client.WithTrustedCertificate(bootstrapToken.Certificate()), client.WithCredentials(bootstrapToken.ServiceAccountName(), bootstrapToken.ServiceAccountToken()), - ) + } + + if trustedCertificate := bootstrapToken.Certificate(); trustedCertificate != nil { + clientOpts = append(clientOpts, client.WithTrustedCertificate(trustedCertificate)) + } else if noPKI { + return fmt.Errorf("%w: --no-pki was specified, but not trusted certificate was provided "+ + "in the bootstrap token", ErrRunFailed) + } + + controllerClient, err := client.New(clientOpts...) if err != nil { return err }