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

Hive 2485/mce 2.6: Backport AssumeRole, credential_process, and kubeconfig exec fixes #2389

Merged
merged 4 commits into from
Aug 1, 2024
Merged
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
20 changes: 16 additions & 4 deletions contrib/pkg/utils/aws/aws.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package aws

import (
"errors"
"fmt"
corev1 "k8s.io/api/core/v1"
"os"
"path/filepath"
"strings"

corev1 "k8s.io/api/core/v1"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"

Expand Down Expand Up @@ -242,6 +244,16 @@ func GetAWSCreds(credsFile, defaultCredsFile string) (string, string, error) {
return accessKeyIDValue.String(), secretAccessKeyValue.String(), nil
}

var awsConfigForbidCredentialProcess utils.ProjectToDirFileFilter = func(key string, contents []byte) (basename string, newContents []byte, err error) {
// First, only process aws_config
bn, newContents, err := utils.ProjectOnlyTheseKeys(constants.AWSConfigSecretKey)(key, contents)
// If that passed, scrub for credential_process
if err == nil && bn != "" && awsclient.ContainsCredentialProcess(newContents) {
return "", nil, errors.New("credential_process is insecure and thus forbidden")
}
return bn, newContents, err
}

// ConfigureCreds loads a secret designated by the environment variables CLUSTERDEPLOYMENT_NAMESPACE
// and CREDS_SECRET_NAME and configures AWS credential environment variables and config files
// accordingly.
Expand All @@ -258,11 +270,11 @@ func ConfigureCreds(c client.Client) {
os.Setenv("AWS_SECRET_ACCESS_KEY", secret)
}
if config := credsSecret.Data[constants.AWSConfigSecretKey]; len(config) != 0 {
// Lay this down as a file
utils.ProjectToDir(credsSecret, constants.AWSCredsMount, constants.AWSConfigSecretKey)
// Lay this down as a file, but forbid credential_process
utils.ProjectToDir(credsSecret, constants.AWSCredsMount, awsConfigForbidCredentialProcess)
os.Setenv("AWS_CONFIG_FILE", filepath.Join(constants.AWSCredsMount, constants.AWSConfigSecretKey))
}
// Allow credential_process in the config file
// This would normally allow credential_process in the config file, but we checked for that above.
os.Setenv("AWS_SDK_LOAD_CONFIG", "true")
// Install cluster proxy trusted CA bundle
utils.InstallCerts(constants.TrustedCABundleDir)
Expand Down
2 changes: 1 addition & 1 deletion contrib/pkg/utils/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func ConfigureCreds(c client.Client) {
if credsSecret == nil {
return
}
utils.ProjectToDir(credsSecret, constants.AzureCredentialsDir)
utils.ProjectToDir(credsSecret, constants.AzureCredentialsDir, nil)
os.Setenv(constants.AzureCredentialsEnvVar, constants.AzureCredentialsDir+"/"+constants.AzureCredentialsName)
// Install cluster proxy trusted CA bundle
utils.InstallCerts(constants.TrustedCABundleDir)
Expand Down
2 changes: 1 addition & 1 deletion contrib/pkg/utils/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func ConfigureCreds(c client.Client) {
if credsSecret == nil {
return
}
utils.ProjectToDir(credsSecret, constants.GCPCredentialsDir)
utils.ProjectToDir(credsSecret, constants.GCPCredentialsDir, nil)
os.Setenv("GOOGLE_CREDENTIALS", constants.GCPCredentialsDir+"/"+constants.GCPCredentialsName)
// Install cluster proxy trusted CA bundle
utils.InstallCerts(constants.TrustedCABundleDir)
Expand Down
48 changes: 44 additions & 4 deletions contrib/pkg/utils/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,56 @@ func loadOrDie(c client.Client, nameEnvKey string, obj client.Object) bool {

}

// ProjectToDirFileFilter is run by ProjectToDir for each key found in the obj.
// If the second return is an error, ProjectToDir will panic with it. Otherwise:
// If ProjectToDir should create the file, the first return should be the base
// name of the file in which the newContents should be written.
// If the file should be skipped, the first return should be the empty string.
type ProjectToDirFileFilter func(key string, contents []byte) (basename string, newContents []byte, err error)

// projectDefault is a ProjectToDirFileFilter that causes ProjectToDir to create
// files for all keys in the obj, naming each file the same as its key.
var projectDefault ProjectToDirFileFilter = func(key string, contents []byte) (string, []byte, error) {
return key, contents, nil
}

// ProjectOnlyTheseKeys returns a ProjectToDirFileFilter that instructs ProjectToDir
// to create only the files with the specified keys. Each file's name will be the
// same as the key. The error return is always nil.
func ProjectOnlyTheseKeys(keys ...string) ProjectToDirFileFilter {
return func(key string, contents []byte) (string, []byte, error) {
if len(keys) == 0 {
// Caller should use nil (projectDefault) instead, but meh.
return key, contents, nil
}
if slice.ContainsString(keys, key, nil) {
// A match, project the file with the key as the basename and unchanged contents
return key, contents, nil
}
// No match; skip this file
return "", nil, nil
}
}

// ProjectToDir simulates what happens when you mount a secret or configmap as a volume on a pod, creating
// files named after each key under `dir` and populating them with the contents represented by the values.
func ProjectToDir(obj client.Object, dir string, keys ...string) {
write := func(filename string, bytes []byte) {
if len(keys) != 0 && !slice.ContainsString(keys, filename, nil) {
// This default behavior can be modified by specifying a non-nil filter to validate, skip, and/or rename
// the file corresponding to each key.
func ProjectToDir(obj client.Object, dir string, filter ProjectToDirFileFilter) {
write := func(key string, bytes []byte) {
if filter == nil {
filter = projectDefault
}
filename, newBytes, err := filter(key, bytes)
if err != nil {
panic(err)
}
if filename == "" {
// Skip this key
return
}
path := filepath.Join(dir, filename)
if err := os.WriteFile(path, bytes, 0400); err != nil {
if err := os.WriteFile(path, newBytes, 0400); err != nil {
log.WithError(err).WithField("path", path).Fatal("Failed to write file")
}
}
Expand Down
4 changes: 2 additions & 2 deletions contrib/pkg/utils/openstack/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ func GetCreds(credsFile string) ([]byte, error) {
// CREDS_SECRET_NAME, and CERTS_SECRET_NAME and configures OpenStack credential config files accordingly.
func ConfigureCreds(c client.Client) {
if credsSecret := utils.LoadSecretOrDie(c, "CREDS_SECRET_NAME"); credsSecret != nil {
utils.ProjectToDir(credsSecret, constants.OpenStackCredentialsDir)
utils.ProjectToDir(credsSecret, constants.OpenStackCredentialsDir, nil)
}
if certsSecret := utils.LoadSecretOrDie(c, "CERTS_SECRET_NAME"); certsSecret != nil {
utils.ProjectToDir(certsSecret, constants.OpenStackCertificatesDir)
utils.ProjectToDir(certsSecret, constants.OpenStackCertificatesDir, nil)
utils.InstallCerts(constants.OpenStackCertificatesDir)
}
// Install cluster proxy trusted CA bundle
Expand Down
4 changes: 2 additions & 2 deletions contrib/pkg/utils/ovirt/ovirt.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ func GetCreds(credsFile string) ([]byte, error) {
// and config files accordingly.
func ConfigureCreds(c client.Client) {
if credsSecret := utils.LoadSecretOrDie(c, "CREDS_SECRET_NAME"); credsSecret != nil {
utils.ProjectToDir(credsSecret, constants.OvirtCredentialsDir)
utils.ProjectToDir(credsSecret, constants.OvirtCredentialsDir, nil)
os.Setenv(constants.OvirtConfigEnvVar, constants.OvirtCredentialsDir+"/"+constants.OvirtCredentialsName)
}
if certsSecret := utils.LoadSecretOrDie(c, "CERTS_SECRET_NAME"); certsSecret != nil {
utils.ProjectToDir(certsSecret, constants.OvirtCertificatesDir)
utils.ProjectToDir(certsSecret, constants.OvirtCertificatesDir, nil)
utils.InstallCerts(constants.OvirtCertificatesDir)
}
// Install cluster proxy trusted CA bundle
Expand Down
4 changes: 2 additions & 2 deletions contrib/pkg/utils/vsphere/vsphere.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ func ConfigureCreds(c client.Client) {
}
// NOTE: I think this is only used for terminateWhenFilesChange(), which we don't really
// care about anymore. Can we get rid of it?
utils.ProjectToDir(credsSecret, constants.VSphereCredentialsDir)
utils.ProjectToDir(credsSecret, constants.VSphereCredentialsDir, nil)
}
if certsSecret := utils.LoadSecretOrDie(c, "CERTS_SECRET_NAME"); certsSecret != nil {
utils.ProjectToDir(certsSecret, constants.VSphereCertificatesDir)
utils.ProjectToDir(certsSecret, constants.VSphereCertificatesDir, nil)
utils.InstallCerts(constants.VSphereCertificatesDir)
}
// Install cluster proxy trusted CA bundle
Expand Down
2 changes: 1 addition & 1 deletion docs/awsassumerolecreds.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Let's create a Secret in `hive` (target namespace in HiveConfig) with credential

```console
$ cat aws-service-provider-config
[default]
[profile source]
aws_access_key_id = XXXXXX
aws_secret_access_key = XXXXX
role_arn = arn:aws:iam::123456:role/hive-aws-service-provider
Expand Down
25 changes: 21 additions & 4 deletions pkg/awsclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"context"
"fmt"
"os"
"regexp"
"strings"

"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -597,7 +599,10 @@ func NewSessionFromSecret(secret *corev1.Secret, region string) (*session.Sessio

// Special case to not use a secret to gather credentials.
if secret != nil {
config := awsCLIConfigFromSecret(secret)
config, err := awsCLIConfigFromSecret(secret)
if err != nil {
return nil, err
}
f, err := os.CreateTemp("", "hive-aws-config")
if err != nil {
return nil, err
Expand Down Expand Up @@ -626,18 +631,30 @@ func NewSessionFromSecret(secret *corev1.Secret, region string) (*session.Sessio
return s, nil
}

var credentialProcessRE = regexp.MustCompile(`(?i)\bcredential_process\b`)

func ContainsCredentialProcess(config []byte) bool {
return len(credentialProcessRE.Find(config)) != 0
}

// awsCLIConfigFromSecret returns an AWS CLI config using the data available in the secret.
func awsCLIConfigFromSecret(secret *corev1.Secret) []byte {
func awsCLIConfigFromSecret(secret *corev1.Secret) ([]byte, error) {
if config, ok := secret.Data[constants.AWSConfigSecretKey]; ok {
return config
if ContainsCredentialProcess(config) {
return nil, errors.New("credential_process is insecure and thus forbidden")
}
return config, nil
}

buf := &bytes.Buffer{}
fmt.Fprint(buf, "[default]\n")
for k, v := range secret.Data {
if strings.ToLower(k) == "credential_process" {
return nil, errors.New("credential_process is insecure and thus forbidden")
}
fmt.Fprintf(buf, "%s = %s\n", k, v)
}
return buf.Bytes()
return buf.Bytes(), nil
}

func awsChinaEndpointResolver(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
Expand Down
17 changes: 2 additions & 15 deletions pkg/controller/argocdregister/argocdregister_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func (r *ArgoCDRegisterController) reconcileCluster(cd *hivev1.ClusterDeployment
return reconcile.Result{}, nil
}

kubeConfigSecretName := cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef.Name
kubeConfigSecretName := cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef.Name // HIVE-2485 ✓
argoCDServerConfigBytes, err := r.generateArgoCDServerConfig(kubeConfigSecretName, cd.Namespace, argoCDNamespace, cdLog)
if err != nil {
return reconcile.Result{}, err
Expand Down Expand Up @@ -320,7 +320,7 @@ func (r *ArgoCDRegisterController) loadArgoCDServiceAccountToken(argoCDNamespace
}

func (r *ArgoCDRegisterController) generateArgoCDServerConfig(kubeconfigSecretName, kubeConfigSecretNamespace, argoCDNamespace string, cdLog log.FieldLogger) ([]byte, error) {
kubeconfig, err := r.loadSecretData(kubeconfigSecretName, kubeConfigSecretNamespace, adminKubeConfigKey)
kubeconfig, err := controllerutils.LoadSecretData(r.Client, kubeconfigSecretName, kubeConfigSecretNamespace, adminKubeConfigKey)
if err != nil {
cdLog.WithError(err).Error("unable to load cluster admin kubeconfig")
return nil, err
Expand Down Expand Up @@ -375,19 +375,6 @@ func tlsClientConfigBuilderFunc(kubeConfig clientcmd.ClientConfig, cdLog log.Fie
return tlsClientConfig, nil
}

func (r *ArgoCDRegisterController) loadSecretData(secretName, namespace, dataKey string) (string, error) {
s := &corev1.Secret{}
err := r.Get(context.TODO(), types.NamespacedName{Name: secretName, Namespace: namespace}, s)
if err != nil {
return "", err
}
retStr, ok := s.Data[dataKey]
if !ok {
return "", fmt.Errorf("secret %s did not contain key %s", secretName, dataKey)
}
return string(retStr), nil
}

// getPredictableSecretName generates a unique secret name by hashing the server API URL,
// which is required as all cluster secrets land in the argocd namespace.
// This code matches what is presently done in ArgoCD (util/db/cluster.go). However the actual
Expand Down
32 changes: 10 additions & 22 deletions pkg/controller/awsprivatelink/awsprivatelink_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/flowcontrol"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
Expand Down Expand Up @@ -301,7 +299,13 @@ func (r *ReconcileAWSPrivateLink) Reconcile(ctx context.Context, request reconci
return reconcile.Result{}, nil
}

return r.reconcilePrivateLink(cd, &hivev1.ClusterMetadata{InfraID: *cp.Spec.InfraID, AdminKubeconfigSecretRef: *cp.Spec.AdminKubeconfigSecretRef}, logger)
return r.reconcilePrivateLink(
cd,
&hivev1.ClusterMetadata{
InfraID: *cp.Spec.InfraID,
AdminKubeconfigSecretRef: *cp.Spec.AdminKubeconfigSecretRef, // HIVE-2585: via ClusterMetadata
},
logger)
}

// shouldSync returns if we should sync the desired ClusterDeployment. If it returns false, it also returns
Expand Down Expand Up @@ -533,7 +537,7 @@ func (r *ReconcileAWSPrivateLink) reconcilePrivateLink(cd *hivev1.ClusterDeploym

// Figure out the API address for cluster.
apiDomain, err := initialURL(r.Client,
client.ObjectKey{Namespace: cd.Namespace, Name: clusterMetadata.AdminKubeconfigSecretRef.Name})
client.ObjectKey{Namespace: cd.Namespace, Name: clusterMetadata.AdminKubeconfigSecretRef.Name}) // HIVE-2485 ✓
if err != nil {
logger.WithError(err).Error("could not get API URL from kubeconfig")

Expand Down Expand Up @@ -1268,7 +1272,7 @@ func newAWSClient(r *ReconcileAWSPrivateLink, cd *hivev1.ClusterDeployment) (*aw
},
AssumeRole: &awsclient.AssumeRoleCredentialsSource{
SecretRef: corev1.SecretReference{
Name: os.Getenv(constants.HiveAWSServiceProviderCredentialsSecretRefEnvVar),
Name: controllerutils.AWSServiceProviderSecretName(""),
Namespace: controllerutils.GetHiveNamespace(),
},
Role: cd.Spec.Platform.AWS.CredentialsAssumeRole,
Expand Down Expand Up @@ -1303,7 +1307,7 @@ func initialURL(c client.Client, key client.ObjectKey) (string, error) {
); err != nil {
return "", err
}
cfg, err := restConfigFromSecret(kubeconfigSecret)
cfg, err := controllerutils.RestConfigFromSecret(kubeconfigSecret, true)
if err != nil {
return "", errors.Wrap(err, "failed to load the kubeconfig")
}
Expand All @@ -1315,22 +1319,6 @@ func initialURL(c client.Client, key client.ObjectKey) (string, error) {
return strings.TrimSuffix(u.Hostname(), "."), nil
}

func restConfigFromSecret(kubeconfigSecret *corev1.Secret) (*rest.Config, error) {
kubeconfigData := kubeconfigSecret.Data[constants.RawKubeconfigSecretKey]
if len(kubeconfigData) == 0 {
kubeconfigData = kubeconfigSecret.Data[constants.KubeconfigSecretKey]
}
if len(kubeconfigData) == 0 {
return nil, errors.New("kubeconfig secret does not contain necessary data")
}
config, err := clientcmd.Load(kubeconfigData)
if err != nil {
return nil, err
}
kubeConfig := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{})
return kubeConfig.ClientConfig()
}

// awsErrCodeEquals returns true if the error matches all these conditions:
// - err is of type awserr.Error
// - Error.Code() equals code
Expand Down
4 changes: 2 additions & 2 deletions pkg/controller/awsprivatelink/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (r *ReconcileAWSPrivateLink) cleanupPreviousProvisionAttempt(cd *hivev1.Clu
}
metadata := &hivev1.ClusterMetadata{
InfraID: *cp.Spec.PrevInfraID,
AdminKubeconfigSecretRef: cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef,
AdminKubeconfigSecretRef: cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef, // HIVE-2485: via ClusterMetadata
}

if err := r.cleanupPrivateLink(cd, metadata, logger); err != nil {
Expand Down Expand Up @@ -138,7 +138,7 @@ func (r *ReconcileAWSPrivateLink) cleanupHostedZone(awsClient awsclient.Client,

if hzID == "" { // since we don't have the hz ID, we try to discover it to prevent leaks
apiDomain, err := initialURL(r.Client,
client.ObjectKey{Namespace: cd.Namespace, Name: metadata.AdminKubeconfigSecretRef.Name})
client.ObjectKey{Namespace: cd.Namespace, Name: metadata.AdminKubeconfigSecretRef.Name}) // HIVE-2485 ✓
if apierrors.IsNotFound(err) {
logger.Info("no hostedZoneID in status and admin kubeconfig does not exist, skipping hosted zone cleanup")
return nil
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/clusterclaim/clusterclaim_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ func (r *ReconcileClusterClaim) applyHiveClaimOwnerRole(claim *hivev1.ClusterCla
APIGroups: []string{corev1.GroupName},
Resources: []string{"secrets"},
ResourceNames: []string{
cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef.Name,
cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef.Name, // HIVE-2485: n/a
cd.Spec.ClusterMetadata.AdminPasswordSecretRef.Name,
},
Verbs: []string{"get"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,14 @@ func (r *ReconcileClusterDeployment) addAdditionalKubeconfigCAs(cd *hivev1.Clust
cdLog log.FieldLogger) error {

adminKubeconfigSecret := &corev1.Secret{}
if err := r.Get(context.Background(), types.NamespacedName{Namespace: cd.Namespace, Name: cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef.Name}, adminKubeconfigSecret); err != nil {
if err := r.Get(
context.Background(),
types.NamespacedName{
Namespace: cd.Namespace,
// HIVE-2485: No need to scrub here
Name: cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef.Name,
},
adminKubeconfigSecret); err != nil {
cdLog.WithError(err).Error("failed to get admin kubeconfig secret")
return err
}
Expand Down Expand Up @@ -658,6 +665,7 @@ func (r *ReconcileClusterDeployment) reconcile(request reconcile.Request, cd *hi
}

// Add cluster deployment as additional owner reference to admin secrets
// HIVE-2485: No need to scrub here
if err := r.addOwnershipToSecret(cd, cdLog, cd.Spec.ClusterMetadata.AdminKubeconfigSecretRef.Name); err != nil {
return reconcile.Result{}, err
}
Expand Down
Loading