ACME: migration to add hardware_serial to DEP assignments#42833
ACME: migration to add hardware_serial to DEP assignments#42833JordanMontgomery merged 13 commits intofeature-ACMEfrom
Conversation
| } | ||
| if !strings.HasSuffix(certPEM, "\n") { | ||
| certPEM += "\n" | ||
| } |
There was a problem hiding this comment.
Fix for a Claude feedback on my latest get certificate PR.
| func (ds *Datastore) GetDeviceInfoForACMERenewal(ctx context.Context, hostUUIDs []string) ([]fleet.DeviceInfoForACMERenewal, error) { | ||
| // TODO(mna): anyone knows what those TODOs (from Sarah's PRs) were for? |
There was a problem hiding this comment.
It's about host_dep_assignments, but not about the new hardware_serial field, so I 'm not sure if there's anything to change here.
There was a problem hiding this comment.
I think the suggestion was we c ould put the model information here as well? I think it is OK not to for now
|
|
||
| stmt := ` | ||
| INSERT INTO host_dep_assignments (host_id, abm_token_id, mdm_migration_deadline) | ||
| INSERT INTO host_dep_assignments (host_id, abm_token_id, mdm_migration_deadline, hardware_serial) |
There was a problem hiding this comment.
Note that while I've added it to the insertion here, I haven't modified the fleet.HostDEPAssignment struct to add that field (nor to load it when it is read). Wasn't clear to me if that was needed and I wanted to change as little as necessary to avoid impacting more than ACME.
There was a problem hiding this comment.
It doesn't seem needed at the moment so I think that is OK
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feature-ACME #42833 +/- ##
===============================================
Coverage ? 66.50%
===============================================
Files ? 2536
Lines ? 202687
Branches ? 9047
===============================================
Hits ? 134797
Misses ? 55654
Partials ? 12236
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
|
@claude review once |
| func Up_20260401153503(tx *sql.Tx) error { | ||
| _, err := tx.Exec(` | ||
| ALTER TABLE host_dep_assignments | ||
| ADD COLUMN hardware_serial varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' | ||
| `) | ||
| if err != nil { | ||
| return errors.Wrap(err, "add host_dep_assignments.hardware_serial column") | ||
| } | ||
|
|
||
| _, err = tx.Exec(` | ||
| UPDATE host_dep_assignments hda | ||
| JOIN hosts h ON h.id = hda.host_id | ||
| SET hda.hardware_serial = h.hardware_serial | ||
| WHERE hda.deleted_at IS NULL | ||
| `) | ||
| if err != nil { | ||
| return errors.Wrap(err, "backfill host_dep_assignments.hardware_serial column") | ||
| } |
There was a problem hiding this comment.
🔴 The migration adds hardware_serial to host_dep_assignments but omits an index on the column, causing GetHostDEPAssignmentsBySerial to perform a full table scan on every ACME certificate lookup. Add ALTER TABLE host_dep_assignments ADD INDEX idx_hdep_hardware_serial (hardware_serial) to the migration to restore query performance.
Extended reasoning...
What the bug is and how it manifests
The migration Up_20260401153503 adds a hardware_serial VARCHAR(255) column to host_dep_assignments and backfills it, but never creates an index on the new column. The PR simultaneously changes GetHostDEPAssignmentsBySerial to query directly on hdep.hardware_serial = ? instead of using the previous subselect approach.
The specific code path that triggers it
In apple_mdm.go, GetHostDEPAssignmentsBySerial now executes:
SELECT host_id, added_at, deleted_at, abm_token_id, mdm_migration_deadline, mdm_migration_completed
FROM host_dep_assignments hdep
WHERE hdep.hardware_serial = ? AND hdep.deleted_at IS NULLThis function is called in hot paths: ACME device attestation challenges and enrollment profile generation. Both are invoked for every ACME certificate issuance and renewal request.
Why existing code does not prevent it
The schema.sql diff confirms the table indexes after this migration are:
PRIMARY KEY (host_id)KEY idx_hdep_response (assign_profile_response, response_updated_at)KEY fk_host_dep_assignments_abm_token_id (abm_token_id)
There is no index on hardware_serial. The previous query used WHERE hdep.host_id IN (SELECT id FROM hosts WHERE hardware_serial = ?), which leveraged the idx_hosts_hardware_serial index on the hosts table plus a primary-key lookup on host_dep_assignments. The new approach eliminates both of those optimizations.
What the impact would be
For large enterprise deployments with tens or hundreds of thousands of DEP-enrolled devices, every ACME certificate lookup/renewal performs a full table scan of host_dep_assignments. This is a direct performance regression on a critical, frequently-invoked code path. The established pattern in this codebase is to index hardware_serial wherever it is queried (the hosts table has idx_hosts_hardware_serial for this reason).
How to fix it
Add a third ALTER TABLE statement to Up_20260401153503:
_, err = tx.Exec("ALTER TABLE host_dep_assignments ADD INDEX idx_hdep_hardware_serial (hardware_serial)")
if err != nil {
return errors.Wrap(err, "add index on host_dep_assignments.hardware_serial")
}Also update schema.sql to include the new key definition.
Step-by-step proof
- Before this PR:
GetHostDEPAssignmentsBySerial("ABC123")runsWHERE hdep.host_id IN (SELECT id FROM hosts WHERE hardware_serial = ?). MySQL usesidx_hosts_hardware_serialto find the host id (index seek, ~1 row), then does a primary-key lookup onhost_dep_assignments-- effectively O(1). - This PR: the migration adds
hardware_serialtohost_dep_assignmentswith no index. The query becomesWHERE hdep.hardware_serial = ?. With no index on this column, MySQL must scan every row inhost_dep_assignmentsto find matches. - On a deployment with 100,000 DEP-enrolled devices, each ACME certificate lookup now reads ~100,000 rows instead of ~1 row. With frequent certificate renewals, this quickly becomes a bottleneck.
- EXPLAIN for the new query would show
type: ALL(full table scan) onhost_dep_assignments; with the index added it would showtype: refusingidx_hdep_hardware_serial.
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41491 - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Added/updated automated tests - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. This should not update any existing timestamp in that table, per their definition: ``` `added_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, <- only set at creation time if null `deleted_at` timestamp NULL DEFAULT NULL, <- not set automatically `response_updated_at` timestamp NULL DEFAULT NULL, <- not set automatically `mdm_migration_deadline` timestamp(6) NULL DEFAULT NULL, <- not set automatically `mdm_migration_completed` timestamp(6) NULL DEFAULT NULL, <- not set automatically ``` - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). --------- Co-authored-by: Jordan Montgomery <elijah.jordan.montgomery@gmail.com>
Related issue: Resolves #41491
Checklist for submitter
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.Testing
Database migrations
This should not update any existing timestamp in that table, per their definition:
COLLATE utf8mb4_unicode_ci).