diff --git a/changes/42405-android-onc-after-cert b/changes/42405-android-onc-after-cert index 95cbec0e14f..284c2ce848a 100644 --- a/changes/42405-android-onc-after-cert +++ b/changes/42405-android-onc-after-cert @@ -1,2 +1,3 @@ - Android Wi-Fi configuration profiles (`openNetworkConfiguration` with `ClientCertKeyPairAlias`) are now withheld until the referenced certificate is installed or terminally failed on the device. - The host OS settings detail column now shows the reason when an Android profile is pending due to a certificate dependency. +- `fleetctl gitops` now processes Android certificates before Android profiles diff --git a/server/datastore/mysql/host_certificate_templates.go b/server/datastore/mysql/host_certificate_templates.go index 2fc6f028a2a..6b86c45b3b2 100644 --- a/server/datastore/mysql/host_certificate_templates.go +++ b/server/datastore/mysql/host_certificate_templates.go @@ -226,7 +226,7 @@ func (ds *Datastore) RequeueWithheldONCProfilesForHost(ctx context.Context, host WHERE host_uuid = ? AND status = ? AND detail LIKE ?`, hostUUID, fleet.MDMDeliveryPending, fleet.ONCProfileWithheldDetailPrefix+"%", ) - return ctxerr.Wrap(ctx, err, "requeue pending android profiles for host") + return ctxerr.Wrap(ctx, err, "requeue withheld ONC profiles for host") } // RetryHostCertificateTemplate resets a failed certificate to pending for automatic retry, @@ -618,11 +618,27 @@ func (ds *Datastore) RevertStaleCertificateTemplates( // For a given certificate template ID, it deletes any rows with status in (pending, failed) // and operation_type=install, then updates rows with operation_type=install to pending remove. // Rows already in remove state are left unchanged (idempotent). +// +// Also requeues any withheld ONC profiles for affected hosts within the same transaction, +// so the reconciler releases them on its next cycle. func (ds *Datastore) SetHostCertificateTemplatesToPendingRemove( ctx context.Context, certificateTemplateID uint, ) error { return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + // Requeue withheld ONC profiles for all hosts with this cert template. + // Must run before deleting host_certificate_templates rows since the + // subquery needs them to find affected hosts. + if _, err := tx.ExecContext(ctx, + `UPDATE host_mdm_android_profiles SET status = NULL + WHERE host_uuid IN ( + SELECT host_uuid FROM host_certificate_templates WHERE certificate_template_id = ? + ) AND status = ? AND detail LIKE ?`, + certificateTemplateID, fleet.MDMDeliveryPending, fleet.ONCProfileWithheldDetailPrefix+"%", + ); err != nil { + return ctxerr.Wrap(ctx, err, "requeue withheld ONC profiles for cert template removal") + } + // Delete rows with status in (pending, failed) and operation_type=install // These certificates were never successfully installed on the device deleteStmt := fmt.Sprintf(` diff --git a/server/datastore/mysql/host_certificate_templates_test.go b/server/datastore/mysql/host_certificate_templates_test.go index 4a1bfad0314..3e2815adc2a 100644 --- a/server/datastore/mysql/host_certificate_templates_test.go +++ b/server/datastore/mysql/host_certificate_templates_test.go @@ -1115,9 +1115,31 @@ func testSetHostCertificateTemplatesToPendingRemove(t *testing.T, ds *Datastore) ) require.NoError(t, err) + // Insert a withheld ONC profile for host-delivered to verify it gets requeued + // when the cert template is removed. + withheldStatus := fleet.MDMDeliveryPending + err = ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{{ + HostUUID: "host-delivered", + ProfileUUID: "prof-onc-wifi", + ProfileName: "onc-wifi", + Status: &withheldStatus, + OperationType: fleet.MDMOperationTypeInstall, + Detail: fleet.ONCProfileWithheldDetailPrefix + ` "wifi-cert" to be installed on the host before applying this profile.`, + }}) + require.NoError(t, err) + err = ds.SetHostCertificateTemplatesToPendingRemove(ctx, setup.template.ID) require.NoError(t, err) + // Verify the withheld ONC profile was requeued (status set to NULL). + // We query raw SQL here because GetHostMDMAndroidProfiles coalesces NULL to 'pending'. + var oncStatus *string + err = ds.writer(ctx).GetContext(ctx, &oncStatus, + "SELECT status FROM host_mdm_android_profiles WHERE host_uuid = ? AND profile_uuid = ?", + "host-delivered", "prof-onc-wifi") + require.NoError(t, err) + require.Nil(t, oncStatus, "withheld ONC profile should have status reset to NULL") + // Verify the pending row was deleted var count int err = ds.writer(ctx).GetContext(ctx, &count, diff --git a/server/service/client.go b/server/service/client.go index 6c1f4183b97..59e4c5bc75f 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -2453,12 +2453,11 @@ func (c *Client) DoGitOps( } } - err = c.doGitOpsPolicies(incoming, teamSoftwareInstallers, teamVPPApps, teamScripts, logFn, dryRun) - if err != nil { - return nil, err - } - - // Apply Android certificates if present + // Apply Android certificates before policies so that certificate templates + // exist on the server before the profile reconciler's next cron cycle. This + // prevents a race where the cron fires after profiles are uploaded (by + // ApplyGroup above) but before cert templates exist, which would cause ONC + // profiles to be sent without waiting for the cert. err = c.doGitOpsAndroidCertificates(incoming, logFn, dryRun) if err != nil { var gitOpsErr *gitOpsValidationError @@ -2468,6 +2467,11 @@ func (c *Client) DoGitOps( return nil, err } + err = c.doGitOpsPolicies(incoming, teamSoftwareInstallers, teamVPPApps, teamScripts, logFn, dryRun) + if err != nil { + return nil, err + } + // apply icon changes from software installers and VPP apps if len(teamSoftwareInstallers) > 0 || len(teamVPPApps) > 0 { iconUpdates := fleet.IconChanges{}.WithUploadedHashes(iconSettings.UploadedHashes).WithSoftware(teamSoftwareInstallers, teamVPPApps)