Просмотр исходного кода

Merge branch 'beta.3.key-rotation'

mergin
Alexander Belanger 5 лет назад
Родитель
Сommit
db27534b65

+ 34 - 15
cmd/migrate/keyrotate/helpers_test.go

@@ -162,10 +162,15 @@ func initKubeIntegration(tester *tester, t *testing.T) {
 	}
 
 	ki := &ints.KubeIntegration{
-		Mechanism:  ints.KubeLocal,
-		ProjectID:  tester.initProjects[0].ID,
-		UserID:     tester.initUsers[0].ID,
-		Kubeconfig: []byte("current-context: testing\n"),
+		Mechanism:             ints.KubeLocal,
+		ProjectID:             tester.initProjects[0].ID,
+		UserID:                tester.initUsers[0].ID,
+		Kubeconfig:            []byte("current-context: testing\n"),
+		ClientCertificateData: []byte("clientcertdata"),
+		ClientKeyData:         []byte("clientkeydata"),
+		Token:                 []byte("token"),
+		Username:              []byte("username"),
+		Password:              []byte("password"),
 	}
 
 	ki, err := tester.repo.KubeIntegration.CreateKubeIntegration(ki)
@@ -216,14 +221,15 @@ func initOIDCIntegration(tester *tester, t *testing.T) {
 	}
 
 	oidc := &ints.OIDCIntegration{
-		Client:       ints.OIDCKube,
-		ProjectID:    tester.initProjects[0].ID,
-		UserID:       tester.initUsers[0].ID,
-		IssuerURL:    []byte("https://oidc.example.com"),
-		ClientID:     []byte("exampleclientid"),
-		ClientSecret: []byte("exampleclientsecret"),
-		IDToken:      []byte("idtoken"),
-		RefreshToken: []byte("refreshtoken"),
+		Client:                   ints.OIDCKube,
+		ProjectID:                tester.initProjects[0].ID,
+		UserID:                   tester.initUsers[0].ID,
+		IssuerURL:                []byte("https://oidc.example.com"),
+		ClientID:                 []byte("exampleclientid"),
+		ClientSecret:             []byte("exampleclientsecret"),
+		CertificateAuthorityData: []byte("cadata"),
+		IDToken:                  []byte("idtoken"),
+		RefreshToken:             []byte("refreshtoken"),
 	}
 
 	oidc, err := tester.repo.OIDCIntegration.CreateOIDCIntegration(oidc)
@@ -419,6 +425,12 @@ func initRegistry(tester *tester, t *testing.T) {
 	reg := &models.Registry{
 		ProjectID: tester.initProjects[0].ID,
 		Name:      "registry-test",
+		TokenCache: ints.RegTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
+		},
 	}
 
 	reg, err := tester.repo.Registry.CreateRegistry(reg)
@@ -441,6 +453,12 @@ func initHelmRepo(tester *tester, t *testing.T) {
 		Name:      "helm-repo-test",
 		RepoURL:   "https://example-repo.com",
 		ProjectID: tester.initProjects[0].Model.ID,
+		TokenCache: ints.HelmRepoTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
+		},
 	}
 
 	hr, err := tester.repo.HelmRepo.CreateHelmRepo(hr)
@@ -460,9 +478,10 @@ func initInfra(tester *tester, t *testing.T) {
 	}
 
 	infra := &models.Infra{
-		Kind:      models.InfraECR,
-		ProjectID: tester.initProjects[0].Model.ID,
-		Status:    models.StatusCreated,
+		Kind:        models.InfraECR,
+		ProjectID:   tester.initProjects[0].Model.ID,
+		Status:      models.StatusCreated,
+		LastApplied: []byte("testing"),
 	}
 
 	infra, err := tester.repo.Infra.CreateInfra(infra)

+ 535 - 0
cmd/migrate/keyrotate/rotate.go

