Repository: replicatedhq/kots
Pull request #284 (2020-01-28 23:47:30)

include auth string in calls to kotsadm


Patch:
diff --git a/cmd/kots/cli/upstream-upgrade.go b/cmd/kots/cli/upstream-upgrade.go
index 195e510f5..df5e2f09a 100644
--- a/cmd/kots/cli/upstream-upgrade.go
+++ b/cmd/kots/cli/upstream-upgrade.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/auth"
 	"github.com/replicatedhq/kots/pkg/k8sutil"
 	"github.com/replicatedhq/kots/pkg/logger"
 	"github.com/spf13/cobra"
@@ -66,7 +67,21 @@ func UpstreamUpgradeCmd() *cobra.Command {
 			if viper.GetBool("deploy") {
 				updateCheckURI = fmt.Sprintf("%s?deploy=true", updateCheckURI)
 			}
-			resp, err := http.Post(updateCheckURI, "application/json", strings.NewReader("{}"))
+
+			authSlug, err := auth.GetOrCreateAuthSlug(v.GetString("namespace"))
+			if err != nil {
+				log.FinishSpinnerWithError()
+				return errors.Wrap(err, "failed to get kotsadm auth slug")
+			}
+
+			newReq, err := http.NewRequest("POST", updateCheckURI, strings.NewReader("{}"))
+			if err != nil {
+				log.FinishSpinnerWithError()
+				return errors.Wrap(err, "failed to create update check request")
+			}
+			newReq.Header.Add("Content-Type", "application/json")
+			newReq.Header.Add("Authorization", authSlug)
+			resp, err := http.DefaultClient.Do(newReq)
 			if err != nil {
 				log.FinishSpinnerWithError()
 				return errors.Wrap(err, "failed to check for updates")
diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go
new file mode 100644
index 000000000..54a06fae8
--- /dev/null
+++ b/pkg/auth/auth.go
@@ -0,0 +1,79 @@
+package auth
+
+import (
+	"fmt"
+
+	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/kotsadm/types"
+	"github.com/replicatedhq/kots/pkg/util"
+	v1 "k8s.io/api/core/v1"
+	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+)
+
+const KotsadmAuthstringSecretName = "kotsadm-authstring"
+const KotsadmAuthstringSecretKey = "kotsadm-authstring"
+
+var authSlugCache string
+
+// SetAuthSlugCache sets the auth slug to be used instead of querying kubernetes
+// this improves run speed by reducing the number of queries, and also allows testing without a kubernetes cluster available
+func SetAuthSlugCache(newval string) {
+	authSlugCache = newval
+}
+
+// GetOrCreateAuthSlug will check for an authslug secret in the provided namespace
+// if one exists, it will return the value from that secret
+// if none exists, it will create one and return that value
+func GetOrCreateAuthSlug(namespace string) (string, error) {
+	if authSlugCache != "" {
+		return authSlugCache, nil
+	}
+
+	cfg, err := config.GetConfig()
+	if err != nil {
+		return "", errors.Wrap(err, "failed to get cluster config")
+	}
+
+	clientset, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return "", errors.Wrap(err, "failed to create kubernetes clientset")
+	}
+
+	existingSecret, err := clientset.CoreV1().Secrets(namespace).Get(KotsadmAuthstringSecretName, metav1.GetOptions{})
+	if err != nil {
+		if !kuberneteserrors.IsNotFound(err) {
+			fmt.Printf("\nUnable to authenticate to the Admin Console running in the %s namespace. Ensure you have read access to secrets in this namespace and try again.\n\n", namespace)
+			return "", errors.Wrap(err, "failed to check for existing kotsadm authstring secret")
+		}
+
+		// secret does not yet exist, so we need to generate a random key and create the secret from that
+		newAuthstring := "Kots " + util.GenPassword(32)
+		newSecret := v1.Secret{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: "v1",
+				Kind:       "Secret",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      KotsadmAuthstringSecretName,
+				Namespace: namespace,
+				Labels: map[string]string{
+					types.KotsadmKey: types.KotsadmLabelValue,
+				},
+			},
+			StringData: map[string]string{KotsadmAuthstringSecretKey: newAuthstring},
+		}
+
+		_, err := clientset.CoreV1().Secrets(namespace).Create(&newSecret)
+		if err != nil {
+			return "", errors.Wrap(err, "failed to create new kotsadm authstring secret")
+		}
+		SetAuthSlugCache(newAuthstring)
+		return newAuthstring, nil
+	}
+
+	SetAuthSlugCache(string(existingSecret.Data[KotsadmAuthstringSecretKey]))
+	return string(existingSecret.Data[KotsadmAuthstringSecretKey]), nil
+}
diff --git a/pkg/download/download.go b/pkg/download/download.go
index c888c981b..9f770185b 100644
--- a/pkg/download/download.go
+++ b/pkg/download/download.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/mholt/archiver"
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/auth"
 	"github.com/replicatedhq/kots/pkg/k8sutil"
 	"github.com/replicatedhq/kots/pkg/logger"
 )
