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 }