github.com/openshift/installer@v1.4.17/pkg/asset/agent/gencrypto/authconfig.go (about)

     1  package gencrypto
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/ecdsa"
     7  	"crypto/elliptic"
     8  	"crypto/rand"
     9  	"crypto/x509"
    10  	"encoding/base64"
    11  	"encoding/pem"
    12  	"fmt"
    13  	"time"
    14  
    15  	"github.com/golang-jwt/jwt/v4"
    16  	"github.com/sirupsen/logrus"
    17  	corev1 "k8s.io/api/core/v1"
    18  	"k8s.io/apimachinery/pkg/api/errors"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/client-go/kubernetes"
    21  	"k8s.io/client-go/rest"
    22  	"k8s.io/client-go/tools/clientcmd"
    23  
    24  	"github.com/openshift/installer/pkg/asset"
    25  	"github.com/openshift/installer/pkg/asset/agent/common"
    26  	"github.com/openshift/installer/pkg/asset/agent/joiner"
    27  	"github.com/openshift/installer/pkg/asset/agent/workflow"
    28  )
    29  
    30  var (
    31  	authTokenSecretNamespace = "openshift-config" //nolint:gosec // no sensitive info
    32  	authTokenSecretName      = "agent-auth-token" //nolint:gosec // no sensitive info
    33  	authTokenSecretDataKey   = "agentAuthToken"
    34  	authTokenPublicDataKey   = "authTokenPublicKey"
    35  )
    36  
    37  // AuthType holds the authenticator type for agent based installer.
    38  const AuthType = "agent-installer-local"
    39  
    40  // AuthConfig is an asset that generates ECDSA public/private keys, JWT token.
    41  type AuthConfig struct {
    42  	PublicKey, AgentAuthToken, AgentAuthTokenExpiry, AuthType string
    43  }
    44  
    45  var _ asset.Asset = (*AuthConfig)(nil)
    46  
    47  // LocalJWTKeyType suggests the key type to be used for the token.
    48  type LocalJWTKeyType string
    49  
    50  const (
    51  	// InfraEnvKey is used to generate token using infra env id.
    52  	InfraEnvKey LocalJWTKeyType = "infra_env_id"
    53  )
    54  
    55  var _ asset.Asset = (*AuthConfig)(nil)
    56  
    57  // Dependencies returns the assets on which the AuthConfig asset depends.
    58  func (a *AuthConfig) Dependencies() []asset.Asset {
    59  	return []asset.Asset{
    60  		&common.InfraEnvID{},
    61  		&workflow.AgentWorkflow{},
    62  		&joiner.AddNodesConfig{},
    63  	}
    64  }
    65  
    66  // Generate generates the auth config for agent installer APIs.
    67  func (a *AuthConfig) Generate(_ context.Context, dependencies asset.Parents) error {
    68  	infraEnvID := &common.InfraEnvID{}
    69  	agentWorkflow := &workflow.AgentWorkflow{}
    70  	dependencies.Get(infraEnvID, agentWorkflow)
    71  	a.AuthType = AuthType
    72  
    73  	publicKey, privateKey, err := keyPairPEM()
    74  	if err != nil {
    75  		return err
    76  	}
    77  	// Encode to Base64 (Standard encoding)
    78  	encodedPubKeyPEM := base64.StdEncoding.EncodeToString([]byte(publicKey))
    79  
    80  	a.PublicKey = encodedPubKeyPEM
    81  
    82  	switch agentWorkflow.Workflow {
    83  	case workflow.AgentWorkflowTypeInstall:
    84  		// Auth tokens do not expire
    85  		token, err := generateToken(infraEnvID.ID, privateKey)
    86  		if err != nil {
    87  			return err
    88  		}
    89  		a.AgentAuthToken = token
    90  	case workflow.AgentWorkflowTypeAddNodes:
    91  		addNodesConfig := &joiner.AddNodesConfig{}
    92  		dependencies.Get(addNodesConfig)
    93  
    94  		// Auth tokens expires after 48 hours
    95  		expiry := time.Now().UTC().Add(48 * time.Hour)
    96  		a.AgentAuthTokenExpiry = expiry.Format(time.RFC3339)
    97  		token, err := generateToken(infraEnvID.ID, privateKey, expiry)
    98  		if err != nil {
    99  			return err
   100  		}
   101  		a.AgentAuthToken = token
   102  
   103  		err = a.createOrUpdateAuthTokenSecret(addNodesConfig.Params.Kubeconfig)
   104  		if err != nil {
   105  			return err
   106  		}
   107  	default:
   108  		return fmt.Errorf("AgentWorkflowType value not supported: %s", agentWorkflow.Workflow)
   109  	}
   110  	return nil
   111  }
   112  
   113  // Name returns the human-friendly name of the asset.
   114  func (*AuthConfig) Name() string {
   115  	return "Agent Installer API Auth Config"
   116  }
   117  
   118  // keyPairPEM returns the public, private keys in PEM format.
   119  func keyPairPEM() (string, string, error) {
   120  	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   121  	if err != nil {
   122  		return "", "", err
   123  	}
   124  
   125  	// encode private key to PEM string
   126  	privBytes, err := x509.MarshalECPrivateKey(priv)
   127  	if err != nil {
   128  		return "", "", err
   129  	}
   130  
   131  	block := &pem.Block{
   132  		Type:  "EC PRIVATE KEY",
   133  		Bytes: privBytes,
   134  	}
   135  
   136  	var privKeyPEM bytes.Buffer
   137  	err = pem.Encode(&privKeyPEM, block)
   138  	if err != nil {
   139  		return "", "", err
   140  	}
   141  
   142  	// encode public key to PEM string
   143  	pubBytes, err := x509.MarshalPKIXPublicKey(priv.Public())
   144  	if err != nil {
   145  		return "", "", err
   146  	}
   147  
   148  	block = &pem.Block{
   149  		Type:  "EC PUBLIC KEY",
   150  		Bytes: pubBytes,
   151  	}
   152  
   153  	var pubKeyPEM bytes.Buffer
   154  	err = pem.Encode(&pubKeyPEM, block)
   155  	if err != nil {
   156  		return "", "", err
   157  	}
   158  
   159  	return pubKeyPEM.String(), privKeyPEM.String(), nil
   160  }
   161  
   162  // generateToken returns a JWT token based on the private key.
   163  func generateToken(id string, privateKkeyPem string, expiry ...time.Time) (string, error) {
   164  	// Create the JWT claims
   165  	claims := jwt.MapClaims{
   166  		string(InfraEnvKey): id,
   167  	}
   168  
   169  	// Set the expiry time if provided
   170  	if len(expiry) > 0 {
   171  		claims["exp"] = expiry[0].Unix()
   172  	}
   173  
   174  	// Create the token using the ES256 signing method and the claims
   175  	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
   176  
   177  	priv, err := jwt.ParseECPrivateKeyFromPEM([]byte(privateKkeyPem))
   178  	if err != nil {
   179  		return "", err
   180  	}
   181  	// Sign the token with the provided private key
   182  	tokenString, err := token.SignedString(priv)
   183  	if err != nil {
   184  		return "", err
   185  	}
   186  	return tokenString, nil
   187  }
   188  
   189  func initClient(kubeconfig string) (*kubernetes.Clientset, error) {
   190  	var err error
   191  	var config *rest.Config
   192  	if kubeconfig != "" {
   193  		config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
   194  	} else {
   195  		config, err = rest.InClusterConfig()
   196  	}
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	k8sclientset, err := kubernetes.NewForConfig(config)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	return k8sclientset, err
   207  }
   208  
   209  func (a *AuthConfig) createOrUpdateAuthTokenSecret(kubeconfigPath string) error {
   210  	k8sclientset, err := initClient(kubeconfigPath)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	// check if secret exists
   215  	retrievedSecret, err := k8sclientset.CoreV1().Secrets(authTokenSecretNamespace).Get(context.Background(), authTokenSecretName, metav1.GetOptions{})
   216  	// if the secret does not exist
   217  	if err != nil {
   218  		if errors.IsNotFound(err) {
   219  			return a.createSecret(k8sclientset)
   220  		}
   221  		// Other errors while trying to get the secret
   222  		return fmt.Errorf("unable to retrieve secret %s/%s: %w", authTokenSecretNamespace, authTokenSecretName, err)
   223  	}
   224  
   225  	// if the secret exists in the cluster, get the token
   226  	retrievedToken, err := extractAuthTokenFromSecret(retrievedSecret)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	expiryTime, err := ParseExpirationFromToken(retrievedToken)
   231  	if err != nil {
   232  		return err
   233  	}
   234  	// Calculate 24 hours before the expiration time
   235  	thresholdTime := expiryTime.Add(-24 * time.Hour)
   236  	// Check if current time is past the thresholdTime time of 24 hours
   237  	if time.Now().UTC().After(thresholdTime) {
   238  		// update the secret in the cluster with a new token from asset store
   239  		err = a.refreshAuthTokenSecret(k8sclientset, retrievedSecret)
   240  		if err != nil {
   241  			return err
   242  		}
   243  		logrus.Debug("Auth token secret regenerated and updated in the cluster")
   244  	} else {
   245  		// Update the token in asset store with the retrieved token from the cluster
   246  		a.AgentAuthToken = retrievedToken
   247  
   248  		retrievedPublicKey, err := extractPublicKeyFromSecret(retrievedSecret)
   249  		if err != nil {
   250  			return err
   251  		}
   252  		// Update the asset store with the retrieved public key associated with the valid token from the cluster
   253  		a.PublicKey = retrievedPublicKey
   254  		logrus.Debugf("Reusing existing auth token (valid up to %s)", expiryTime)
   255  	}
   256  	return err
   257  }
   258  
   259  func (a *AuthConfig) createSecret(k8sclientset kubernetes.Interface) error {
   260  	// Create a Secret
   261  	secret := &corev1.Secret{
   262  		ObjectMeta: metav1.ObjectMeta{
   263  			Name: authTokenSecretName,
   264  			Annotations: map[string]string{
   265  				"updatedAt": "", // Initially set to empty
   266  			},
   267  		},
   268  		Type: corev1.SecretTypeOpaque,
   269  		Data: map[string][]byte{
   270  			authTokenSecretDataKey: []byte(a.AgentAuthToken),
   271  			authTokenPublicDataKey: []byte(a.PublicKey),
   272  		},
   273  	}
   274  	_, err := k8sclientset.CoreV1().Secrets(authTokenSecretNamespace).Create(context.Background(), secret, metav1.CreateOptions{})
   275  	if err != nil {
   276  		return fmt.Errorf("failed to create auth token secret: %w", err)
   277  	}
   278  	logrus.Debugf("Created auth token secret %s/%s", authTokenSecretNamespace, authTokenSecretName)
   279  
   280  	return nil
   281  }
   282  
   283  func (a *AuthConfig) refreshAuthTokenSecret(k8sclientset kubernetes.Interface, retrievedSecret *corev1.Secret) error {
   284  	retrievedSecret.Data[authTokenSecretDataKey] = []byte(a.AgentAuthToken)
   285  	retrievedSecret.Data[authTokenPublicDataKey] = []byte(a.PublicKey)
   286  	// only for informational purposes
   287  	retrievedSecret.Annotations["updatedAt"] = time.Now().UTC().Format(time.RFC3339)
   288  
   289  	_, err := k8sclientset.CoreV1().Secrets(authTokenSecretNamespace).Update(context.TODO(), retrievedSecret, metav1.UpdateOptions{})
   290  	if err != nil {
   291  		return err
   292  	}
   293  	logrus.Debugf("Updated auth token secret %s/%s", authTokenSecretNamespace, authTokenSecretName)
   294  	return nil
   295  }
   296  
   297  // GetAuthTokenFromCluster returns a token string stored as the secret from the cluster.
   298  func GetAuthTokenFromCluster(ctx context.Context, kubeconfigPath string) (string, error) {
   299  	client, err := initClient(kubeconfigPath)
   300  	if err != nil {
   301  		return "", err
   302  	}
   303  
   304  	retrievedSecret, err := client.CoreV1().Secrets(authTokenSecretNamespace).Get(ctx, authTokenSecretName, metav1.GetOptions{})
   305  	if err != nil {
   306  		return "", err
   307  	}
   308  	authToken, err := extractAuthTokenFromSecret(retrievedSecret)
   309  	if err != nil {
   310  		return "", err
   311  	}
   312  	return authToken, err
   313  }
   314  
   315  func extractAuthTokenFromSecret(secret *corev1.Secret) (string, error) {
   316  	existingAgentAuthToken, exists := secret.Data[authTokenSecretDataKey]
   317  	if !exists || len(existingAgentAuthToken) == 0 {
   318  		return "", fmt.Errorf("auth token secret %s/%s does not contain the key %s or is empty", authTokenSecretNamespace, authTokenSecretName, authTokenSecretDataKey)
   319  	}
   320  	return string(existingAgentAuthToken), nil
   321  }
   322  
   323  func extractPublicKeyFromSecret(secret *corev1.Secret) (string, error) {
   324  	existingPublicKey, exists := secret.Data[authTokenPublicDataKey]
   325  	if !exists || len(existingPublicKey) == 0 {
   326  		return "", fmt.Errorf("auth token secret %s/%s does not contain the key %s or is empty", authTokenSecretNamespace, authTokenSecretName, authTokenPublicDataKey)
   327  	}
   328  	return string(existingPublicKey), nil
   329  }