...

Source file src/edge-infra.dev/pkg/edge/controllers/bannerctl/cert_management_test.go

Documentation: edge-infra.dev/pkg/edge/controllers/bannerctl

     1  package bannerctl
     2  
     3  import (
     4  	"context"
     5  	"crypto/x509"
     6  	"crypto/x509/pkix"
     7  	"encoding/base64"
     8  	"encoding/pem"
     9  	"fmt"
    10  	"math/big"
    11  	"time"
    12  
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  
    15  	gcpProject "edge-infra.dev/pkg/lib/gcp/project"
    16  	"edge-infra.dev/pkg/lib/uuid"
    17  
    18  	ctrl "sigs.k8s.io/controller-runtime"
    19  
    20  	bannerAPI "edge-infra.dev/pkg/edge/apis/banner/v1alpha1"
    21  	"edge-infra.dev/pkg/edge/controllers/util/edgedb"
    22  	"edge-infra.dev/pkg/edge/gcpinfra"
    23  )
    24  
    25  func (s *Suite) TestReconcileCAManagement() {
    26  	bannerGUID := uuid.New().UUID
    27  	generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix))))
    28  	banner := &bannerAPI.Banner{
    29  		ObjectMeta: metav1.ObjectMeta{
    30  			Name: bannerGUID,
    31  		},
    32  		Spec: bannerAPI.BannerSpec{
    33  			DisplayName: "banner-for-ca-stuff",
    34  			GCP: bannerAPI.GCPConfig{
    35  				ProjectID: generatedProjID,
    36  			},
    37  			BSL: bannerAPI.BSLConfig{
    38  				EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{
    39  					ID: uuid.New().UUID,
    40  				},
    41  				Organization: bannerAPI.BSLOrganization{
    42  					Name: "test-org-for-ca-stuff",
    43  				},
    44  			},
    45  		},
    46  	}
    47  
    48  	s.createBanner(banner)
    49  	defer s.deleteBanner(banner)
    50  
    51  	// Create a new BannerReconciler
    52  	r := &BannerReconciler{
    53  		Log:           ctrl.Log.WithName("controllers").WithName("Banner"),
    54  		EdgeDB:        &edgedb.EdgeDB{DB: s.db},
    55  		SecretManager: s.sMgr,
    56  	}
    57  
    58  	err := r.reconcileCerts(s.ctx, banner)
    59  	s.Require().Nil(err)
    60  }
    61  
    62  func (s *Suite) TestCreateCAPool() {
    63  	bannerGUID := uuid.New().UUID
    64  	generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix))))
    65  	banner := &bannerAPI.Banner{
    66  		ObjectMeta: metav1.ObjectMeta{
    67  			Name: bannerGUID,
    68  		},
    69  		Spec: bannerAPI.BannerSpec{
    70  			DisplayName: "banner-for-ca-stuff-2",
    71  			GCP: bannerAPI.GCPConfig{
    72  				ProjectID: generatedProjID,
    73  			},
    74  			BSL: bannerAPI.BSLConfig{
    75  				EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{
    76  					ID: uuid.New().UUID,
    77  				},
    78  				Organization: bannerAPI.BSLOrganization{
    79  					Name: "test-org-for-ca-stuff-2",
    80  				},
    81  			},
    82  		},
    83  	}
    84  
    85  	s.createBanner(banner)
    86  	defer s.deleteBanner(banner)
    87  
    88  	// Create a new BannerReconciler
    89  	r := &BannerReconciler{
    90  		Log:           ctrl.Log.WithName("controllers").WithName("Banner"),
    91  		EdgeDB:        &edgedb.EdgeDB{DB: s.db},
    92  		SecretManager: s.sMgr,
    93  	}
    94  
    95  	poolID, err := r.reconcileCAPool(context.TODO(), bannerGUID)
    96  	s.Require().Nil(err)
    97  	s.Require().NotNil(poolID)
    98  
    99  	// confirm returned poolID matches correct bannerID
   100  	var bannerIDfromPool string
   101  	err = s.db.QueryRow("SELECT banner_edge_id FROM ca_pools WHERE ca_pool_edge_id = $1", poolID).Scan(&bannerIDfromPool)
   102  	s.Require().Nil(err)
   103  	s.Require().Equal(bannerGUID, bannerIDfromPool)
   104  
   105  	err = r.reconcileCACerts(s.ctx, banner, poolID)
   106  	s.Require().Nil(err)
   107  
   108  	// confirm the created ca cert using the ca pool's id
   109  	var caCert string
   110  	err = s.db.QueryRow("SELECT ca_cert_edge_id FROM ca_certificates WHERE ca_pool_edge_id = $1", poolID).Scan(&caCert)
   111  	s.Require().Nil(err)
   112  	s.Require().NotNil(caCert)
   113  }
   114  
   115  func (s *Suite) TestCreateCACert() {
   116  	template := &x509.Certificate{
   117  		SerialNumber: big.NewInt(1),
   118  		Subject: pkix.Name{
   119  			CommonName: "emissary-ca",
   120  		},
   121  		NotBefore:             time.Now(),
   122  		NotAfter:              time.Now().AddDate(5, 0, 0),
   123  		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
   124  		ExtKeyUsage:           []x509.ExtKeyUsage{},
   125  		IsCA:                  true,
   126  		BasicConstraintsValid: true,
   127  	}
   128  
   129  	cert, privateKey, err := generateCert(template)
   130  	s.Require().Nil(err)
   131  	s.Require().NotNil(cert)
   132  	s.Require().NotNil(privateKey)
   133  
   134  	// Decode the base64-encoded certificate
   135  	_, err = base64.StdEncoding.Decode(privateKey, privateKey)
   136  	s.Require().Nil(err)
   137  
   138  	_, err = base64.StdEncoding.Decode(cert, cert)
   139  	s.Require().Nil(err)
   140  
   141  	// Decode the PEM-encoded certificate
   142  	block, _ := pem.Decode(cert)
   143  	s.Require().NotNil(block)
   144  	s.Require().Equal("CERTIFICATE", block.Type)
   145  
   146  	// Parse the certificate, confirm it's valid
   147  	parsedCert, err := x509.ParseCertificate(block.Bytes)
   148  	s.Require().Nil(err)
   149  	s.Require().NotNil(parsedCert)
   150  
   151  	// confirm the certificate is not expired
   152  	s.Require().True(time.Now().Before(parsedCert.NotAfter))
   153  	s.Require().True(parsedCert.IsCA)
   154  
   155  	// Decode the PEM-encoded private key
   156  	block, _ = pem.Decode(privateKey)
   157  	s.Require().NotNil(block)
   158  	s.Require().Equal("EC PRIVATE KEY", block.Type)
   159  
   160  	// Parse the private key, confirm it's valid
   161  	parsedPrivateKey, err := x509.ParseECPrivateKey(block.Bytes)
   162  	s.Require().Nil(err)
   163  	s.Require().NotNil(parsedPrivateKey)
   164  }
   165  
   166  func (s *Suite) TestReconcileCACert() {
   167  	bannerGUID := uuid.New().UUID
   168  	generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix))))
   169  	r := &BannerReconciler{
   170  		Log:           ctrl.Log.WithName("controllers").WithName("Banner"),
   171  		EdgeDB:        &edgedb.EdgeDB{DB: s.db},
   172  		SecretManager: s.sMgr,
   173  	}
   174  
   175  	// Create a Banner instance
   176  	banner := &bannerAPI.Banner{
   177  		ObjectMeta: metav1.ObjectMeta{
   178  			Name: bannerGUID,
   179  		},
   180  		Spec: bannerAPI.BannerSpec{
   181  			DisplayName: "banner-for-ca-stuff-3",
   182  			GCP: bannerAPI.GCPConfig{
   183  				ProjectID: generatedProjID,
   184  			},
   185  			BSL: bannerAPI.BSLConfig{
   186  				EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{
   187  					ID: uuid.New().UUID,
   188  				},
   189  				Organization: bannerAPI.BSLOrganization{
   190  					Name: "test-org-for-ca-stuff-3",
   191  				},
   192  			},
   193  		},
   194  	}
   195  
   196  	s.createBanner(banner)
   197  	defer s.deleteBanner(banner)
   198  
   199  	poolID, err := r.reconcileCAPool(context.TODO(), bannerGUID)
   200  	s.Require().Nil(err)
   201  	s.Require().NotNil(poolID)
   202  
   203  	err = r.reconcileCACerts(s.ctx, banner, poolID)
   204  	s.Require().Nil(err)
   205  }
   206  
   207  func (s *Suite) TestReconcileCACertsRotation() {
   208  	bannerGUID := uuid.New().UUID
   209  	generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix))))
   210  	banner := &bannerAPI.Banner{
   211  		ObjectMeta: metav1.ObjectMeta{
   212  			Name: bannerGUID,
   213  		},
   214  		Spec: bannerAPI.BannerSpec{
   215  			DisplayName: "banner-for-ca-stuff-4",
   216  			GCP: bannerAPI.GCPConfig{
   217  				ProjectID: generatedProjID,
   218  			},
   219  			BSL: bannerAPI.BSLConfig{
   220  				EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{
   221  					ID: uuid.New().UUID,
   222  				},
   223  				Organization: bannerAPI.BSLOrganization{
   224  					Name: "test-org-for-ca-stuff-4",
   225  				},
   226  			},
   227  		},
   228  	}
   229  
   230  	s.createBanner(banner)
   231  	defer s.deleteBanner(banner)
   232  
   233  	// Create a new BannerReconciler
   234  	r := &BannerReconciler{
   235  		Log:           ctrl.Log.WithName("controllers").WithName("Banner"),
   236  		EdgeDB:        &edgedb.EdgeDB{DB: s.db},
   237  		SecretManager: s.sMgr,
   238  	}
   239  
   240  	// Create CA pool
   241  	poolID, err := r.reconcileCAPool(context.TODO(), bannerGUID)
   242  	s.Require().Nil(err)
   243  	s.Require().NotNil(poolID)
   244  
   245  	// Insert active and staged CA certs
   246  	activeCertID := uuid.New().UUID
   247  	stagedCertID := uuid.New().UUID
   248  	activeExpiration := time.Now().AddDate(0, 9, 0).Add(time.Second * 3) // 9 months from now
   249  	stagedExpiration := time.Now().AddDate(5, 0, 0)                      // 5 years from now
   250  
   251  	_, err = s.db.Exec(`
   252          INSERT INTO ca_certificates (ca_cert_edge_id, ca_pool_edge_id, status, cert_ref, private_key_ref, expiration)
   253          VALUES ($1, $2, 'active', 'active-cert-ref', 'active-private-key-ref', $3),
   254                 ($4, $5, 'staged', 'staged-cert-ref', 'staged-private-key-ref', $6)
   255      `, activeCertID, poolID, activeExpiration, stagedCertID, poolID, stagedExpiration)
   256  	s.Require().Nil(err)
   257  
   258  	// Reconcile CA certs
   259  	err = r.reconcileCACerts(s.ctx, banner, poolID)
   260  	s.Require().Nil(err)
   261  
   262  	// Verify that rotation does not occur before the rotation window is reached
   263  	var status string
   264  	err = s.db.QueryRow("SELECT status FROM ca_certificates WHERE ca_cert_edge_id = $1", activeCertID).Scan(&status)
   265  	s.Require().Nil(err)
   266  	s.Require().Equal("active", status)
   267  
   268  	err = s.db.QueryRow("SELECT status FROM ca_certificates WHERE ca_cert_edge_id = $1", stagedCertID).Scan(&status)
   269  	s.Require().Nil(err)
   270  	s.Require().Equal("staged", status)
   271  
   272  	// Here we will wait a few seconds for the active cert to reach the rotation window, call reconcileCACerts again
   273  	// and verify that the certs are now rotated
   274  	time.Sleep(time.Second * 4)
   275  
   276  	// Reconcile CA certs
   277  	err = r.reconcileCACerts(s.ctx, banner, poolID)
   278  	s.Require().Nil(err)
   279  
   280  	// Verify that the active cert is now retired and the staged cert is now active
   281  	err = s.db.QueryRow("SELECT status FROM ca_certificates WHERE ca_cert_edge_id = $1", activeCertID).Scan(&status)
   282  	s.Require().Nil(err)
   283  	s.Require().Equal("retired", status)
   284  
   285  	err = s.db.QueryRow("SELECT status FROM ca_certificates WHERE ca_cert_edge_id = $1", stagedCertID).Scan(&status)
   286  	s.Require().Nil(err)
   287  	s.Require().Equal("active", status)
   288  }
   289  
   290  func (s *Suite) TestReconcileCACertsCreateStagedCert() {
   291  	bannerGUID := uuid.New().UUID
   292  	generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix))))
   293  	banner := &bannerAPI.Banner{
   294  		ObjectMeta: metav1.ObjectMeta{
   295  			Name: bannerGUID,
   296  		},
   297  		Spec: bannerAPI.BannerSpec{
   298  			DisplayName: "banner-for-ca-stuff-5",
   299  			GCP: bannerAPI.GCPConfig{
   300  				ProjectID: generatedProjID,
   301  			},
   302  			BSL: bannerAPI.BSLConfig{
   303  				EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{
   304  					ID: uuid.New().UUID,
   305  				},
   306  				Organization: bannerAPI.BSLOrganization{
   307  					Name: "test-org-for-ca-stuff-5",
   308  				},
   309  			},
   310  		},
   311  	}
   312  
   313  	s.createBanner(banner)
   314  	defer s.deleteBanner(banner)
   315  
   316  	// Create a new BannerReconciler
   317  	r := &BannerReconciler{
   318  		Log:           ctrl.Log.WithName("controllers").WithName("Banner"),
   319  		EdgeDB:        &edgedb.EdgeDB{DB: s.db},
   320  		SecretManager: s.sMgr,
   321  	}
   322  
   323  	// Create CA pool
   324  	poolID, err := r.reconcileCAPool(context.TODO(), bannerGUID)
   325  	s.Require().Nil(err)
   326  	s.Require().NotNil(poolID)
   327  
   328  	// Insert an active CA cert
   329  	activeCertID := uuid.New().UUID
   330  	activeExpiration := time.Now().AddDate(0, 11, 0) // 11 months from now
   331  
   332  	_, err = s.db.Exec(`
   333          INSERT INTO ca_certificates (ca_cert_edge_id, ca_pool_edge_id, status, cert_ref, private_key_ref, expiration)
   334          VALUES ($1, $2, 'active', 'active-cert-ref', 'active-private-key-ref', $3)
   335      `, activeCertID, poolID, activeExpiration)
   336  	s.Require().Nil(err)
   337  
   338  	// Reconcile CA certs
   339  	err = r.reconcileCACerts(s.ctx, banner, poolID)
   340  	s.Require().Nil(err)
   341  
   342  	// Verify that a new staged cert has been created
   343  	var stagedCertID string
   344  	err = s.db.QueryRow("SELECT ca_cert_edge_id FROM ca_certificates WHERE ca_pool_edge_id = $1 AND status = 'staged'", poolID).Scan(&stagedCertID)
   345  	s.Require().Nil(err)
   346  	s.Require().NotNil(stagedCertID)
   347  
   348  	// Verify that the active cert is still active
   349  	var status string
   350  	err = s.db.QueryRow("SELECT status FROM ca_certificates WHERE ca_cert_edge_id = $1", activeCertID).Scan(&status)
   351  	s.Require().Nil(err)
   352  	s.Require().Equal("active", status)
   353  }
   354  
   355  func (s *Suite) TestReconcileCACertsUpdateRetiredToDeleted() {
   356  	bannerGUID := uuid.New().UUID
   357  	generatedProjID := fmt.Sprintf("%s-%s", gcpinfra.ProjectIDPrefix, gcpProject.RandAN(29-(len(gcpinfra.ProjectIDPrefix))))
   358  	banner := &bannerAPI.Banner{
   359  		ObjectMeta: metav1.ObjectMeta{
   360  			Name: bannerGUID,
   361  		},
   362  		Spec: bannerAPI.BannerSpec{
   363  			DisplayName: "banner-for-ca-stuff-6",
   364  			GCP: bannerAPI.GCPConfig{
   365  				ProjectID: generatedProjID,
   366  			},
   367  			BSL: bannerAPI.BSLConfig{
   368  				EnterpriseUnit: bannerAPI.BSLEnterpriseUnit{
   369  					ID: uuid.New().UUID,
   370  				},
   371  				Organization: bannerAPI.BSLOrganization{
   372  					Name: "test-org-for-ca-stuff-6",
   373  				},
   374  			},
   375  		},
   376  	}
   377  
   378  	s.createBanner(banner)
   379  	defer s.deleteBanner(banner)
   380  
   381  	// Create a new BannerReconciler
   382  	r := &BannerReconciler{
   383  		Log:           ctrl.Log.WithName("controllers").WithName("Banner"),
   384  		EdgeDB:        &edgedb.EdgeDB{DB: s.db},
   385  		SecretManager: s.sMgr,
   386  	}
   387  
   388  	// Create CA pool
   389  	poolID, err := r.reconcileCAPool(context.TODO(), bannerGUID)
   390  	s.Require().Nil(err)
   391  	s.Require().NotNil(poolID)
   392  
   393  	// Insert a retired CA cert that is past its expiration
   394  	retiredCertID := uuid.New().UUID
   395  	expiredDate := time.Now().AddDate(-1, 0, 0) // 1 year ago
   396  	activeCertID := uuid.New().UUID
   397  	activeExpiration := time.Now().AddDate(5, 0, 0)
   398  
   399  	_, err = s.db.Exec(`
   400          INSERT INTO ca_certificates (ca_cert_edge_id, ca_pool_edge_id, status, cert_ref, private_key_ref, expiration)
   401          VALUES 
   402              ($1, $2, 'retired', 'retired-cert-ref', 'retired-private-key-ref', $3),
   403              ($4, $5, 'active', 'active-cert-ref', 'active-private-key-ref', $6)
   404      `, retiredCertID, poolID, expiredDate, activeCertID, poolID, activeExpiration)
   405  	s.Require().Nil(err)
   406  
   407  	// Reconcile CA certs
   408  	err = r.reconcileCACerts(s.ctx, banner, poolID)
   409  	s.Require().Nil(err)
   410  
   411  	// Verify that the retired cert is now deleted
   412  	var status string
   413  	err = s.db.QueryRow("SELECT status FROM ca_certificates WHERE ca_cert_edge_id = $1", retiredCertID).Scan(&status)
   414  	s.Require().Nil(err)
   415  	s.Require().Equal("deleted", status)
   416  }
   417  

View as plain text