Certificate-less bootstrap tokens (#93)

This commit is contained in:
Nikolay Edigaryev 2023-06-23 00:53:03 +04:00 committed by GitHub
parent dcc954631b
commit c4c1851aff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 38 deletions

View File

@ -30,10 +30,6 @@ type BootstrapToken struct {
} }
func New(rawCertificate []byte, serviceAccountName string, serviceAccountToken string) (*BootstrapToken, error) { func New(rawCertificate []byte, serviceAccountName string, serviceAccountToken string) (*BootstrapToken, error) {
if len(rawCertificate) == 0 {
return nil, fmt.Errorf("%w: empty certificate", ErrFailedToCreateBootstrapToken)
}
if serviceAccountName == "" { if serviceAccountName == "" {
return nil, fmt.Errorf("%w: empty service account name", ErrFailedToCreateBootstrapToken) return nil, fmt.Errorf("%w: empty service account name", ErrFailedToCreateBootstrapToken)
} }
@ -42,18 +38,23 @@ func New(rawCertificate []byte, serviceAccountName string, serviceAccountToken s
return nil, fmt.Errorf("%w: empty service account token", ErrFailedToCreateBootstrapToken) return nil, fmt.Errorf("%w: empty service account token", ErrFailedToCreateBootstrapToken)
} }
// Parse certificate // Optionally parse a certificate
var certificate *x509.Certificate
var err error
if len(rawCertificate) != 0 {
block, _ := pem.Decode(rawCertificate) block, _ := pem.Decode(rawCertificate)
if block == nil { if block == nil {
return nil, fmt.Errorf("%w: failed to parse certificate: expected a PEM format", return nil, fmt.Errorf("%w: failed to parse certificate: expected a PEM format",
ErrFailedToCreateBootstrapToken) ErrFailedToCreateBootstrapToken)
} }
certificate, err := x509.ParseCertificate(block.Bytes) certificate, err = x509.ParseCertificate(block.Bytes)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: failed to parse certificate: %v", return nil, fmt.Errorf("%w: failed to parse certificate: %v",
ErrFailedToCreateBootstrapToken, err) ErrFailedToCreateBootstrapToken, err)
} }
}
return &BootstrapToken{ return &BootstrapToken{
version: version, version: version,
@ -78,10 +79,6 @@ func NewFromString(rawBootstrapToken string) (*BootstrapToken, error) {
return nil, fmt.Errorf("%w: missing service account credentials", ErrInvalidBootstrapTokenFormat) 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 { if len(splits) > 4 {
return nil, fmt.Errorf("%w: extraneous data", ErrInvalidBootstrapTokenFormat) return nil, fmt.Errorf("%w: extraneous data", ErrInvalidBootstrapTokenFormat)
} }
@ -96,20 +93,26 @@ func NewFromString(rawBootstrapToken string) (*BootstrapToken, error) {
return nil, fmt.Errorf("%w: failed to decode service account token: %v", return nil, fmt.Errorf("%w: failed to decode service account token: %v",
ErrInvalidBootstrapTokenFormat, err) ErrInvalidBootstrapTokenFormat, err)
} }
rawCertificate, err := encoding.DecodeString(splits[3])
// Optionally parse the certificate
var certificate *x509.Certificate
var rawCertificate []byte
if len(splits) == 4 {
rawCertificate, err = encoding.DecodeString(splits[3])
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: failed to decode certificate: %v", return nil, fmt.Errorf("%w: failed to decode certificate: %v",
ErrInvalidBootstrapTokenFormat, err) ErrInvalidBootstrapTokenFormat, err)
} }
// Parse certificate
block, _ := pem.Decode(rawCertificate) block, _ := pem.Decode(rawCertificate)
certificate, err := x509.ParseCertificate(block.Bytes) certificate, err = x509.ParseCertificate(block.Bytes)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: failed to parse certificate: %v", return nil, fmt.Errorf("%w: failed to parse certificate: %v",
ErrFailedToCreateBootstrapToken, err) ErrFailedToCreateBootstrapToken, err)
} }
}
return &BootstrapToken{ return &BootstrapToken{
version: version, version: version,
@ -121,12 +124,19 @@ func NewFromString(rawBootstrapToken string) (*BootstrapToken, error) {
} }
func (bt *BootstrapToken) String() string { 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, versionPrefix,
version, version,
encoding.EncodeToString([]byte(bt.serviceAccountName)), encoding.EncodeToString([]byte(bt.serviceAccountName)),
encoding.EncodeToString([]byte(bt.serviceAccountToken)), encoding.EncodeToString([]byte(bt.serviceAccountToken)),
encoding.EncodeToString(bt.rawCertificate), certificatePart,
) )
} }

View File

@ -10,9 +10,6 @@ import (
) )
func TestBootstrapTokenTwoWay(t *testing.T) { func TestBootstrapTokenTwoWay(t *testing.T) {
serviceAccountName := "admin"
serviceAccountToken := uuid.New().String()
tlsCert, err := controllercmd.GenerateSelfSignedControllerCertificate() tlsCert, err := controllercmd.GenerateSelfSignedControllerCertificate()
require.NoError(t, err) require.NoError(t, err)
@ -22,7 +19,19 @@ func TestBootstrapTokenTwoWay(t *testing.T) {
} }
certificatePEM := pem.EncodeToMemory(block) 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) require.NoError(t, err)
bootstrapTokenNew, err := bootstraptoken.NewFromString(bootstrapTokenOld.String()) bootstrapTokenNew, err := bootstraptoken.NewFromString(bootstrapTokenOld.String())

View File

@ -21,6 +21,7 @@ var ErrBootstrapTokenNotProvided = errors.New("no bootstrap token provided")
var bootstrapTokenRaw string var bootstrapTokenRaw string
var logFilePath string var logFilePath string
var stringToStringResources map[string]string var stringToStringResources map[string]string
var noPKI bool
func newRunCommand() *cobra.Command { func newRunCommand() *cobra.Command {
cmd := &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.") "optional path to a file where logs (up to 100 Mb) will be written.")
cmd.PersistentFlags().StringToStringVar(&stringToStringResources, "resources", map[string]string{}, cmd.PersistentFlags().StringToStringVar(&stringToStringResources, "resources", map[string]string{},
"resources that this worker provides") "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 return cmd
} }
func runWorker(cmd *cobra.Command, args []string) (err error) { func runWorker(cmd *cobra.Command, args []string) (err error) {
// Parse controller URL
controllerURL, err := netconstants.NormalizeAddress(args[0]) controllerURL, err := netconstants.NormalizeAddress(args[0])
if err != nil { if err != nil {
return err return err
} }
// Parse bootstrap token
if bootstrapTokenRaw == "" { if bootstrapTokenRaw == "" {
return ErrBootstrapTokenNotProvided return ErrBootstrapTokenNotProvided
} }
@ -59,11 +67,19 @@ func runWorker(cmd *cobra.Command, args []string) (err error) {
return fmt.Errorf("%w: %v", ErrRunFailed, err) return fmt.Errorf("%w: %v", ErrRunFailed, err)
} }
controllerClient, err := client.New( clientOpts := []client.Option{
client.WithAddress(controllerURL.String()), client.WithAddress(controllerURL.String()),
client.WithTrustedCertificate(bootstrapToken.Certificate()),
client.WithCredentials(bootstrapToken.ServiceAccountName(), bootstrapToken.ServiceAccountToken()), 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 { if err != nil {
return err return err
} }