From b6b024b18f323eb86eb367c1f52f8035d9436451 Mon Sep 17 00:00:00 2001 From: "Nilson.Henao" Date: Sun, 20 Sep 2026 15:15:15 +0000 Subject: [PATCH] fix: name agent clock skew instead of reporting it as a controller outage When an agent's clock runs ahead of the controller's by more than the 10 s JWT tolerance, every request it signs fails verification with JWTClaimValidationFailed on the "nbf" or "iat" claim. The controller mapped that to a generic authentication failure, so the agent's log said the controller was unreachable or unauthorised and the real cause -- a clock a few seconds ahead -- was nowhere in either log. Measured on Controller 3.8.2 with Edgelet v1.0.3-rc.1 and a deliberate +24 h step on the node: reconciliation stalled and neither side named the clock. This classifies those two claim failures as a retryable ServiceUnavailableError with a new CONTROLLER_AGENT_CLOCK_SKEW code and the message "Agent clock is ahead of the controller: ...". Retryable is deliberate, and unchanged from today's behaviour in effect: an agent that receives non-retryable auth failures deprovisions itself after five attempts (edgelet internal/fieldagent/status_auth_gate.go), which would turn a few seconds of drift into a node that must be bootstrapped again. Only the diagnosis was missing. Signed-off-by: Nilson.Henao Co-Authored-By: Claude Opus 5 (1M context) --- src/helpers/agent-auth-error-codes.js | 1 + src/helpers/agent-auth-error-utils.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/helpers/agent-auth-error-codes.js b/src/helpers/agent-auth-error-codes.js index 77f4e15f..e10dff0f 100644 --- a/src/helpers/agent-auth-error-codes.js +++ b/src/helpers/agent-auth-error-codes.js @@ -11,5 +11,6 @@ module.exports = { CONTROLLER_DB_BUSY: 'CONTROLLER_DB_BUSY', CONTROLLER_VAULT_UNAVAILABLE: 'CONTROLLER_VAULT_UNAVAILABLE', CONTROLLER_AUTH_VERIFICATION_FAILED: 'CONTROLLER_AUTH_VERIFICATION_FAILED', + CONTROLLER_AGENT_CLOCK_SKEW: 'CONTROLLER_AGENT_CLOCK_SKEW', CONTROLLER_NOT_READY: 'CONTROLLER_NOT_READY' } diff --git a/src/helpers/agent-auth-error-utils.js b/src/helpers/agent-auth-error-utils.js index e7b7d36d..f9d9722c 100644 --- a/src/helpers/agent-auth-error-utils.js +++ b/src/helpers/agent-auth-error-utils.js @@ -54,6 +54,21 @@ function classifyVerifyJwtFailure (error) { if (errorName === 'JWSSignatureVerificationFailed' || message.includes('signature verification failed') || message.includes('invalid signature')) { return new AgentAuthenticationError(CODES.AGENT_JWT_SIGNATURE_INVALID, 'Agent JWT signature invalid') } + if (errorName === 'JWTClaimValidationFailed' && + (message.includes('"nbf" claim timestamp check failed') || message.includes('"iat" claim timestamp check failed'))) { + // The agent signs every request with nbf = iat = its own clock, and the + // controller verifies with a 10 s tolerance, so this means the agent's + // clock runs ahead of the controller's. Deliberately reported as a + // retryable ServiceUnavailableError, not as a 401: an agent that gets + // non-retryable auth failures deprovisions itself after 5 attempts over + // 60 s (edgelet internal/fieldagent/status_auth_gate.go), which would + // turn a few seconds of clock drift into a node that has to be + // bootstrapped again. Retrying is right; only the diagnosis was missing. + return new ServiceUnavailableError( + CODES.CONTROLLER_AGENT_CLOCK_SKEW, + `Agent clock is ahead of the controller: ${message}` + ) + } if (isVaultInfrastructureError(error)) { return new ServiceUnavailableError(CODES.CONTROLLER_VAULT_UNAVAILABLE, 'Vault temporarily unavailable') }