Skip to content

Editing only Username or Password on an NDES CA skips credential validation #50260

Description

@marcusallen97

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:

  • Have been confirmed to consistently lead to reproduction in multiple Fleet instances.
  • Describe the workflow that led to the error, but have not yet been reproduced in multiple Fleet instances.
  1. On Fleet Premium with a reachable NDES server, configure a working NDES SCEP CA under Settings > Integrations > Certificate enrollment.
  2. Edit that CA and change only the Username field to something invalid. Leave SCEP URL, Admin URL, and Password untouched.
  3. Click Save.
  4. Observe the save succeeds immediately with no validation error, and the CA is now broken.
  5. 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:

Frontendfrontend/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.

Backendee/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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status
    📥 Inbox

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions