Serialize enrollment install with enrolling.lock

Concurrent enroll-url launches could both pass the enrollment
marker check and run overlapping elevated installs, racing on
LoadOrExchangeAPIKey and overwriting agent.key.

Add an exclusive flock on {configDir}/enrolling.lock for the
full install path and re-check IsEnrolled under that lock so
only one install exchanges a token and configures the device.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-07-15 17:14:33 +02:00
parent 1329f2a28e
commit ae769f52a1
11 changed files with 476 additions and 57 deletions

View File

@@ -36,6 +36,9 @@ var ErrServerURLMismatch = errors.New("server URL does not match persisted confi
// only when serverURL matches the server_url persisted in config.json.
// Otherwise the enrollment token is exchanged, the key and server binding
// are saved to disk immediately, and then the key is returned.
//
// Concurrent callers are serialized with an exclusive lock so only one
// process exchanges a one-shot enrollment token for a given state dir.
func LoadOrExchangeAPIKey(
ctx context.Context,
dir string,
@@ -48,6 +51,12 @@ func LoadOrExchangeAPIKey(
return "", fmt.Errorf("invalid server URL: %w", err)
}
release, err := AcquireEnrollmentLock(dir)
if err != nil {
return "", fmt.Errorf("cannot acquire enrollment lock: %w", err)
}
defer release()
apiKey, err := LoadAPIKey(dir)
if err == nil {
if err := validatePersistedServerURL(dir, normalized); err != nil {