Fleet versions
- Discovered: 4.90.0-rc.2607281725 (local dev build, revision
ebeec8a26)
- Reproduced: 4.90.0-rc.2607281725 (local dev build, revision
ebeec8a26)
Web browser and operating system: Chrome 150.0.7871.187 running on macOS 26.5.2
💥 Actual behavior
Editing only the Username or only the Password of an existing NDES SCEP certificate authority saves successfully with no validation error, even when the new credentials are invalid. Fleet only performs its live credential check against the real NDES server when the Admin URL field is part of the same edit, so a bad username or password silently passes and the CA is left in a broken state.
The misconfiguration isn't surfaced at save time. It only appears later and indirectly, the next time a host tries to pull a SCEP challenge and hits the NDESInvalidError path — producing "Invalid NDES admin credentials. Fleet couldn't populate $FLEET_VAR_NDES_SCEP_CHALLENGE..." as a profile failure on the host, far from the action that caused it.
🛠️ Expected behavior
Saving a Username-only or Password-only change should run the same live NDES credential validation that an Admin URL edit triggers, and fail with the same "Invalid NDES SCEP admin URL or credentials" error when the credentials are wrong — so the admin finds out immediately, at save time.
🧑💻 Steps to reproduce
These steps:
- On Fleet Premium with a reachable NDES server, configure a working NDES SCEP CA under Settings > Integrations > Certificate enrollment.
- Edit that CA and change only the Username field to something invalid. Leave SCEP URL, Admin URL, and Password untouched.
- Click Save.
- Observe the save succeeds immediately with no validation error, and the CA is now broken.
- For contrast, edit the CA again and change the Admin URL — the live validation runs and correctly rejects bad credentials.
🕯️ More info (optional)
Found during QA of #49298 (which fixed the wording of this error's remediation path but did not touch validation logic). Traced through the code:
Frontend — frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx builds the update payload with deepDifference(fullCertAuthority, originalValues) (~line 96, ndes_scep_proxy case). The payload therefore contains only the fields that actually changed — editing just the Username means admin_url is absent from the request entirely, not sent as unchanged.
Backend — ee/server/service/certificate_authorities.go, validateNDESSCEPProxyUpdate (~line 1406):
func (svc *Service) validateNDESSCEPProxyUpdate(ctx context.Context, ndesSCEP *fleet.NDESSCEPProxyCAUpdatePayload, oldCA *fleet.CertificateAuthority, errPrefix string) error {
if ndesSCEP.URL != nil {
// ...validates SCEP URL only
}
if ndesSCEP.AdminURL != nil {
// builds a full NDESSCEPProxyCA struct (merging old + new Username/Password/URL)
// and calls svc.scepConfigService.ValidateNDESSCEPAdminURL(ctx, NDESProxy) —
// THIS is the live credential check against the real NDES server.
}
return nil
}
The live credential probe (ValidateNDESSCEPAdminURL) is nested entirely inside if ndesSCEP.AdminURL != nil. Because deepDifference leaves AdminURL as nil whenever it isn't the field being edited, that whole block — including the credential check — is skipped for a Username-only or Password-only edit. There is no equivalent if ndesSCEP.Username != nil / if ndesSCEP.Password != nil branch.
Suggested fix: run the live ValidateNDESSCEPAdminURL check when ndesSCEP.Username != nil || ndesSCEP.Password != nil as well, not only when ndesSCEP.AdminURL != nil. This likely needs the same "merge old + new fields into a full NDESSCEPProxyCA struct" logic that already exists in the AdminURL branch, just gated on any of the three credential-bearing fields changing.
Related: #46380, #49298
Fleet versions
ebeec8a26)ebeec8a26)Web browser and operating system: Chrome 150.0.7871.187 running on macOS 26.5.2
💥 Actual behavior
Editing only the Username or only the Password of an existing NDES SCEP certificate authority saves successfully with no validation error, even when the new credentials are invalid. Fleet only performs its live credential check against the real NDES server when the Admin URL field is part of the same edit, so a bad username or password silently passes and the CA is left in a broken state.
The misconfiguration isn't surfaced at save time. It only appears later and indirectly, the next time a host tries to pull a SCEP challenge and hits the
NDESInvalidErrorpath — producing "Invalid NDES admin credentials. Fleet couldn't populate $FLEET_VAR_NDES_SCEP_CHALLENGE..." as a profile failure on the host, far from the action that caused it.🛠️ Expected behavior
Saving a Username-only or Password-only change should run the same live NDES credential validation that an Admin URL edit triggers, and fail with the same "Invalid NDES SCEP admin URL or credentials" error when the credentials are wrong — so the admin finds out immediately, at save time.
🧑💻 Steps to reproduce
These steps:
🕯️ More info (optional)
Found during QA of #49298 (which fixed the wording of this error's remediation path but did not touch validation logic). Traced through the code:
Frontend —
frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsxbuilds the update payload withdeepDifference(fullCertAuthority, originalValues)(~line 96,ndes_scep_proxycase). The payload therefore contains only the fields that actually changed — editing just the Username meansadmin_urlis absent from the request entirely, not sent as unchanged.Backend —
ee/server/service/certificate_authorities.go,validateNDESSCEPProxyUpdate(~line 1406):The live credential probe (
ValidateNDESSCEPAdminURL) is nested entirely insideif ndesSCEP.AdminURL != nil. BecausedeepDifferenceleavesAdminURLasnilwhenever it isn't the field being edited, that whole block — including the credential check — is skipped for a Username-only or Password-only edit. There is no equivalentif ndesSCEP.Username != nil/if ndesSCEP.Password != nilbranch.Suggested fix: run the live
ValidateNDESSCEPAdminURLcheck whenndesSCEP.Username != nil || ndesSCEP.Password != nilas well, not only whenndesSCEP.AdminURL != nil. This likely needs the same "merge old + new fields into a fullNDESSCEPProxyCAstruct" logic that already exists in theAdminURLbranch, just gated on any of the three credential-bearing fields changing.Related: #46380, #49298