Renewal of DigiCert certificates on macOS (#28449)
Adds renewal of Digicert certificates: https://github.com/fleetdm/fleet/issues/26553 . Does not attempt to renew custom SCEP or NDES. Also we aren't actually calling the DigiCert renewal endpoint at this time because we don't believe we need to and we can't necessarily do that as we weren't previously storing the serial number however this change adds storage of the serial number. # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Added/updated automated tests - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Fleet-managed DigiCert certificates will be renewed 30 days before expiry for those valid longer than 30 days or when half the validity period remains for certificates valid 30 days or less. This only applies to certificates that were initially requested after this feature was added. For hosts with DigiCert certificates originally requested prior to this renew feature, manually resending the profile will generate a new certificate which will be automatically renewed before its next expiry.
|
||||
@@ -933,6 +933,9 @@ func newCleanupsAndAggregationSchedule(
|
||||
return service.RenewSCEPCertificates(ctx, logger, ds, config, commander)
|
||||
},
|
||||
),
|
||||
schedule.WithJob("renew_host_mdm_managed_certificates", func(ctx context.Context) error {
|
||||
return ds.RenewMDMManagedCertificates(ctx)
|
||||
}),
|
||||
schedule.WithJob("query_results_cleanup", func(ctx context.Context) error {
|
||||
config, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -66,7 +67,6 @@ func WithLogger(logger kitlog.Logger) Opt {
|
||||
}
|
||||
|
||||
func (s *Service) VerifyProfileID(ctx context.Context, config fleet.DigiCertIntegration) error {
|
||||
|
||||
client := fleethttp.NewClient(fleethttp.WithTimeout(s.timeout))
|
||||
|
||||
config.URL = strings.TrimRight(config.URL, "/")
|
||||
@@ -251,6 +251,13 @@ func (s *Service) GetCertificate(ctx context.Context, config fleet.DigiCertInteg
|
||||
return nil, ctxerr.Errorf(ctx, "unexpected DigiCert delivery format: %s", certResp.DeliveryFormat)
|
||||
}
|
||||
|
||||
// Serial number is an up to 20-byte(40 char) hex string
|
||||
_, err = hex.DecodeString(certResp.SerialNumber)
|
||||
if err != nil || certResp.SerialNumber == "" || len(certResp.SerialNumber) > 40 {
|
||||
level.Error(s.logger).Log("msg", "DigiCert certificate returned with invalid serial number", "serial_number", certResp.SerialNumber, "decode_err", err)
|
||||
return nil, ctxerr.Errorf(ctx, "invalid DigiCert serial number: %s", certResp.SerialNumber)
|
||||
}
|
||||
|
||||
if len(certResp.Certificate) == 0 {
|
||||
return nil, ctxerr.Errorf(ctx, "did not receive DigiCert certificate")
|
||||
}
|
||||
@@ -279,8 +286,10 @@ func (s *Service) GetCertificate(ctx context.Context, config fleet.DigiCertInteg
|
||||
}
|
||||
|
||||
return &fleet.DigiCertCertificate{
|
||||
PfxData: pkcs12Data,
|
||||
Password: password,
|
||||
NotValidAfter: cert.NotAfter,
|
||||
PfxData: pkcs12Data,
|
||||
Password: password,
|
||||
NotValidBefore: cert.NotBefore,
|
||||
NotValidAfter: cert.NotAfter,
|
||||
SerialNumber: certResp.SerialNumber,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -521,9 +521,11 @@ func (ds *Datastore) GetHostMDMCertificateProfile(ctx context.Context, hostUUID
|
||||
hmap.profile_uuid,
|
||||
hmap.status,
|
||||
hmmc.challenge_retrieved_at,
|
||||
hmmc.not_valid_before,
|
||||
hmmc.not_valid_after,
|
||||
hmmc.type,
|
||||
hmmc.ca_name
|
||||
hmmc.ca_name,
|
||||
hmmc.serial
|
||||
FROM
|
||||
host_mdm_apple_profiles hmap
|
||||
JOIN host_mdm_managed_certificates hmmc
|
||||
@@ -551,6 +553,65 @@ func (ds *Datastore) CleanUpMDMManagedCertificates(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenewMDMManagedCertificates marks managed certificate profiles for resend when renewal is required
|
||||
func (ds *Datastore) RenewMDMManagedCertificates(ctx context.Context) error {
|
||||
// Fetch all MDM Managed digicert certificates that aren't already queued for resend(hmap.status=null) and which
|
||||
// * Have a validity period > 30 days and are expiring in the next 30 days
|
||||
// * Have a validity period <= 30 days and are within half the validity period of expiration
|
||||
// nb: we SELECT not_valid_after and validity_period here so we can use them in the HAVING clause, but
|
||||
// we don't actually need them for the update logic.
|
||||
hostCertsToRenew := []struct {
|
||||
HostUUID string `db:"host_uuid"`
|
||||
ProfileUUID string `db:"profile_uuid"`
|
||||
NotValidAfter time.Time `db:"not_valid_after"`
|
||||
ValidityPeriod int `db:"validity_period"`
|
||||
}{}
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &hostCertsToRenew, `
|
||||
SELECT
|
||||
hmmc.host_uuid,
|
||||
hmmc.profile_uuid,
|
||||
hmmc.not_valid_after,
|
||||
DATEDIFF(hmmc.not_valid_after, hmmc.not_valid_before) AS validity_period
|
||||
FROM
|
||||
host_mdm_managed_certificates hmmc
|
||||
INNER JOIN
|
||||
host_mdm_apple_profiles hmap
|
||||
ON hmmc.host_uuid = hmap.host_uuid AND hmmc.profile_uuid = hmap.profile_uuid
|
||||
WHERE
|
||||
hmmc.type = ? AND hmap.status IS NOT NULL
|
||||
HAVING
|
||||
validity_period IS NOT NULL AND
|
||||
((validity_period > 30 AND not_valid_after < DATE_ADD(NOW(), INTERVAL 30 DAY)) OR
|
||||
(validity_period <= 30 AND not_valid_after < DATE_ADD(NOW(), INTERVAL validity_period/2 DAY)))
|
||||
LIMIT 1000`, fleet.CAConfigDigiCert)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "retrieving mdm managed certificates to renew")
|
||||
}
|
||||
if len(hostCertsToRenew) == 0 {
|
||||
level.Debug(ds.logger).Log("msg", "No digicert certificates to renew")
|
||||
return nil
|
||||
}
|
||||
|
||||
// This will trigger a resend next time profiles are checked
|
||||
updateQuery := `UPDATE host_mdm_apple_profiles SET status = NULL WHERE `
|
||||
hostProfileClause := ``
|
||||
values := []interface{}{}
|
||||
for _, hostCertToRenew := range hostCertsToRenew {
|
||||
hostProfileClause += `(host_uuid = ? AND profile_uuid = ?) OR `
|
||||
values = append(values, hostCertToRenew.HostUUID, hostCertToRenew.ProfileUUID)
|
||||
}
|
||||
|
||||
level.Debug(ds.logger).Log("msg", "Renewing MDM managed digicert certificates", "len(hostCertsToRenew)", len(hostCertsToRenew))
|
||||
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
_, err := tx.ExecContext(ctx, updateQuery+strings.TrimSuffix(hostProfileClause, " OR "), values...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating mdm managed certificates to renew")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (ds *Datastore) NewMDMAppleEnrollmentProfile(
|
||||
ctx context.Context,
|
||||
payload fleet.MDMAppleEnrollmentProfilePayload,
|
||||
@@ -5883,16 +5944,20 @@ func (ds *Datastore) BulkUpsertMDMManagedCertificates(ctx context.Context, paylo
|
||||
host_uuid,
|
||||
profile_uuid,
|
||||
challenge_retrieved_at,
|
||||
not_valid_before,
|
||||
not_valid_after,
|
||||
type,
|
||||
ca_name
|
||||
ca_name,
|
||||
serial
|
||||
)
|
||||
VALUES %s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
challenge_retrieved_at = VALUES(challenge_retrieved_at),
|
||||
not_valid_before = VALUES(not_valid_before),
|
||||
not_valid_after = VALUES(not_valid_after),
|
||||
type = VALUES(type),
|
||||
ca_name = VALUES(ca_name)`,
|
||||
ca_name = VALUES(ca_name),
|
||||
serial = VALUES(serial)`,
|
||||
strings.TrimSuffix(valuePart, ","),
|
||||
)
|
||||
|
||||
@@ -5901,8 +5966,8 @@ func (ds *Datastore) BulkUpsertMDMManagedCertificates(ctx context.Context, paylo
|
||||
}
|
||||
|
||||
generateValueArgs := func(p *fleet.MDMBulkUpsertManagedCertificatePayload) (string, []any) {
|
||||
valuePart := "(?, ?, ?, ?, ?, ?),"
|
||||
args := []any{p.HostUUID, p.ProfileUUID, p.ChallengeRetrievedAt, p.NotValidAfter, p.Type, p.CAName}
|
||||
valuePart := "(?, ?, ?, ?, ?, ?, ?, ?),"
|
||||
args := []any{p.HostUUID, p.ProfileUUID, p.ChallengeRetrievedAt, p.NotValidBefore, p.NotValidAfter, p.Type, p.CAName, p.Serial}
|
||||
return valuePart, args
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,8 @@ func TestMDMApple(t *testing.T) {
|
||||
{"TestMDMGetABMTokenOrgNamesAssociatedWithTeam", testMDMGetABMTokenOrgNamesAssociatedWithTeam},
|
||||
{"HostMDMCommands", testHostMDMCommands},
|
||||
{"IngestMDMAppleDeviceFromOTAEnrollment", testIngestMDMAppleDeviceFromOTAEnrollment},
|
||||
{"MDMManagedCertificates", testMDMManagedCertificates},
|
||||
{"MDMManagedSCEPCertificates", testMDMManagedSCEPCertificates},
|
||||
{"MDMManagedDigicertCertificates", testMDMManagedDigicertCertificates},
|
||||
{"AppleMDMSetBatchAsyncLastSeenAt", testAppleMDMSetBatchAsyncLastSeenAt},
|
||||
{"TestMDMAppleProfileLabels", testMDMAppleProfileLabels},
|
||||
{"AggregateMacOSSettingsAllPlatforms", testAggregateMacOSSettingsAllPlatforms},
|
||||
@@ -608,7 +609,6 @@ func testHostDetailsMDMProfiles(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, gotProfs, 1)
|
||||
assert.Equal(t, &fleet.MDMDeliveryVerifying, gotProfs[0].Status)
|
||||
|
||||
}
|
||||
|
||||
func TestIngestMDMAppleDevicesFromDEPSync(t *testing.T) {
|
||||
@@ -2676,7 +2676,8 @@ func testDeleteMDMAppleProfilesForHost(t *testing.T, ds *Datastore) {
|
||||
}
|
||||
|
||||
func createDiskEncryptionRecord(ctx context.Context, ds *Datastore, t *testing.T, host *fleet.Host, key string, decryptable bool,
|
||||
threshold time.Time) {
|
||||
threshold time.Time,
|
||||
) {
|
||||
err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, key, "", nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, decryptable, threshold)
|
||||
@@ -7190,7 +7191,7 @@ func TestGetMDMAppleOSUpdatesSettingsByHostSerial(t *testing.T) {
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
func testMDMManagedCertificates(t *testing.T, ds *Datastore) {
|
||||
func testMDMManagedSCEPCertificates(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
initialCP := storeDummyConfigProfileForTest(t, ds)
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
@@ -7231,12 +7232,14 @@ func testMDMManagedCertificates(t *testing.T, ds *Datastore) {
|
||||
assert.Nil(t, profile)
|
||||
|
||||
challengeRetrievedAt := time.Now().Add(-time.Hour).UTC().Round(time.Microsecond)
|
||||
notValidBefore := time.Now().UTC().Round(time.Microsecond)
|
||||
notValidAfter := time.Now().Add(24 * time.Hour).UTC().Round(time.Microsecond)
|
||||
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMBulkUpsertManagedCertificatePayload{
|
||||
{
|
||||
HostUUID: host.UUID,
|
||||
ProfileUUID: initialCP.ProfileUUID,
|
||||
ChallengeRetrievedAt: &challengeRetrievedAt,
|
||||
NotValidBefore: ¬ValidBefore,
|
||||
NotValidAfter: ¬ValidAfter,
|
||||
Type: fleet.CAConfigCustomSCEPProxy,
|
||||
CAName: "test-ca",
|
||||
@@ -7252,10 +7255,20 @@ func testMDMManagedCertificates(t *testing.T, ds *Datastore) {
|
||||
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
|
||||
require.NotNil(t, profile.ChallengeRetrievedAt)
|
||||
assert.Equal(t, &challengeRetrievedAt, profile.ChallengeRetrievedAt)
|
||||
assert.Equal(t, ¬ValidBefore, profile.NotValidBefore)
|
||||
assert.Equal(t, ¬ValidAfter, profile.NotValidAfter)
|
||||
assert.Equal(t, fleet.CAConfigCustomSCEPProxy, profile.Type)
|
||||
assert.Nil(t, profile.Serial)
|
||||
assert.Equal(t, "test-ca", profile.CAName)
|
||||
|
||||
// Renew should do nothing for SCEP
|
||||
err = ds.RenewMDMManagedCertificates(ctx)
|
||||
require.NoError(t, err)
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, profile.Status)
|
||||
assert.Equal(t, fleet.MDMDeliveryPending, *profile.Status)
|
||||
|
||||
// Cleanup should do nothing
|
||||
err = ds.CleanUpMDMManagedCertificates(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -7290,6 +7303,232 @@ func testMDMManagedCertificates(t *testing.T, ds *Datastore) {
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
func testMDMManagedDigicertCertificates(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
initialCP := storeDummyConfigProfileForTest(t, ds)
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
OsqueryHostID: ptr.String("host0-osquery-id"),
|
||||
NodeKey: ptr.String("host0-node-key"),
|
||||
UUID: "host0-test-mdm-profiles",
|
||||
Hostname: "hostname0",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Host and profile are not linked
|
||||
profile, err := ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, profile)
|
||||
|
||||
err = ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
{
|
||||
ProfileUUID: initialCP.ProfileUUID,
|
||||
ProfileIdentifier: initialCP.Identifier,
|
||||
ProfileName: initialCP.Name,
|
||||
HostUUID: host.UUID,
|
||||
Status: &fleet.MDMDeliveryVerified,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
CommandUUID: "command-uuid",
|
||||
Checksum: []byte("checksum"),
|
||||
},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Host and profile do not have certificate metadata
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, profile)
|
||||
|
||||
notValidBefore := time.Now().UTC().Round(time.Microsecond)
|
||||
notValidAfter := time.Now().Add(29 * 24 * time.Hour).UTC().Round(time.Microsecond)
|
||||
serial := "3ABADCAFEF684D6348F5EC95AEFF468F237A9D7E"
|
||||
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMBulkUpsertManagedCertificatePayload{
|
||||
{
|
||||
HostUUID: host.UUID,
|
||||
ProfileUUID: initialCP.ProfileUUID,
|
||||
ChallengeRetrievedAt: nil,
|
||||
NotValidBefore: ¬ValidBefore,
|
||||
NotValidAfter: ¬ValidAfter,
|
||||
Type: fleet.CAConfigDigiCert,
|
||||
CAName: "test-ca",
|
||||
Serial: &serial,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that the managed certificate was inserted correctly
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, profile)
|
||||
assert.Equal(t, host.UUID, profile.HostUUID)
|
||||
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
|
||||
assert.Nil(t, profile.ChallengeRetrievedAt)
|
||||
assert.Equal(t, ¬ValidBefore, profile.NotValidBefore)
|
||||
assert.Equal(t, ¬ValidAfter, profile.NotValidAfter)
|
||||
assert.Equal(t, fleet.CAConfigDigiCert, profile.Type)
|
||||
require.NotNil(t, profile.Serial)
|
||||
assert.Equal(t, serial, *profile.Serial)
|
||||
assert.Equal(t, "test-ca", profile.CAName)
|
||||
|
||||
// Renew should not do anything yet so the MDM delivery status should stay "verified"
|
||||
err = ds.RenewMDMManagedCertificates(ctx)
|
||||
require.NoError(t, err)
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, profile.Status)
|
||||
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
|
||||
|
||||
t.Run("Renew scenario 1 - validity window > 30 days", func(t *testing.T) {
|
||||
// Set not_valid_before to 31 days in the past the validity window becomes 60 days, of which there are
|
||||
// 29 left which should trigger the first renewal scenario(window > 30 days, renew when < 30
|
||||
// days left)
|
||||
notValidBefore := time.Now().Add(-31 * 24 * time.Hour).UTC().Round(time.Microsecond)
|
||||
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMBulkUpsertManagedCertificatePayload{
|
||||
{
|
||||
HostUUID: host.UUID,
|
||||
ProfileUUID: initialCP.ProfileUUID,
|
||||
ChallengeRetrievedAt: nil,
|
||||
NotValidBefore: ¬ValidBefore,
|
||||
NotValidAfter: ¬ValidAfter,
|
||||
Type: fleet.CAConfigDigiCert,
|
||||
CAName: "test-ca",
|
||||
Serial: &serial,
|
||||
},
|
||||
})
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `
|
||||
UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ?
|
||||
`, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Verify the policy is not currently marked for resend and that the upsert executed correctly
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, profile.Status)
|
||||
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
|
||||
|
||||
assert.Equal(t, host.UUID, profile.HostUUID)
|
||||
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
|
||||
assert.Nil(t, profile.ChallengeRetrievedAt)
|
||||
assert.Equal(t, ¬ValidBefore, profile.NotValidBefore)
|
||||
assert.Equal(t, ¬ValidAfter, profile.NotValidAfter)
|
||||
assert.Equal(t, fleet.CAConfigDigiCert, profile.Type)
|
||||
require.NotNil(t, profile.Serial)
|
||||
assert.Equal(t, serial, *profile.Serial)
|
||||
assert.Equal(t, "test-ca", profile.CAName)
|
||||
|
||||
// Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed
|
||||
err = ds.RenewMDMManagedCertificates(ctx)
|
||||
require.NoError(t, err)
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, profile.Status)
|
||||
|
||||
// Cleanup should do nothing
|
||||
err = ds.CleanUpMDMManagedCertificates(ctx)
|
||||
require.NoError(t, err)
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, profile)
|
||||
})
|
||||
|
||||
t.Run("Renew scenario 2 - validity window < 30 days", func(t *testing.T) {
|
||||
// Set not_valid_before to 15 days in the past and not_valid_after to 14 days in the future so the
|
||||
// validity window becomes 29 days, of which there are 14 left which should trigger the second
|
||||
// renewal scenario(window < 30 days, renew when there is half that time left)
|
||||
notValidBefore := time.Now().Add(-15 * 24 * time.Hour).UTC().Round(time.Microsecond)
|
||||
notValidAfter := time.Now().Add(14 * 24 * time.Hour).UTC().Round(time.Microsecond)
|
||||
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMBulkUpsertManagedCertificatePayload{
|
||||
{
|
||||
HostUUID: host.UUID,
|
||||
ProfileUUID: initialCP.ProfileUUID,
|
||||
ChallengeRetrievedAt: nil,
|
||||
NotValidBefore: ¬ValidBefore,
|
||||
NotValidAfter: ¬ValidAfter,
|
||||
Type: fleet.CAConfigDigiCert,
|
||||
CAName: "test-ca",
|
||||
Serial: &serial,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `
|
||||
UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ?
|
||||
`, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the policy is not currently marked for resend and that the upsert executed correctly
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, profile.Status)
|
||||
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
|
||||
|
||||
assert.Equal(t, host.UUID, profile.HostUUID)
|
||||
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
|
||||
assert.Nil(t, profile.ChallengeRetrievedAt)
|
||||
assert.Equal(t, ¬ValidBefore, profile.NotValidBefore)
|
||||
assert.Equal(t, ¬ValidAfter, profile.NotValidAfter)
|
||||
assert.Equal(t, fleet.CAConfigDigiCert, profile.Type)
|
||||
require.NotNil(t, profile.Serial)
|
||||
assert.Equal(t, serial, *profile.Serial)
|
||||
assert.Equal(t, "test-ca", profile.CAName)
|
||||
|
||||
// Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed
|
||||
err = ds.RenewMDMManagedCertificates(ctx)
|
||||
require.NoError(t, err)
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, profile.Status)
|
||||
|
||||
// Cleanup should do nothing
|
||||
err = ds.CleanUpMDMManagedCertificates(ctx)
|
||||
require.NoError(t, err)
|
||||
profile, err = ds.GetHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, "test-ca")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, profile)
|
||||
})
|
||||
|
||||
badProfileUUID := uuid.NewString()
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `
|
||||
INSERT INTO host_mdm_managed_certificates (host_uuid, profile_uuid) VALUES (?, ?)
|
||||
`, host.UUID, badProfileUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
var uid string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM host_mdm_managed_certificates WHERE profile_uuid = ?`,
|
||||
badProfileUUID)
|
||||
})
|
||||
require.Equal(t, badProfileUUID, uid)
|
||||
|
||||
// Cleanup should delete the above orphaned record
|
||||
err = ds.CleanUpMDMManagedCertificates(ctx)
|
||||
require.NoError(t, err)
|
||||
err = ExecAdhocSQLWithError(ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM host_mdm_managed_certificates WHERE profile_uuid = ?`,
|
||||
badProfileUUID)
|
||||
})
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
func testAppleMDMSetBatchAsyncLastSeenAt(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20250421085116, Down_20250421085116)
|
||||
}
|
||||
|
||||
func Up_20250421085116(tx *sql.Tx) error {
|
||||
if columnsExists(tx, "host_mdm_managed_certificates", "serial", "not_valid_before") {
|
||||
return nil
|
||||
}
|
||||
_, err := tx.Exec(`
|
||||
ALTER TABLE host_mdm_managed_certificates
|
||||
ADD COLUMN serial varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
ADD COLUMN not_valid_before datetime(6) NULL
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add serial columns to host_mdm_managed_certificates table: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20250421085116(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -292,9 +292,11 @@ type HostMDMCertificateProfile struct {
|
||||
ProfileUUID string `db:"profile_uuid"`
|
||||
Status *MDMDeliveryStatus `db:"status"`
|
||||
ChallengeRetrievedAt *time.Time `db:"challenge_retrieved_at"`
|
||||
NotValidBefore *time.Time `db:"not_valid_before"`
|
||||
NotValidAfter *time.Time `db:"not_valid_after"`
|
||||
Type CAConfigAssetType `db:"type"`
|
||||
CAName string `db:"ca_name"`
|
||||
Serial *string `db:"serial"`
|
||||
}
|
||||
|
||||
type HostMDMProfileDetail string
|
||||
@@ -976,9 +978,11 @@ type MDMBulkUpsertManagedCertificatePayload struct {
|
||||
ProfileUUID string
|
||||
HostUUID string
|
||||
ChallengeRetrievedAt *time.Time
|
||||
NotValidBefore *time.Time
|
||||
NotValidAfter *time.Time
|
||||
Type CAConfigAssetType
|
||||
CAName string
|
||||
Serial *string
|
||||
}
|
||||
|
||||
// MDMAppleEnrolledDeviceInfo represents the information of a device enrolled
|
||||
|
||||
@@ -1993,6 +1993,9 @@ type Datastore interface {
|
||||
// CleanUpMDMManagedCertificates removes all managed certificates that are not associated with any host+profile.
|
||||
CleanUpMDMManagedCertificates(ctx context.Context) error
|
||||
|
||||
// RenewMDMManagedCertificates marks managed certificate profiles for resend when renewal is required
|
||||
RenewMDMManagedCertificates(ctx context.Context) error
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Secret variables
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
)
|
||||
|
||||
type DigiCertCertificate struct {
|
||||
PfxData []byte
|
||||
Password string
|
||||
NotValidAfter time.Time
|
||||
PfxData []byte
|
||||
Password string
|
||||
NotValidBefore time.Time
|
||||
NotValidAfter time.Time
|
||||
SerialNumber string
|
||||
}
|
||||
|
||||
type DigiCertService interface {
|
||||
|
||||
@@ -1252,6 +1252,8 @@ type GetHostMDMCertificateProfileFunc func(ctx context.Context, hostUUID string,
|
||||
|
||||
type CleanUpMDMManagedCertificatesFunc func(ctx context.Context) error
|
||||
|
||||
type RenewMDMManagedCertificatesFunc func(ctx context.Context) error
|
||||
|
||||
type UpsertSecretVariablesFunc func(ctx context.Context, secretVariables []fleet.SecretVariable) error
|
||||
|
||||
type GetSecretVariablesFunc func(ctx context.Context, names []string) ([]fleet.SecretVariable, error)
|
||||
@@ -3168,6 +3170,9 @@ type DataStore struct {
|
||||
CleanUpMDMManagedCertificatesFunc CleanUpMDMManagedCertificatesFunc
|
||||
CleanUpMDMManagedCertificatesFuncInvoked bool
|
||||
|
||||
RenewMDMManagedCertificatesFunc RenewMDMManagedCertificatesFunc
|
||||
RenewMDMManagedCertificatesFuncInvoked bool
|
||||
|
||||
UpsertSecretVariablesFunc UpsertSecretVariablesFunc
|
||||
UpsertSecretVariablesFuncInvoked bool
|
||||
|
||||
@@ -7581,6 +7586,13 @@ func (s *DataStore) CleanUpMDMManagedCertificates(ctx context.Context) error {
|
||||
return s.CleanUpMDMManagedCertificatesFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *DataStore) RenewMDMManagedCertificates(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
s.RenewMDMManagedCertificatesFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.RenewMDMManagedCertificatesFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *DataStore) UpsertSecretVariables(ctx context.Context, secretVariables []fleet.SecretVariable) error {
|
||||
s.mu.Lock()
|
||||
s.UpsertSecretVariablesFuncInvoked = true
|
||||
|
||||
@@ -4383,11 +4383,13 @@ func preprocessProfileContents(
|
||||
return ctxerr.Wrap(ctx, err, "replacing Fleet variable for DigiCert password")
|
||||
}
|
||||
managedCertificatePayloads = append(managedCertificatePayloads, &fleet.MDMBulkUpsertManagedCertificatePayload{
|
||||
HostUUID: hostUUID,
|
||||
ProfileUUID: profUUID,
|
||||
NotValidAfter: &cert.NotValidAfter,
|
||||
Type: fleet.CAConfigDigiCert,
|
||||
CAName: caName,
|
||||
HostUUID: hostUUID,
|
||||
ProfileUUID: profUUID,
|
||||
NotValidBefore: &cert.NotValidBefore,
|
||||
NotValidAfter: &cert.NotValidAfter,
|
||||
Type: fleet.CAConfigDigiCert,
|
||||
CAName: caName,
|
||||
Serial: &cert.SerialNumber,
|
||||
})
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user