@@ -53,7 +54,20 @@ func Download(appSlug string, path string, downloadOptions DownloadOptions) erro
 		}
 	}()
 
-	resp, err := http.Get(fmt.Sprintf("http://localhost:3000/api/v1/kots/%s", appSlug))
+	authSlug, err := auth.GetOrCreateAuthSlug(downloadOptions.Namespace)
+	if err != nil {
+		log.FinishSpinnerWithError()
+		return errors.Wrap(err, "failed to get kotsadm auth slug")
+	}
+
+	newRequest, err := http.NewRequest("get", fmt.Sprintf("http://localhost:3000/api/v1/kots/%s", appSlug), nil)
+	if err != nil {
+		log.FinishSpinnerWithError()
+		return errors.Wrap(err, "failed to create download request")
+	}
+	newRequest.Header.Add("Authorization", authSlug)
+
+	resp, err := http.DefaultClient.Do(newRequest)
 	if err != nil {
 		log.FinishSpinnerWithError()
 		return errors.Wrap(err, "failed to get from kotsadm")
diff --git a/pkg/k8sutil/portforward.go b/pkg/k8sutil/portforward.go
index 3f268c516..4ac3796cc 100644
--- a/pkg/k8sutil/portforward.go
+++ b/pkg/k8sutil/portforward.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/phayes/freeport"
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/auth"
 	"github.com/replicatedhq/kots/pkg/logger"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -179,6 +180,13 @@ func PortForward(kubeContext string, localPort int, remotePort int, namespace st
 				}
 				req.Header.Set("Accept", "application/json")
 
+				authSlug, err := auth.GetOrCreateAuthSlug(namespace)
+				if err != nil {
+					runtime.HandleError(errors.Wrap(err, "failed to get kotsadm auth slug"))
+					continue
+				}
+				req.Header.Add("Authorization", authSlug)
+
 				resp, err := http.DefaultClient.Do(req)
 				if err != nil {
 					runtime.HandleError(errors.Wrap(err, "failed to get ports"))
diff --git a/pkg/k8sutil/updaterole.go b/pkg/k8sutil/updaterole.go
new file mode 100644
index 000000000..38a0cbde0
--- /dev/null
+++ b/pkg/k8sutil/updaterole.go
@@ -0,0 +1,29 @@
+package k8sutil
+
+import (
+	"github.com/replicatedhq/kots/pkg/util"
+	"k8s.io/api/rbac/v1"
+)
+
+func UpdateRole(existingRole, desiredRole *v1.Role) {
+	newRules := []v1.PolicyRule{}
+
+	// merge roles, with any rules only present in the desired role being added to the existing role
+	for _, desiredRule := range desiredRole.Rules {
+		foundMatch := false
+		for _, existingRule := range existingRole.Rules {
+			if util.CompareStringArrays(desiredRule.Resources, existingRule.Resources) &&
+				util.CompareStringArrays(desiredRule.APIGroups, existingRule.APIGroups) &&
+				util.CompareStringArrays(desiredRule.NonResourceURLs, existingRule.NonResourceURLs) &&
+				util.CompareStringArrays(desiredRule.ResourceNames, existingRule.ResourceNames) &&
+				util.CompareStringArrays(desiredRule.Verbs, existingRule.Verbs) {
+				foundMatch = true
+			}
+		}
+		if !foundMatch {
+			newRules = append(newRules, desiredRule)
+		}
+	}
+
+	existingRole.Rules = append(existingRole.Rules, newRules...)
+}
diff --git a/pkg/kotsadm/api.go b/pkg/kotsadm/api.go
index 4ee90b123..d6f2c66eb 100644
--- a/pkg/kotsadm/api.go
+++ b/pkg/kotsadm/api.go
@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/k8sutil"
 	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	corev1 "k8s.io/api/core/v1"
 	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
@@ -114,7 +115,7 @@ func ensureApiRBAC(namespace string, clientset *kubernetes.Clientset) error {
 }
 
 func ensureApiRole(namespace string, clientset *kubernetes.Clientset) error {
-	_, err := clientset.RbacV1().Roles(namespace).Get("kotsadm-api-role", metav1.GetOptions{})
+	currentRole, err := clientset.RbacV1().Roles(namespace).Get("kotsadm-api-role", metav1.GetOptions{})
 	if err != nil {
 		if !kuberneteserrors.IsNotFound(err) {
 			return errors.Wrap(err, "failed to get role")
@@ -126,7 +127,12 @@ func ensureApiRole(namespace string, clientset *kubernetes.Clientset) error {
 		}
 	}
 
-	// We have never changed the role, so there is no "upgrade" applied
+	// we have now changed the role, so an upgrade is required
+	k8sutil.UpdateRole(currentRole, apiRole(namespace))
+	_, err = clientset.RbacV1().Roles(namespace).Update(currentRole)
+	if err != nil {
+		return errors.Wrap(err, "failed to update role")
+	}
 
 	return nil
 }
diff --git a/pkg/kotsadm/api_objects.go b/pkg/kotsadm/api_objects.go
index c1eb2191c..c8fe48cae 100644
--- a/pkg/kotsadm/api_objects.go
+++ b/pkg/kotsadm/api_objects.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/auth"
 	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	"github.com/replicatedhq/kots/pkg/util"
 	appsv1 "k8s.io/api/apps/v1"
@@ -23,7 +24,7 @@ func apiRole(namespace string) *rbacv1.Role {
 			Name:      "kotsadm-api-role",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		// creation cannot be restricted by name
@@ -42,7 +43,7 @@ func apiRole(namespace string) *rbacv1.Role {
 			{
 				APIGroups:     []string{""},
 				Resources:     []string{"secrets"},
-				ResourceNames: []string{"kotsadm-encryption", "kotsadm-gitops"},
+				ResourceNames: []string{"kotsadm-encryption", "kotsadm-gitops", auth.KotsadmAuthstringSecretName},
 				Verbs:         metav1.Verbs{"get", "update"},
 			},
 			{
@@ -66,7 +67,7 @@ func apiRoleBinding(namespace string) *rbacv1.RoleBinding {
 			Name:      "kotsadm-api-rolebinding",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Subjects: []rbacv1.Subject{
@@ -96,7 +97,7 @@ func apiServiceAccount(namespace string) *corev1.ServiceAccount {
 			Name:      "kotsadm-api",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 	}
@@ -118,11 +119,11 @@ func updateApiDeployment(deployment *appsv1.Deployment, deployOptions types.Depl
 	if deployment.ObjectMeta.Labels == nil {
 		deployment.ObjectMeta.Labels = map[string]string{}
 	}
-	deployment.ObjectMeta.Labels[KotsadmKey] = KotsadmLabelValue
+	deployment.ObjectMeta.Labels[types.KotsadmKey] = types.KotsadmLabelValue
 	if deployment.Spec.Template.ObjectMeta.Labels == nil {
 		deployment.Spec.Template.ObjectMeta.Labels = map[string]string{}
 	}
-	deployment.Spec.Template.ObjectMeta.Labels[KotsadmKey] = KotsadmLabelValue
+	deployment.Spec.Template.ObjectMeta.Labels[types.KotsadmKey] = types.KotsadmLabelValue
 
 	// security context (added in 1.11.0)
 	deployment.Spec.Template.Spec.SecurityContext = &securityContext
@@ -181,7 +182,7 @@ func apiDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 			Name:      "kotsadm-api",
 			Namespace: deployOptions.Namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: appsv1.DeploymentSpec{
@@ -193,8 +194,8 @@ func apiDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 			Template: corev1.PodTemplateSpec{
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: map[string]string{
-						"app":      "kotsadm-api",
-						KotsadmKey: KotsadmLabelValue,
+						"app":            "kotsadm-api",
+						types.KotsadmKey: types.KotsadmLabelValue,
 					},
 				},
 				Spec: corev1.PodSpec{
@@ -352,7 +353,7 @@ func apiService(namespace string) *corev1.Service {
 			Name:      "kotsadm-api-node",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: corev1.ServiceSpec{
diff --git a/pkg/kotsadm/application_metadata_objects.go b/pkg/kotsadm/application_metadata_objects.go
index bcf74b526..f57c8bc7f 100644
--- a/pkg/kotsadm/application_metadata_objects.go
+++ b/pkg/kotsadm/application_metadata_objects.go
@@ -1,6 +1,7 @@
 package kotsadm
 
 import (
+	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -8,7 +9,7 @@ import (
 func applicationMetadataConfig(data []byte, namespace string) *corev1.ConfigMap {
 	labels := map[string]string{}
 	labels["kotsadm"] = "application"
-	labels[KotsadmKey] = KotsadmLabelValue
+	labels[types.KotsadmKey] = types.KotsadmLabelValue
 
 	configMap := &corev1.ConfigMap{
 		TypeMeta: metav1.TypeMeta{
diff --git a/pkg/kotsadm/kotsadm.go b/pkg/kotsadm/kotsadm.go
index 4c5842239..55c647f05 100644
--- a/pkg/kotsadm/kotsadm.go
+++ b/pkg/kotsadm/kotsadm.go
@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/k8sutil"
 	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	corev1 "k8s.io/api/core/v1"
 	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
@@ -16,9 +17,6 @@ import (
 
 var timeoutWaitingForKotsadm = time.Duration(time.Minute * 2)
 
-const KotsadmKey = "kots.io/kotsadm"
-const KotsadmLabelValue = "true"
-
 func getKotsadmYAML(deployOptions types.DeployOptions) (map[string][]byte, error) {
 	docs := map[string][]byte{}
 	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
@@ -117,7 +115,7 @@ func ensureKotsadmRBAC(namespace string, clientset *kubernetes.Clientset) error
 }
 
 func ensureKotsadmRole(namespace string, clientset *kubernetes.Clientset) error {
-	_, err := clientset.RbacV1().Roles(namespace).Get("kotsadm-role", metav1.GetOptions{})
+	currentRole, err := clientset.RbacV1().Roles(namespace).Get("kotsadm-role", metav1.GetOptions{})
 	if err != nil {
 		if !kuberneteserrors.IsNotFound(err) {
 			return errors.Wrap(err, "failed to get role")
@@ -129,6 +127,13 @@ func ensureKotsadmRole(namespace string, clientset *kubernetes.Clientset) error
 		}
 	}
 
+	// we have now changed the role, so an upgrade is required
+	k8sutil.UpdateRole(currentRole, kotsadmRole(namespace))
+	_, err = clientset.RbacV1().Roles(namespace).Update(currentRole)
+	if err != nil {
+		return errors.Wrap(err, "failed to update role")
+	}
+
 	return nil
 }
 
diff --git a/pkg/kotsadm/kotsadm_objects.go b/pkg/kotsadm/kotsadm_objects.go
index dff955e4b..54704294e 100644
--- a/pkg/kotsadm/kotsadm_objects.go
+++ b/pkg/kotsadm/kotsadm_objects.go
@@ -3,6 +3,7 @@ package kotsadm
 import (
 	"fmt"
 
+	"github.com/replicatedhq/kots/pkg/auth"
 	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	"github.com/replicatedhq/kots/pkg/util"
 	appsv1 "k8s.io/api/apps/v1"
@@ -22,7 +23,7 @@ func kotsadmRole(namespace string) *rbacv1.Role {
 			Name:      "kotsadm-role",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		// creation cannot be restricted by name
@@ -45,6 +46,7 @@ func kotsadmRole(namespace string) *rbacv1.Role {
 					"kotsadm-encryption",
 					"kotsadm-gitops",
 					"kotsadm-password",
+					auth.KotsadmAuthstringSecretName,
 				},
 				Verbs: metav1.Verbs{"get", "update"},
 			},
@@ -69,7 +71,7 @@ func kotsadmRoleBinding(namespace string) *rbacv1.RoleBinding {
 			Name:      "kotsadm-rolebinding",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Subjects: []rbacv1.Subject{
@@ -99,7 +101,7 @@ func kotsadmServiceAccount(namespace string) *corev1.ServiceAccount {
 			Name:      "kotsadm",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 	}
@@ -124,7 +126,7 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 			Name:      "kotsadm",
 			Namespace: deployOptions.Namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: appsv1.DeploymentSpec{
@@ -136,8 +138,8 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 			Template: corev1.PodTemplateSpec{
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: map[string]string{
-						"app":      "kotsadm",
-						KotsadmKey: KotsadmLabelValue,
+						"app":            "kotsadm",
+						types.KotsadmKey: types.KotsadmLabelValue,
 					},
 				},
 				Spec: corev1.PodSpec{
diff --git a/pkg/kotsadm/minio_objects.go b/pkg/kotsadm/minio_objects.go
index 32181d1a7..664802bbb 100644
--- a/pkg/kotsadm/minio_objects.go
+++ b/pkg/kotsadm/minio_objects.go
@@ -83,7 +83,7 @@ func minioStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 			Name:      "kotsadm-minio",
 			Namespace: deployOptions.Namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: appsv1.StatefulSetSpec{
@@ -100,7 +100,7 @@ func minioStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 					ObjectMeta: metav1.ObjectMeta{
 						Name: "kotsadm-minio",
 						Labels: map[string]string{
-							KotsadmKey: KotsadmLabelValue,
+							types.KotsadmKey: types.KotsadmLabelValue,
 						},
 					},
 					Spec: corev1.PersistentVolumeClaimSpec{
@@ -118,8 +118,8 @@ func minioStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 			Template: corev1.PodTemplateSpec{
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: map[string]string{
-						"app":      "kotsadm-minio",
-						KotsadmKey: KotsadmLabelValue,
+						"app":            "kotsadm-minio",
+						types.KotsadmKey: types.KotsadmLabelValue,
 					},
 				},
 				Spec: corev1.PodSpec{
@@ -243,7 +243,7 @@ func minioService(namespace string) *corev1.Service {
 			Name:      "kotsadm-minio",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: corev1.ServiceSpec{
diff --git a/pkg/kotsadm/operator_objects.go b/pkg/kotsadm/operator_objects.go
index 2af8486dd..e3944b978 100644
--- a/pkg/kotsadm/operator_objects.go
+++ b/pkg/kotsadm/operator_objects.go
@@ -22,7 +22,7 @@ func operatorClusterRole(namespace string) *rbacv1.ClusterRole {
 			Name:      "kotsadm-operator-role",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Rules: []rbacv1.PolicyRule{
@@ -47,7 +47,7 @@ func operatorClusterRoleBinding(namespace string) *rbacv1.ClusterRoleBinding {
 			Name:      "kotsadm-operator-rolebinding",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Subjects: []rbacv1.Subject{
@@ -77,7 +77,7 @@ func operatorRole(namespace string) *rbacv1.Role {
 			Name:      "kotsadm-operator-role",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Rules: []rbacv1.PolicyRule{
@@ -102,7 +102,7 @@ func operatorRoleBinding(namespace string) *rbacv1.RoleBinding {
 			Name:      "kotsadm-operator-rolebinding",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Subjects: []rbacv1.Subject{
@@ -132,7 +132,7 @@ func operatorServiceAccount(namespace string) *corev1.ServiceAccount {
 			Name:      "kotsadm-operator",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 	}
@@ -154,11 +154,11 @@ func updateOperatorDeployment(deployment *appsv1.Deployment, deployOptions types
 	if deployment.ObjectMeta.Labels == nil {
 		deployment.ObjectMeta.Labels = map[string]string{}
 	}
-	deployment.ObjectMeta.Labels[KotsadmKey] = KotsadmLabelValue
+	deployment.ObjectMeta.Labels[types.KotsadmKey] = types.KotsadmLabelValue
 	if deployment.Spec.Template.ObjectMeta.Labels == nil {
 		deployment.Spec.Template.ObjectMeta.Labels = map[string]string{}
 	}
-	deployment.Spec.Template.ObjectMeta.Labels[KotsadmKey] = KotsadmLabelValue
+	deployment.Spec.Template.ObjectMeta.Labels[types.KotsadmKey] = types.KotsadmLabelValue
 
 	// security context (added in 1.11.0)
 	deployment.Spec.Template.Spec.SecurityContext = &securityContext
@@ -217,7 +217,7 @@ func operatorDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 			Name:      "kotsadm-operator",
 			Namespace: deployOptions.Namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: appsv1.DeploymentSpec{
@@ -229,8 +229,8 @@ func operatorDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 			Template: corev1.PodTemplateSpec{
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: map[string]string{
-						"app":      "kotsadm-operator",
-						KotsadmKey: KotsadmLabelValue,
+						"app":            "kotsadm-operator",
+						types.KotsadmKey: types.KotsadmLabelValue,
 					},
 				},
 				Spec: corev1.PodSpec{
diff --git a/pkg/kotsadm/postgres_objects.go b/pkg/kotsadm/postgres_objects.go
index 0459bfb03..b97e502d0 100644
--- a/pkg/kotsadm/postgres_objects.go
+++ b/pkg/kotsadm/postgres_objects.go
@@ -58,7 +58,7 @@ func postgresStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet
 			Name:      "kotsadm-postgres",
 			Namespace: deployOptions.Namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: appsv1.StatefulSetSpec{
@@ -72,7 +72,7 @@ func postgresStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet
 					ObjectMeta: metav1.ObjectMeta{
 						Name: "kotsadm-postgres",
 						Labels: map[string]string{
-							KotsadmKey: KotsadmLabelValue,
+							types.KotsadmKey: types.KotsadmLabelValue,
 						},
 					},
 					Spec: corev1.PersistentVolumeClaimSpec{
@@ -90,8 +90,8 @@ func postgresStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet
 			Template: corev1.PodTemplateSpec{
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: map[string]string{
-						"app":      "kotsadm-postgres",
-						KotsadmKey: KotsadmLabelValue,
+						"app":            "kotsadm-postgres",
+						types.KotsadmKey: types.KotsadmLabelValue,
 					},
 				},
 				Spec: corev1.PodSpec{
@@ -198,7 +198,7 @@ func postgresService(namespace string) *corev1.Service {
 			Name:      "kotsadm-postgres",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: corev1.ServiceSpec{
diff --git a/pkg/kotsadm/schemahero_objects.go b/pkg/kotsadm/schemahero_objects.go
index eaef5e48c..ca5cb346d 100644
--- a/pkg/kotsadm/schemahero_objects.go
+++ b/pkg/kotsadm/schemahero_objects.go
@@ -30,7 +30,7 @@ func migrationsPod(deployOptions types.DeployOptions) *corev1.Pod {
 			Name:      name,
 			Namespace: deployOptions.Namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Spec: corev1.PodSpec{
diff --git a/pkg/kotsadm/secrets_objects.go b/pkg/kotsadm/secrets_objects.go
index bd3b01d22..dc3159479 100644
--- a/pkg/kotsadm/secrets_objects.go
+++ b/pkg/kotsadm/secrets_objects.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/google/uuid"
+	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -22,7 +23,7 @@ func jwtSecret(namespace string, jwt string) *corev1.Secret {
 			Name:      "kotsadm-session",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Data: map[string][]byte{
@@ -47,7 +48,7 @@ func pgSecret(namespace string, password string) *corev1.Secret {
 			Name:      "kotsadm-postgres",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Data: map[string][]byte{
@@ -69,7 +70,7 @@ func sharedPasswordSecret(namespace string, bcryptPassword string) *corev1.Secre
 			Name:      "kotsadm-password",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Data: map[string][]byte{
@@ -90,7 +91,7 @@ func s3Secret(namespace string, accessKey string, secretKey string) *corev1.Secr
 			Name:      "kotsadm-minio",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Data: map[string][]byte{
@@ -112,7 +113,7 @@ func apiEncryptionKeySecret(namespace string, key string) *corev1.Secret {
 			Name:      "kotsadm-encryption",
 			Namespace: namespace,
 			Labels: map[string]string{
-				KotsadmKey: KotsadmLabelValue,
+				types.KotsadmKey: types.KotsadmLabelValue,
 			},
 		},
 		Data: map[string][]byte{
diff --git a/pkg/kotsadm/types/constants.go b/pkg/kotsadm/types/constants.go
new file mode 100644
index 000000000..6f05b7967
--- /dev/null
+++ b/pkg/kotsadm/types/constants.go
@@ -0,0 +1,4 @@
+package types
+
+const KotsadmKey = "kots.io/kotsadm"
+const KotsadmLabelValue = "true"
diff --git a/pkg/upload/upload.go b/pkg/upload/upload.go
index 8d5a90981..5e0134662 100644
--- a/pkg/upload/upload.go
+++ b/pkg/upload/upload.go
@@ -16,6 +16,7 @@ import (
 	"github.com/manifoldco/promptui"
 	"github.com/pkg/errors"
 	kotsscheme "github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme"
+	"github.com/replicatedhq/kots/pkg/auth"
 	"github.com/replicatedhq/kots/pkg/docker/registry"
 	"github.com/replicatedhq/kots/pkg/logger"
 	"github.com/replicatedhq/kots/pkg/util"
@@ -168,7 +169,7 @@ func createUploadRequest(path string, uploadOptions UploadOptions, uri string) (
 			"slug":         uploadOptions.ExistingAppSlug,
 			"versionLabel": uploadOptions.versionLabel,
 			"updateCursor": uploadOptions.updateCursor,
-			// Intnetionally not including registry info here.  Updating settings should be its own thing.
+			// Intentionally not including registry info here.  Updating settings should be its own thing.
 		}
 		b, err := json.Marshal(metadata)
 		if err != nil {
@@ -217,11 +218,17 @@ func createUploadRequest(path string, uploadOptions UploadOptions, uri string) (
 		return nil, errors.Wrap(err, "failed to close writer")
 	}
 
+	authSlug, err := auth.GetOrCreateAuthSlug(uploadOptions.Namespace)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get auth slug")
+	}
+
 	req, err := http.NewRequest(method, uri, body)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to create new request")
 	}
 
+	req.Header.Set("Authorization", authSlug)
 	req.Header.Set("Content-Type", writer.FormDataContentType())
 	return req, nil
 }
diff --git a/pkg/upload/upload_license.go b/pkg/upload/upload_license.go
index 0b7a98969..e60a6ba5c 100644
--- a/pkg/upload/upload_license.go
+++ b/pkg/upload/upload_license.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/auth"
 	"github.com/replicatedhq/kots/pkg/k8sutil"
 	"github.com/replicatedhq/kots/pkg/logger"
 )
@@ -111,11 +112,17 @@ func createUploadLicenseRequest(license string, uploadLicenseOptions UploadLicen
 		return nil, errors.Wrap(err, "failed to marshal json")
 	}
 
+	authSlug, err := auth.GetOrCreateAuthSlug(uploadLicenseOptions.Namespace)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get kotsadm auth slug")
+	}
+
 	req, err := http.NewRequest("POST", uri, bytes.NewBuffer(b))
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to create new request")
 	}
 
 	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Authorization", authSlug)
 	return req, nil
 }
diff --git a/pkg/util/util.go b/pkg/util/util.go
index 123d6f7c0..5d61033f8 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -2,10 +2,18 @@ package util
 
 import (
 	"bytes"
+	crand "crypto/rand"
 	"fmt"
+	"math/big"
+	rand "math/rand"
 	"net/url"
+	"time"
 )
 
+func init() {
+	rand.Seed(time.Now().UnixNano())
+}
+
 func IsURL(str string) bool {
 	_, err := url.ParseRequestURI(str)
 	if err != nil {
@@ -62,6 +70,57 @@ func IntPointer(x int) *int64 {
 	return &xout
 }
 
+var passwordLetters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+
+// generates a [0-9a-zA-Z] password of the specified length
+func GenPassword(length int) string {
+	lettersLen := big.NewInt(int64(len(passwordLetters)))
+
+	var outRunes []rune
+	for i := 0; i < length; i++ {
+		cryptoRandNum, err := crand.Int(crand.Reader, lettersLen)
+		var randNum int64
+		if err != nil {
+			// print error message and fallback to math.rand's number generator
+			fmt.Printf("failed to get cryptographically random number: %v\n", err)
+			randNum = int64(rand.Intn(len(passwordLetters)))
+		} else {
+			randNum = cryptoRandNum.Int64()
+		}
+
+		outRunes = append(outRunes, passwordLetters[randNum])
+	}
+	return string(outRunes)
+}
+
+// CompareStringArrays returns true if all elements in arr1 are present in arr2 and the other way around.
+// it does not check for equal counts of duplicates, or for ordering.
+func CompareStringArrays(arr1, arr2 []string) bool {
+	for _, str1 := range arr1 {
+		foundMatch := false
+		for _, str2 := range arr2 {
+			if str1 == str2 {
+				foundMatch = true
+			}
+		}
+		if !foundMatch {
+			return false
+		}
+	}
+	for _, str2 := range arr2 {
+		foundMatch := false
+		for _, str1 := range arr1 {
+			if str1 == str2 {
+				foundMatch = true
+			}
+		}
+		if !foundMatch {
+			return false
+		}
+	}
+	return true
+}
+
 type ActionableError struct {
 	Message string
 }


Test patch:
diff --git a/integration/upload/upload_test.go b/integration/upload/upload_test.go
index 1f3c48334..21f9be7b7 100644
--- a/integration/upload/upload_test.go
+++ b/integration/upload/upload_test.go
@@ -5,7 +5,9 @@ import (
 	"path"
 	"testing"
 
+	"github.com/replicatedhq/kots/pkg/auth"
 	kotsupload "github.com/replicatedhq/kots/pkg/upload"
+	"github.com/replicatedhq/kots/pkg/util"
 	"github.com/stretchr/testify/require"
 )
 
@@ -33,6 +35,7 @@ func Test_Upload(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.path, func(t *testing.T) {
 			req := require.New(t)
+			auth.SetAuthSlugCache("Kots " + util.GenPassword(32))
 
 			uploadOptions := kotsupload.UploadOptions{
 				Namespace:       test.namespace,
diff --git a/pkg/k8sutil/updaterole_test.go b/pkg/k8sutil/updaterole_test.go
new file mode 100644
index 000000000..0ed064be4
--- /dev/null
+++ b/pkg/k8sutil/updaterole_test.go
@@ -0,0 +1,152 @@
+package k8sutil
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	rbacv1 "k8s.io/api/rbac/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestUpdateRole(t *testing.T) {
+	tests := []struct {
+		name          string
+		existingRole  *rbacv1.Role
+		desiredRole   *rbacv1.Role
+		expectedRules []rbacv1.PolicyRule
+	}{
+		{
+			name: "apiRuleUpdate", // based on the api rbac policy
+			existingRole: &rbacv1.Role{
+				Rules: []rbacv1.PolicyRule{
+					{
+						APIGroups:     []string{""},
+						Resources:     []string{"configmaps"},
+						ResourceNames: []string{"kotsadm-application-metadata", "kotsadm-gitops"},
+						Verbs:         metav1.Verbs{"get", "delete", "update"},
+					},
+					{
+						APIGroups: []string{""},
+						Resources: []string{"configmaps"},
+						Verbs:     metav1.Verbs{"create"},
+					},
+					{
+						APIGroups:     []string{""},
+						Resources:     []string{"secrets"},
+						ResourceNames: []string{"kotsadm-encryption", "kotsadm-gitops"},
+						Verbs:         metav1.Verbs{"get", "update"},
+					},
+					{
+						APIGroups: []string{""},
+						Resources: []string{"secrets"},
+						Verbs:     metav1.Verbs{"create"},
+					},
+					{
+						APIGroups: []string{"blah"},
+						Resources: []string{"blah"},
+						Verbs:     metav1.Verbs{"blah"}, // this rule is not in the desired set, but should be preserved
+					},
+				},
+			},
+			desiredRole: &rbacv1.Role{
+				// creation cannot be restricted by name
+				Rules: []rbacv1.PolicyRule{
+					{
+						APIGroups:     []string{""},
+						Resources:     []string{"configmaps"},
+						ResourceNames: []string{"kotsadm-application-metadata", "kotsadm-gitops"},
+						Verbs:         metav1.Verbs{"get", "delete", "update"},
+					},
+					{
+						APIGroups: []string{""},
+						Resources: []string{"configmaps"},
+						Verbs:     metav1.Verbs{"create"},
+					},
+					{
+						APIGroups:     []string{""},
+						Resources:     []string{"secrets"},
+						ResourceNames: []string{"kotsadm-encryption", "kotsadm-gitops", "anotherName"}, // added resource name
+						Verbs:         metav1.Verbs{"get", "update"},
+					},
+					{
+						APIGroups: []string{""},
+						Resources: []string{"secrets"},
+						Verbs:     metav1.Verbs{"create"},
+					},
+					{
+						APIGroups: []string{"v1"}, // added api group
+						Resources: []string{"secrets"},
+						Verbs:     metav1.Verbs{"create"},
+					},
+					{
+						APIGroups: []string{""},
+						Resources: []string{"secrets", "test"}, // added resource type group
+						Verbs:     metav1.Verbs{"create"},
+					},
+					{
+						APIGroups: []string{""},
+						Resources: []string{"secrets"},
+						Verbs:     metav1.Verbs{"create", "test"}, // added verb
+					},
+				},
+			},
+			expectedRules: []rbacv1.PolicyRule{
+				{
+					APIGroups:     []string{""},
+					Resources:     []string{"configmaps"},
+					ResourceNames: []string{"kotsadm-application-metadata", "kotsadm-gitops"},
+					Verbs:         metav1.Verbs{"get", "delete", "update"},
+				},
+				{
+					APIGroups: []string{""},
+					Resources: []string{"configmaps"},
+					Verbs:     metav1.Verbs{"create"},
+				},
+				{
+					APIGroups:     []string{""},
+					Resources:     []string{"secrets"},
+					ResourceNames: []string{"kotsadm-encryption", "kotsadm-gitops"},
+					Verbs:         metav1.Verbs{"get", "update"},
+				},
+				{
+					APIGroups: []string{""},
+					Resources: []string{"secrets"},
+					Verbs:     metav1.Verbs{"create"},
+				},
+				{
+					APIGroups: []string{"blah"},
+					Resources: []string{"blah"},
+					Verbs:     metav1.Verbs{"blah"}, // this rule is not in the desired set, but should be preserved
+				},
+				{
+					APIGroups:     []string{""},
+					Resources:     []string{"secrets"},
+					ResourceNames: []string{"kotsadm-encryption", "kotsadm-gitops", "anotherName"}, // added resource name
+					Verbs:         metav1.Verbs{"get", "update"},
+				},
+				{
+					APIGroups: []string{"v1"}, // added api group
+					Resources: []string{"secrets"},
+					Verbs:     metav1.Verbs{"create"},
+				},
+				{
+					APIGroups: []string{""},
+					Resources: []string{"secrets", "test"}, // added resource type group
+					Verbs:     metav1.Verbs{"create"},
+				},
+				{
+					APIGroups: []string{""},
+					Resources: []string{"secrets"},
+					Verbs:     metav1.Verbs{"create", "test"}, // added verb
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := require.New(t)
+			UpdateRole(tt.existingRole, tt.desiredRole)
+			req.ElementsMatch(tt.existingRole.Rules, tt.expectedRules)
+		})
+	}
+}
diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go
index f271adb19..660b7764b 100644
--- a/pkg/util/util_test.go
+++ b/pkg/util/util_test.go
@@ -116,3 +116,70 @@ func TestIntPointer(t *testing.T) {
 		})
 	}
 }
+
+func TestGenPassword(t *testing.T) {
+	tests := []struct {
+		name   string
+		length int
+	}{
+		{
+			name:   "8",
+			length: 8,
+		},
+		{
+			name:   "32",
+			length: 32,
+		},
+		{
+			name:   "0",
+			length: 0,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := require.New(t)
+			got := GenPassword(tt.length)
+			req.Len(got, tt.length)
+		})
+	}
+}
+
+func TestCompareStringArrays(t *testing.T) {
+	tests := []struct {
+		name string
+		arr1 []string
+		arr2 []string
+		want bool
+	}{
+		{
+			name: "empty arrays",
+			arr1: []string{},
+			arr2: []string{},
+			want: true,
+		},
+		{
+			name: "one empty array",
+			arr1: []string{},
+			arr2: []string{"element"},
+			want: false,
+		},
+		{
+			name: "superset",
+			arr1: []string{"different element", "element"},
+			arr2: []string{"element"},
+			want: false,
+		},
+		{
+			name: "duplicates",
+			arr1: []string{"different element", "element"},
+			arr2: []string{"element", "element", "different element"},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := require.New(t)
+			req.Equal(CompareStringArrays(tt.arr1, tt.arr2), tt.want)
+		})
+	}
+}