@@ -1,7 +1,10 @@
 package keyrotate
 
 import (
+	"fmt"
+
 	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	gorm "github.com/porter-dev/porter/internal/repository/gorm"
 
 	_gorm "gorm.io/gorm"
@@ -17,6 +20,66 @@ func Rotate(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		return err
 	}
 
+	err = rotateClusterCandidateModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateRegistryModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateHelmRepoModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateInfraModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateKubeIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateBasicIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateOIDCIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateOAuthIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateGCPIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
+	err = rotateAWSIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -62,5 +125,477 @@ func rotateClusterModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
+	fmt.Printf("rotated %d clusters", count)
+
+	return nil
+}
+
+func rotateClusterCandidateModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&models.ClusterCandidate{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewClusterRepository(db, oldKey).(*gorm.ClusterRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		ccs := []*models.ClusterCandidate{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&ccs).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, cc := range ccs {
+			err := repo.DecryptClusterCandidateData(cc, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, cc := range ccs {
+			err := repo.EncryptClusterCandidateData(cc, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(cc).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d cluster candidates", count)
+
+	return nil
+}
+
+func rotateRegistryModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&models.Registry{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// registry-scoped repository
+	repo := gorm.NewRegistryRepository(db, oldKey).(*gorm.RegistryRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		regs := []*models.Registry{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Preload("TokenCache").Find(&regs).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, reg := range regs {
+			err := repo.DecryptRegistryData(reg, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, reg := range regs {
+			err := repo.EncryptRegistryData(reg, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(reg).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d registries", count)
+
+	return nil
+}
+
+func rotateHelmRepoModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&models.HelmRepo{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// helm repo-scoped repository
+	repo := gorm.NewHelmRepoRepository(db, oldKey).(*gorm.HelmRepoRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		hrs := []*models.HelmRepo{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Preload("TokenCache").Find(&hrs).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, hr := range hrs {
+			err := repo.DecryptHelmRepoData(hr, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, hr := range hrs {
+			err := repo.EncryptHelmRepoData(hr, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(hr).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d helm repos", count)
+
+	return nil
+}
+
+func rotateInfraModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&models.Infra{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// helm repo-scoped repository
+	repo := gorm.NewInfraRepository(db, oldKey).(*gorm.InfraRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		infras := []*models.Infra{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&infras).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, infra := range infras {
+			err := repo.DecryptInfraData(infra, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, infra := range infras {
+			err := repo.EncryptInfraData(infra, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(infra).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d infras", count)
+
+	return nil
+}
+
+func rotateKubeIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.KubeIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewKubeIntegrationRepository(db, oldKey).(*gorm.KubeIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		kis := []*ints.KubeIntegration{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&kis).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, ki := range kis {
+			err := repo.DecryptKubeIntegrationData(ki, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, ki := range kis {
+			err := repo.EncryptKubeIntegrationData(ki, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(ki).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d kube integrations", count)
+
+	return nil
+}
+
+func rotateBasicIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.BasicIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewBasicIntegrationRepository(db, oldKey).(*gorm.BasicIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		basics := []*ints.BasicIntegration{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&basics).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, basic := range basics {
+			err := repo.DecryptBasicIntegrationData(basic, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, basic := range basics {
+			err := repo.EncryptBasicIntegrationData(basic, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(basic).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d basic integrations", count)
+
+	return nil
+}
+
+func rotateOIDCIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.OIDCIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewOIDCIntegrationRepository(db, oldKey).(*gorm.OIDCIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		oidcs := []*ints.OIDCIntegration{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&oidcs).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, oidc := range oidcs {
+			err := repo.DecryptOIDCIntegrationData(oidc, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, oidc := range oidcs {
+			err := repo.EncryptOIDCIntegrationData(oidc, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(oidc).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d oidc integrations", count)
+
+	return nil
+}
+
+func rotateOAuthIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.OAuthIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewOAuthIntegrationRepository(db, oldKey).(*gorm.OAuthIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		oauths := []*ints.OAuthIntegration{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&oauths).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, oauth := range oauths {
+			err := repo.DecryptOAuthIntegrationData(oauth, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, oauth := range oauths {
+			err := repo.EncryptOAuthIntegrationData(oauth, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(oauth).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d oauth integrations", count)
+
+	return nil
+}
+
+func rotateGCPIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.GCPIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewGCPIntegrationRepository(db, oldKey).(*gorm.GCPIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		gcps := []*ints.GCPIntegration{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&gcps).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, gcp := range gcps {
+			err := repo.DecryptGCPIntegrationData(gcp, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, gcp := range gcps {
+			err := repo.EncryptGCPIntegrationData(gcp, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(gcp).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d gcp integrations", count)
+
+	return nil
+}
+
+func rotateAWSIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.AWSIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewAWSIntegrationRepository(db, oldKey).(*gorm.AWSIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		awss := []*ints.AWSIntegration{}
+
+		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&awss).Error; err != nil {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, aws := range awss {
+			err := repo.DecryptAWSIntegrationData(aws, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, aws := range awss {
+			err := repo.EncryptAWSIntegrationData(aws, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(aws).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d aws integrations", count)
+
 	return nil
 }

+ 550 - 4
cmd/migrate/keyrotate/rotate_test.go

@@ -1,11 +1,11 @@
 package keyrotate_test
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
 	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	gorm "github.com/porter-dev/porter/internal/repository/gorm"
 )
 
@@ -22,7 +22,7 @@ func TestClusterModelRotation(t *testing.T) {
 
 	setupTestEnv(tester, t)
 
-	for i := 0; i < 1; i++ {
+	for i := 0; i < 128; i++ {
 		initCluster(tester, t)
 	}
 
@@ -45,8 +45,6 @@ func TestClusterModelRotation(t *testing.T) {
 
 	// decrypt with the old key
 	for _, c := range clusters {
-		fmt.Println("GOT TOKEN", string(c.TokenCache.Token))
-
 		cluster, err := repo.ReadCluster(c.ID)
 
 		if err != nil {
@@ -62,3 +60,551 @@ func TestClusterModelRotation(t *testing.T) {
 		}
 	}
 }
+
+func TestClusterCandidateModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_cluster_candidate_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 256; i++ {
+		initClusterCandidate(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all clusters decoded properly
+	repo := gorm.NewClusterRepository(tester.DB, &newKey).(*gorm.ClusterRepository)
+
+	ccs := []*models.ClusterCandidate{}
+
+	if err := tester.DB.Find(&ccs).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, c := range ccs {
+		cc, err := repo.ReadClusterCandidate(c.ID)
+
+		if err != nil {
+			t.Fatalf("error reading cluster: %v\n", err)
+		}
+
+		if string(cc.AWSClusterIDGuess) != "example-cluster-0" {
+			t.Errorf("%s\n", string(cc.AWSClusterIDGuess))
+		}
+
+		if string(cc.Kubeconfig) != "current-context: testing\n" {
+			t.Errorf("%s\n", string(cc.Kubeconfig))
+		}
+	}
+}
+
+func TestRegistryModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_registry_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 144; i++ {
+		initRegistry(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all registries decoded properly
+	repo := gorm.NewRegistryRepository(tester.DB, &newKey).(*gorm.RegistryRepository)
+
+	regs := []*models.Registry{}
+
+	if err := tester.DB.Preload("TokenCache").Find(&regs).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, r := range regs {
+		registry, err := repo.ReadRegistry(r.ID)
+
+		if err != nil {
+			t.Fatalf("error reading registry: %v\n", err)
+		}
+
+		if string(registry.TokenCache.Token) != "token-1" {
+			t.Errorf("%s\n", string(registry.TokenCache.Token))
+		}
+	}
+}
+
+func TestHelmRepoModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_hr_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 169; i++ {
+		initHelmRepo(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all helm repos decoded properly
+	repo := gorm.NewHelmRepoRepository(tester.DB, &newKey).(*gorm.HelmRepoRepository)
+
+	hrs := []*models.HelmRepo{}
+
+	if err := tester.DB.Preload("TokenCache").Find(&hrs).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, h := range hrs {
+		hr, err := repo.ReadHelmRepo(h.ID)
+
+		if err != nil {
+			t.Fatalf("error reading helm repo: %v\n", err)
+		}
+
+		if string(hr.TokenCache.Token) != "token-1" {
+			t.Errorf("%s\n", string(hr.TokenCache.Token))
+		}
+	}
+}
+
+func TestInfraModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_infra_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initInfra(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all infras decoded properly
+	repo := gorm.NewInfraRepository(tester.DB, &newKey).(*gorm.InfraRepository)
+
+	infras := []*models.Infra{}
+
+	if err := tester.DB.Find(&infras).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, i := range infras {
+		infra, err := repo.ReadInfra(i.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(infra.LastApplied) != "testing" {
+			t.Errorf("%s\n", string(infra.LastApplied))
+		}
+	}
+}
+
+func TestKubeIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_ki_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initKubeIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all kis decoded properly
+	repo := gorm.NewKubeIntegrationRepository(tester.DB, &newKey).(*gorm.KubeIntegrationRepository)
+
+	kis := []*ints.KubeIntegration{}
+
+	if err := tester.DB.Find(&kis).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range kis {
+		ki, err := repo.ReadKubeIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(ki.Kubeconfig) != "current-context: testing\n" {
+			t.Errorf("%s\n", string(ki.Kubeconfig))
+		}
+
+		if string(ki.ClientCertificateData) != "clientcertdata" {
+			t.Errorf("%s\n", string(ki.ClientCertificateData))
+		}
+
+		if string(ki.ClientKeyData) != "clientkeydata" {
+			t.Errorf("%s\n", string(ki.ClientKeyData))
+		}
+
+		if string(ki.Token) != "token" {
+			t.Errorf("%s\n", string(ki.Token))
+		}
+
+		if string(ki.Username) != "username" {
+			t.Errorf("%s\n", string(ki.Username))
+		}
+
+		if string(ki.Password) != "password" {
+			t.Errorf("%s\n", string(ki.Password))
+		}
+	}
+}
+
+func TestBasicIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_basic_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initBasicIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all basics decoded properly
+	repo := gorm.NewBasicIntegrationRepository(tester.DB, &newKey).(*gorm.BasicIntegrationRepository)
+
+	basics := []*ints.BasicIntegration{}
+
+	if err := tester.DB.Find(&basics).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range basics {
+		basic, err := repo.ReadBasicIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(basic.Username) != "username" {
+			t.Errorf("%s\n", string(basic.Username))
+		}
+
+		if string(basic.Password) != "password" {
+			t.Errorf("%s\n", string(basic.Password))
+		}
+	}
+}
+
+func TestOIDCIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_oidc_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initOIDCIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all oidcs decoded properly
+	repo := gorm.NewOIDCIntegrationRepository(tester.DB, &newKey).(*gorm.OIDCIntegrationRepository)
+
+	oidcs := []*ints.OIDCIntegration{}
+
+	if err := tester.DB.Find(&oidcs).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range oidcs {
+		oidc, err := repo.ReadOIDCIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(oidc.IssuerURL) != "https://oidc.example.com" {
+			t.Errorf("%s\n", string(oidc.IssuerURL))
+		}
+
+		if string(oidc.ClientID) != "exampleclientid" {
+			t.Errorf("%s\n", string(oidc.ClientID))
+		}
+
+		if string(oidc.ClientSecret) != "exampleclientsecret" {
+			t.Errorf("%s\n", string(oidc.ClientSecret))
+		}
+
+		if string(oidc.CertificateAuthorityData) != "cadata" {
+			t.Errorf("%s\n", string(oidc.CertificateAuthorityData))
+		}
+
+		if string(oidc.IDToken) != "idtoken" {
+			t.Errorf("%s\n", string(oidc.IDToken))
+		}
+
+		if string(oidc.RefreshToken) != "refreshtoken" {
+			t.Errorf("%s\n", string(oidc.RefreshToken))
+		}
+	}
+}
+
+func TestOAuthIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_oauth_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initOAuthIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all oauths decoded properly
+	repo := gorm.NewOAuthIntegrationRepository(tester.DB, &newKey).(*gorm.OAuthIntegrationRepository)
+
+	oauths := []*ints.OAuthIntegration{}
+
+	if err := tester.DB.Find(&oauths).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range oauths {
+		oauth, err := repo.ReadOAuthIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(oauth.ClientID) != "exampleclientid" {
+			t.Errorf("%s\n", string(oauth.ClientID))
+		}
+
+		if string(oauth.AccessToken) != "idtoken" {
+			t.Errorf("%s\n", string(oauth.AccessToken))
+		}
+
+		if string(oauth.RefreshToken) != "refreshtoken" {
+			t.Errorf("%s\n", string(oauth.RefreshToken))
+		}
+	}
+}
+
+func TestGCPIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_gcp_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initGCPIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all gcps decoded properly
+	repo := gorm.NewGCPIntegrationRepository(tester.DB, &newKey).(*gorm.GCPIntegrationRepository)
+
+	gcps := []*ints.GCPIntegration{}
+
+	if err := tester.DB.Find(&gcps).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range gcps {
+		gcp, err := repo.ReadGCPIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(gcp.GCPKeyData) != "{\"test\":\"key\"}" {
+			t.Errorf("%s\n", string(gcp.GCPKeyData))
+		}
+	}
+}
+
+func TestAWSIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_aws_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initAWSIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all awss decoded properly
+	repo := gorm.NewAWSIntegrationRepository(tester.DB, &newKey).(*gorm.AWSIntegrationRepository)
+
+	awss := []*ints.AWSIntegration{}
+
+	if err := tester.DB.Find(&awss).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range awss {
+		aws, err := repo.ReadAWSIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(aws.AWSClusterID) != "example-cluster-0" {
+			t.Errorf("%s\n", string(aws.AWSClusterID))
+		}
+
+		if string(aws.AWSAccessKeyID) != "accesskey" {
+			t.Errorf("%s\n", string(aws.AWSAccessKeyID))
+		}
+
+		if string(aws.AWSSecretAccessKey) != "secret" {
+			t.Errorf("%s\n", string(aws.AWSSecretAccessKey))
+		}
+
+		if string(aws.AWSSessionToken) != "optional" {
+			t.Errorf("%s\n", string(aws.AWSSessionToken))
+		}
+	}
+}

+ 0 - 1
cmd/migrate/keyrotate/temp.go

@@ -1 +0,0 @@
-package keyrotate

+ 35 - 0
cmd/migrate/main.go

@@ -2,6 +2,9 @@ package main
 
 import (
 	"fmt"
+	"log"
+
+	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
 
 	adapter "github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/config"
@@ -9,6 +12,8 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 
 	ints "github.com/porter-dev/porter/internal/models/integrations"
+
+	"github.com/joeshaw/envdecode"
 )
 
 func main() {
@@ -54,4 +59,34 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
+
+	if shouldRotate, oldKeyStr, newKeyStr := shouldKeyRotate(); shouldRotate {
+		oldKey := [32]byte{}
+		newKey := [32]byte{}
+
+		copy(oldKey[:], []byte(oldKeyStr))
+		copy(newKey[:], []byte(newKeyStr))
+
+		err := keyrotate.Rotate(db, &oldKey, &newKey)
+
+		if err != nil {
+			panic(err)
+		}
+	}
+}
+
+type RotateConf struct {
+	OldEncryptionKey string `env:"OLD_ENCRYPTION_KEY"`
+	NewEncryptionKey string `env:"NEW_ENCRYPTION_KEY"`
+}
+
+func shouldKeyRotate() (bool, string, string) {
+	var c RotateConf
+
+	if err := envdecode.StrictDecode(&c); err != nil {
+		log.Fatalf("Failed to decode migration conf: %s", err)
+		return false, "", ""
+	}
+
+	return c.OldEncryptionKey != "" && c.NewEncryptionKey != "", c.OldEncryptionKey, c.NewEncryptionKey
 }

+ 10 - 3
internal/auth/sessionstore/sessionstore.go

@@ -111,8 +111,10 @@ func (store *PGStore) save(session *sessions.Session) error {
 
 // NewStore takes an initialized db and session key pairs to create a session-store in postgres db.
 func NewStore(repo *repository.Repository, conf config.ServerConf) (*PGStore, error) {
-	keyPairs := [][]byte{
-		conf.CookieSecret,
+	keyPairs := [][]byte{}
+
+	for _, key := range conf.CookieSecrets {
+		keyPairs = append(keyPairs, []byte(key))
 	}
 
 	dbStore := &PGStore{
@@ -129,9 +131,14 @@ func NewStore(repo *repository.Repository, conf config.ServerConf) (*PGStore, er
 
 // NewFilesystemStore takes session key pairs to create a session-store in the local fs without using a db.
 func NewFilesystemStore(conf config.ServerConf) *sessions.FilesystemStore {
+	keyPairs := [][]byte{}
+
+	for _, key := range conf.CookieSecrets {
+		keyPairs = append(keyPairs, []byte(key))
+	}
 
 	// Defaults to os.TempDir() when first argument (path) isn't specified.
-	store := sessions.NewFilesystemStore("", conf.CookieSecret)
+	store := sessions.NewFilesystemStore("", keyPairs...)
 
 	return store
 }

+ 4 - 2
internal/auth/sessionstore/sessionstore_test.go

@@ -34,7 +34,7 @@ func TestPGStore(t *testing.T) {
 	repo := test.NewRepository(true)
 
 	ss, err := sessionstore.NewStore(repo, config.ServerConf{
-		CookieSecret: []byte("secret"),
+		CookieSecrets: []string{"secret"},
 	})
 
 	if err != nil {
@@ -128,13 +128,15 @@ func TestPGStore(t *testing.T) {
 	if err = ss.Save(req, headerOnlyResponseWriter(m), session); err != nil {
 		t.Fatal("Failed to save session:", err.Error())
 	}
+
+	t.Errorf("")
 }
 
 func TestSessionOptionsAreUniquePerSession(t *testing.T) {
 	repo := test.NewRepository(true)
 
 	ss, err := sessionstore.NewStore(repo, config.ServerConf{
-		CookieSecret: []byte("secret"),
+		CookieSecrets: []string{"secret"},
 	})
 
 	if err != nil {

+ 2 - 2
internal/config/config.go

@@ -22,7 +22,7 @@ type ServerConf struct {
 	Port                 int           `env:"SERVER_PORT,default=8080"`
 	StaticFilePath       string        `env:"STATIC_FILE_PATH,default=/porter/static"`
 	CookieName           string        `env:"COOKIE_NAME,default=porter"`
-	CookieSecret         []byte        `env:"COOKIE_SECRET,default=secret"`
+	CookieSecrets        []string      `env:"COOKIE_SECRETS,default=hashkey;blockkey"`
 	TokenGeneratorSecret string        `env:"TOKEN_GENERATOR_SECRET,default=secret"`
 	TimeoutRead          time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
 	TimeoutWrite         time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
@@ -66,7 +66,7 @@ func FromEnv() *Conf {
 	var c Conf
 
 	if err := envdecode.StrictDecode(&c); err != nil {
-		log.Fatalf("Failed to decode: %s", err)
+		log.Fatalf("Failed to decode server conf: %s", err)
 	}
 
 	return &c

+ 0 - 4
internal/repository/gorm/infra.go

@@ -1,8 +1,6 @@
 package gorm
 
 import (
-	"fmt"
-
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
@@ -61,8 +59,6 @@ func (repo *InfraRepository) ReadInfra(id uint) (*models.Infra, error) {
 		return nil, err
 	}
 
-	fmt.Println("INNFRA LAST APPLIED", string(infra.LastApplied))
-
 	err := repo.DecryptInfraData(infra, repo.key)
 
 	if err != nil {