EST certificate proxy backend and configs (#34689)

#34275
This commit is contained in:
Dante Catalfamo 2025-11-04 16:27:15 -05:00 committed by GitHub
parent ac69cb7bcc
commit 37722a925f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 815 additions and 107 deletions

View file

@ -25,9 +25,9 @@ import (
"github.com/fleetdm/fleet/v4/ee/server/scim"
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
"github.com/fleetdm/fleet/v4/ee/server/service/est"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
"github.com/fleetdm/fleet/v4/ee/server/service/hydrant"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/pkg/scripts"
"github.com/fleetdm/fleet/v4/server"
@ -802,7 +802,7 @@ the way that the Fleet server works.
var softwareTitleIconStore fleet.SoftwareTitleIconStore
var distributedLock fleet.Lock
if license.IsPremium() {
hydrantService := hydrant.NewService(hydrant.WithLogger(logger))
hydrantService := est.NewService(est.WithLogger(logger))
profileMatcher := apple_mdm.NewProfileMatcher(redisPool)
if config.S3.SoftwareInstallersBucket != "" {
if config.S3.BucketsAndPrefixesMatch() {

View file

@ -859,6 +859,15 @@ func (cmd *GenerateGitopsCommand) generateCertificateAuthorities(filePath string
})
}
}
if estCA, ok := result["custom_est_proxy"]; ok && estCA != nil {
for _, intg := range estCA.([]any) {
intg.(map[string]interface{})["password"] = cmd.AddComment(filePath, "TODO: Add your EST password here")
cmd.Messages.SecretWarnings = append(cmd.Messages.SecretWarnings, SecretWarning{
Filename: "default.yml",
Key: "certificate_authorities.custom_est_proxy.password",
})
}
}
if smallstep, ok := result["smallstep"]; ok && smallstep != nil {
for _, intg := range smallstep.([]interface{}) {
intg.(map[string]interface{})["password"] = cmd.AddComment(filePath, "TODO: Add your Smallstep password here")

View file

@ -559,6 +559,14 @@ func (MockClient) GetCertificateAuthoritiesSpec(includeSecrets bool) (*fleet.Gro
ClientSecret: maskSecret("some-hydrant-client-secret", includeSecrets),
},
},
EST: []fleet.ESTProxyCA{
{
Name: "some-est-name",
URL: "https://some-est-url.example.com",
Username: "some-est-username",
Password: maskSecret("some-est-password", includeSecrets),
},
},
Smallstep: []fleet.SmallstepSCEPProxyCA{
{
Name: "some-smallstep-name",

View file

@ -1,4 +1,9 @@
certificate_authorities:
custom_est_proxy:
- name: some-est-name
password: some-est-password
url: https://some-est-url.example.com
username: some-est-username
custom_scep_proxy:
- challenge: some-custom-scep-proxy-challenge
name: some-custom-scep-proxy-name

View file

@ -1,4 +1,9 @@
certificate_authorities:
custom_est_proxy:
- name: some-est-name
password: ___GITOPS_COMMENT_7___
url: https://some-est-url.example.com
username: some-est-username
custom_scep_proxy:
- challenge: ___GITOPS_COMMENT_5___
name: some-custom-scep-proxy-name
@ -26,9 +31,9 @@ certificate_authorities:
smallstep:
- challenge_url: https://some-smallstep-challenge-url.com
name: some-smallstep-name
password: ___GITOPS_COMMENT_7___
password: ___GITOPS_COMMENT_8___
url: https://some-smallstep-url.com
username: ___GITOPS_COMMENT_8___
username: ___GITOPS_COMMENT_9___
features:
enable_host_users: true
enable_software_inventory: true
@ -75,8 +80,8 @@ mdm:
entity_id: some-mdm-entity-id.com
idp_name: some-other-idp-name
issuer_uri: https://some-mdm-issuer-uri.com
metadata: ___GITOPS_COMMENT_9___
metadata_url: ___GITOPS_COMMENT_10___
metadata: ___GITOPS_COMMENT_10___
metadata_url: ___GITOPS_COMMENT_11___
end_user_license_agreement: ./lib/eula/test.pdf
volume_purchasing_program:
- location: Fleet Device Management Inc.
@ -91,7 +96,7 @@ org_info:
org_logo_url_light_background: http://some-org-logo-url-light-background.com
org_name: Fleet
secrets:
- secret: ___GITOPS_COMMENT_11___
- secret: ___GITOPS_COMMENT_12___
server_settings:
ai_features_disabled: false
debug_host_ids:
@ -111,8 +116,8 @@ sso_settings:
entity_id: dogfood.fleetdm.com
idp_image_url: http://some-sso-idp-image-url.com
idp_name: some-idp-name
metadata: ___GITOPS_COMMENT_12___
metadata_url: ___GITOPS_COMMENT_13___
metadata: ___GITOPS_COMMENT_13___
metadata_url: ___GITOPS_COMMENT_14___
sso_server_url: https://sso.fleetdm.com
webhook_settings:
activities_webhook:

View file

@ -50,6 +50,11 @@ labels:
name: Label C
org_settings:
certificate_authorities:
custom_est_proxy:
- name: some-est-name
password: # TODO: Add your EST password here
url: https://some-est-url.example.com
username: some-est-username
custom_scep_proxy:
- challenge: # TODO: Add your custom SCEP proxy challenge here
name: some-custom-scep-proxy-name
@ -178,7 +183,7 @@ policies:
critical: false
description: This is a global policy
install_software:
hash_sha256: ___GITOPS_COMMENT_14___
hash_sha256: ___GITOPS_COMMENT_15___
labels_include_any:
- Label A
- Label B

View file

@ -32,6 +32,11 @@ labels:
name: Label C
org_settings:
certificate_authorities:
custom_est_proxy:
- name: some-est-name
password: # TODO: Add your EST password here
url: https://some-est-url.example.com
username: some-est-username
custom_scep_proxy:
- challenge: # TODO: Add your custom SCEP proxy challenge here
name: some-custom-scep-proxy-name

View file

@ -1855,6 +1855,51 @@ This activity contains the following fields:
}
```
## added_custom_est_proxy
Generated when a custom EST certificate authority configuration is added in Fleet.
This activity contains the following fields:
- "name": Name of the certificate authority.
#### Example
```json
{
"name": "EST_WIFI"
}
```
## deleted_custom_est_proxy
Generated when a custom EST certificate authority configuration is deleted in Fleet.
This activity contains the following fields:
- "name": Name of the certificate authority.
#### Example
```json
{
"name": "EST_WIFI"
}
```
## edited_custom_est_proxy
Generated when a custom EST certificate authority configuration is edited in Fleet.
This activity contains the following fields:
- "name": Name of the certificate authority.
#### Example
```json
{
"name": "EST_WIFI"
}
```
## added_smallstep
Generated when Smallstep certificate authority configuration is added in Fleet.

View file

@ -91,6 +91,22 @@ func (svc *Service) NewCertificateAuthority(ctx context.Context, p fleet.Certifi
activity = fleet.ActivityAddedHydrant{}
}
if p.CustomESTProxy != nil {
p.CustomESTProxy.Preprocess()
if err := svc.validateEST(ctx, p.CustomESTProxy, errPrefix); err != nil {
return nil, err
}
caToCreate.Type = string(fleet.CATypeCustomESTProxy)
caToCreate.Name = &p.CustomESTProxy.Name
caToCreate.URL = &p.CustomESTProxy.URL
caToCreate.Username = &p.CustomESTProxy.Username
caToCreate.Password = &p.CustomESTProxy.Password
caDisplayType = "custom EST"
activity = fleet.ActivityAddedCustomESTProxy{}
}
if p.NDESSCEPProxy != nil {
p.NDESSCEPProxy.Preprocess()
@ -175,6 +191,9 @@ func (svc *Service) validatePayload(p *fleet.CertificateAuthorityPayload, errPre
if p.Smallstep != nil {
casToCreate++
}
if p.CustomESTProxy != nil {
casToCreate++
}
if casToCreate == 0 {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sA certificate authority must be specified", errPrefix)}
}
@ -311,12 +330,37 @@ func (svc *Service) validateHydrant(ctx context.Context, hydrantCA *fleet.Hydran
if hydrantCA.ClientSecret == "" || hydrantCA.ClientSecret == fleet.MaskedPassword {
return fleet.NewInvalidArgumentError("client_secret", fmt.Sprintf("%sInvalid Hydrant Client Secret. Please correct and try again.", errPrefix))
}
if err := svc.hydrantService.ValidateHydrantURL(ctx, *hydrantCA); err != nil {
if err := svc.estService.ValidateESTURL(ctx, fleet.ESTProxyCA{
ID: hydrantCA.ID,
Name: hydrantCA.Name,
URL: hydrantCA.URL,
Username: hydrantCA.ClientID,
Password: hydrantCA.ClientSecret,
}); err != nil {
return fleet.NewInvalidArgumentError("url", fmt.Sprintf("%sInvalid Hydrant URL. Please correct and try again.", errPrefix))
}
return nil
}
func (svc *Service) validateEST(ctx context.Context, estProxyCA *fleet.ESTProxyCA, errPrefix string) error {
if err := validateCAName(estProxyCA.Name, errPrefix); err != nil {
return err
}
if err := validateURL(estProxyCA.URL, "EST", errPrefix); err != nil {
return err
}
if estProxyCA.Username == "" {
return fleet.NewInvalidArgumentError("username", fmt.Sprintf("%sInvalid EST Username. Please correct and try again.", errPrefix))
}
if estProxyCA.Password == "" || estProxyCA.Password == fleet.MaskedPassword {
return fleet.NewInvalidArgumentError("password", fmt.Sprintf("%sInvalid EST Password. Please correct and try again.", errPrefix))
}
if err := svc.estService.ValidateESTURL(ctx, *estProxyCA); err != nil {
return fleet.NewInvalidArgumentError("url", fmt.Sprintf("%sInvalid EST URL. Please correct and try again.", errPrefix))
}
return nil
}
func validateURL(caURL, displayType, errPrefix string) error {
if u, err := url.ParseRequestURI(caURL); err != nil {
return fleet.NewInvalidArgumentError("url", fmt.Sprintf("%sInvalid %s URL. Please correct and try again.", errPrefix, displayType))
@ -425,6 +469,10 @@ func (svc *Service) DeleteCertificateAuthority(ctx context.Context, certificateA
activity = fleet.ActivityDeletedSmallstep{
Name: ca.Name,
}
case string(fleet.CATypeCustomESTProxy):
activity = fleet.ActivityDeletedCustomESTProxy{
Name: ca.Name,
}
}
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), activity); err != nil {
@ -528,6 +576,16 @@ func (svc *Service) getCertificateAuthoritiesBatchOperations(ctx context.Context
return nil, err
}
}
for _, ca := range incoming.EST {
if strings.TrimSpace(ca.Name) == "" {
return nil, fleet.NewInvalidArgumentError("name", "certificate_authorities.custom_est_proxy: CA name cannot be empty.")
}
ca.Preprocess()
if err := checkAllNames(ca.Name, "custom_est_proxy", "Custom EST Proxy"); err != nil {
return nil, err
}
}
// preprocess smallstep
for _, ca := range incoming.Smallstep {
if ca.Name == "" {
@ -556,6 +614,9 @@ func (svc *Service) getCertificateAuthoritiesBatchOperations(ctx context.Context
if err := svc.processHydrantCAs(ctx, batchOps, incoming.Hydrant, existing.Hydrant); err != nil {
return nil, err
}
if err := svc.processESTCAs(ctx, batchOps, incoming.EST, existing.EST); err != nil {
return nil, err
}
if err := svc.processSmallstepCAs(ctx, batchOps, incoming.Smallstep, existing.Smallstep); err != nil {
return nil, err
}
@ -800,6 +861,59 @@ func (svc *Service) processHydrantCAs(ctx context.Context, batchOps *fleet.Certi
return nil
}
func (svc *Service) processESTCAs(ctx context.Context, batchOps *fleet.CertificateAuthoritiesBatchOperations, incomingCAs []fleet.ESTProxyCA, existingCAs []fleet.ESTProxyCA) error {
incomingByName := make(map[string]*fleet.ESTProxyCA)
for _, incoming := range incomingCAs {
// Note: caller is responsible for ensuring incoming list has no duplicates
incomingByName[incoming.Name] = &incoming
}
existingByName := make(map[string]*fleet.ESTProxyCA)
for _, existing := range existingCAs {
// if existing CA isn't in the incoming list, we should delete it
if _, ok := incomingByName[existing.Name]; !ok {
batchOps.Delete = append(batchOps.Delete, &fleet.CertificateAuthority{
Type: string(fleet.CATypeCustomESTProxy),
Name: &existing.Name,
URL: &existing.URL,
Username: &existing.Username,
Password: &existing.Password,
})
}
// Note: datastore is responsible for ensuring no existing list has no duplicates
existingByName[existing.Name] = &existing
}
for name, incoming := range incomingByName {
if err := svc.validateEST(ctx, incoming, "certificate_authorities.custom_est_proxy: "); err != nil {
return err
}
// create the payload to be added or updated
if _, ok := existingByName[name]; ok {
// update existing
batchOps.Update = append(batchOps.Update, &fleet.CertificateAuthority{
Type: string(fleet.CATypeCustomESTProxy),
Name: &incoming.Name,
URL: &incoming.URL,
Username: &incoming.Username,
Password: &incoming.Password,
})
} else {
// add new
batchOps.Add = append(batchOps.Add, &fleet.CertificateAuthority{
Type: string(fleet.CATypeCustomESTProxy),
Name: &incoming.Name,
URL: &incoming.URL,
Username: &incoming.Username,
Password: &incoming.Password,
})
}
}
return nil
}
func (svc *Service) processSmallstepCAs(ctx context.Context, batchOps *fleet.CertificateAuthoritiesBatchOperations, incomingCAs []fleet.SmallstepSCEPProxyCA, existingCAs []fleet.SmallstepSCEPProxyCA) error {
incomingByName := make(map[string]*fleet.SmallstepSCEPProxyCA)
for _, incoming := range incomingCAs {
@ -881,6 +995,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityAddedHydrant{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for added hydrant")
}
case string(fleet.CATypeCustomESTProxy):
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityAddedCustomESTProxy{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for added custom est proxy")
}
case string(fleet.CATypeSmallstep):
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityAddedSmallstep{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for added smallstep SCEP proxy")
@ -905,6 +1023,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEditedHydrant{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for edited hydrant")
}
case string(fleet.CATypeCustomESTProxy):
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEditedCustomESTProxy{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activityu for edited custom EST proxy")
}
case string(fleet.CATypeSmallstep):
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEditedSmallstep{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for edited smallstep SCEP proxy")
@ -929,6 +1051,10 @@ func (svc *Service) recordActivitiesBatchApplyCAs(ctx context.Context, ops *flee
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDeletedHydrant{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for deleted hydrant")
}
case string(fleet.CATypeCustomESTProxy):
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDeletedCustomESTProxy{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for deleted custom EST proxy")
}
case string(fleet.CATypeSmallstep):
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDeletedSmallstep{Name: *ca.Name}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for deleted smallstep SCEP proxy")
@ -962,7 +1088,8 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
var activity fleet.ActivityDetails
var caActivityName string
if p.DigiCertCAUpdatePayload != nil {
switch {
case p.DigiCertCAUpdatePayload != nil:
if p.DigiCertCAUpdatePayload.IsEmpty() {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sDigiCert CA update payload is empty", errPrefix)}
}
@ -989,8 +1116,7 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
caActivityName = *oldCA.Name
}
activity = fleet.ActivityEditedDigiCert{Name: caActivityName}
}
if p.HydrantCAUpdatePayload != nil {
case p.HydrantCAUpdatePayload != nil:
if p.HydrantCAUpdatePayload.IsEmpty() {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sHydrant CA update payload is empty", errPrefix)}
}
@ -1013,8 +1139,30 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
caActivityName = *oldCA.Name
}
activity = fleet.ActivityEditedHydrant{Name: caActivityName}
}
if p.NDESSCEPProxyCAUpdatePayload != nil {
case p.CustomESTCAUpdatePayload != nil:
if p.CustomESTCAUpdatePayload.IsEmpty() {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sCustom EST CA update payload is empty", errPrefix)}
}
if err := p.CustomESTCAUpdatePayload.ValidateRelatedFields(errPrefix, *oldCA.Name); err != nil {
return err
}
p.CustomESTCAUpdatePayload.Preprocess()
if err := svc.validateCustomESTUpdate(ctx, p.CustomESTCAUpdatePayload, oldCA, errPrefix); err != nil {
return err
}
caToUpdate.Type = string(fleet.CATypeCustomESTProxy)
caToUpdate.Name = p.CustomESTCAUpdatePayload.Name
caToUpdate.URL = p.CustomESTCAUpdatePayload.URL
caToUpdate.Username = p.CustomESTCAUpdatePayload.Username
caToUpdate.Password = p.CustomESTCAUpdatePayload.Password
if caToUpdate.Name != nil {
caActivityName = *caToUpdate.Name
} else {
caActivityName = *oldCA.Name
}
activity = fleet.ActivityEditedCustomESTProxy{Name: caActivityName}
case p.NDESSCEPProxyCAUpdatePayload != nil:
if p.NDESSCEPProxyCAUpdatePayload.IsEmpty() {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sNDES SCEP Proxy CA update payload is empty", errPrefix)}
}
@ -1037,8 +1185,7 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
caActivityName = *oldCA.Name
}
activity = fleet.ActivityEditedNDESSCEPProxy{}
}
if p.CustomSCEPProxyCAUpdatePayload != nil {
case p.CustomSCEPProxyCAUpdatePayload != nil:
if p.CustomSCEPProxyCAUpdatePayload.IsEmpty() {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sCustom SCEP Proxy CA update payload is empty", errPrefix)}
}
@ -1061,8 +1208,7 @@ func (svc *Service) UpdateCertificateAuthority(ctx context.Context, id uint, p f
}
activity = fleet.ActivityEditedCustomSCEPProxy{Name: caActivityName}
}
if p.SmallstepSCEPProxyCAUpdatePayload != nil {
case p.SmallstepSCEPProxyCAUpdatePayload != nil:
if p.SmallstepSCEPProxyCAUpdatePayload.IsEmpty() {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sSmallstep SCEP Proxy CA update payload is empty", errPrefix)}
}
@ -1196,10 +1342,10 @@ func (svc *Service) validateHydrantUpdate(ctx context.Context, hydrant *fleet.Hy
return err
}
hydrantCAToVerify := fleet.HydrantCA{ // The hydrant service for verification only requires the URL.
hydrantCAToVerify := fleet.ESTProxyCA{ // The hydrant service for verification only requires the URL.
URL: *hydrant.URL,
}
if err := svc.hydrantService.ValidateHydrantURL(ctx, hydrantCAToVerify); err != nil {
if err := svc.estService.ValidateESTURL(ctx, hydrantCAToVerify); err != nil {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid Hydrant URL. Please correct and try again.", errPrefix)}
}
}
@ -1217,6 +1363,38 @@ func (svc *Service) validateHydrantUpdate(ctx context.Context, hydrant *fleet.Hy
return nil
}
func (svc *Service) validateCustomESTUpdate(ctx context.Context, estUpdate *fleet.CustomESTCAUpdatePayload, oldCA *fleet.CertificateAuthority, errPrefix string) error {
if estUpdate.Name != nil {
if err := validateCAName(*estUpdate.Name, errPrefix); err != nil {
return err
}
}
if estUpdate.URL != nil {
if err := validateURL(*estUpdate.URL, "EST", errPrefix); err != nil {
return err
}
hydrantCAToVerify := fleet.ESTProxyCA{ // The EST service for verification only requires the URL.
URL: *estUpdate.URL,
}
if err := svc.estService.ValidateESTURL(ctx, hydrantCAToVerify); err != nil {
return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid EST URL. Please correct and try again.", errPrefix)}
}
}
if estUpdate.Username != nil && *estUpdate.Username == "" {
return &fleet.BadRequestError{
Message: fmt.Sprintf("%sInvalid EST Username. Please correct and try again.", errPrefix),
}
}
if estUpdate.Password != nil && *estUpdate.Password == "" {
return &fleet.BadRequestError{
Message: fmt.Sprintf("%sInvalid EST Password. Please correct and try again.", errPrefix),
}
}
return nil
}
func (svc *Service) validateNDESSCEPProxyUpdate(ctx context.Context, ndesSCEP *fleet.NDESSCEPProxyCAUpdatePayload, oldCA *fleet.CertificateAuthority, errPrefix string) error {
// some methods in this fuction require the NDESSCEPProxyCA type so we convert the ndes update payload here

View file

@ -12,7 +12,7 @@ import (
"testing"
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
"github.com/fleetdm/fleet/v4/ee/server/service/hydrant"
"github.com/fleetdm/fleet/v4/ee/server/service/est"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql"
@ -204,7 +204,7 @@ func TestCreatingCertificateAuthorities(t *testing.T) {
ds: ds,
authz: authorizer,
digiCertService: digicert.NewService(),
hydrantService: hydrant.NewService(),
estService: est.NewService(),
scepConfigService: &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil },
@ -1199,7 +1199,7 @@ func TestUpdatingCertificateAuthorities(t *testing.T) {
ds: ds,
authz: authorizer,
digiCertService: digicert.NewService(),
hydrantService: hydrant.NewService(),
estService: est.NewService(),
scepConfigService: &scep_mock.SCEPConfigService{
ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil },
ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil },

View file

@ -1,4 +1,4 @@
package hydrant
package est
import (
"context"
@ -25,17 +25,17 @@ type Service struct {
client *http.Client
}
// Compile-time check for HydrantService interface
var _ fleet.HydrantService = (*Service)(nil)
// Compile-time check for ESTService interface
var _ fleet.ESTService = (*Service)(nil)
func NewService(opts ...Opt) fleet.HydrantService {
func NewService(opts ...Opt) fleet.ESTService {
s := &Service{}
s.populateOpts(opts)
s.client = fleethttp.NewClient(fleethttp.WithTimeout(s.timeout))
return s
}
// Opt is the type for Hydrant integration options.
// Opt is the type for EST integration options.
type Opt func(*Service)
// WithTimeout sets the timeout to use for the HTTP client.
@ -64,60 +64,60 @@ func (s *Service) populateOpts(opts []Opt) {
}
}
func (s *Service) ValidateHydrantURL(ctx context.Context, hydrantCA fleet.HydrantCA) error {
reqURL := hydrantCA.URL + "/cacerts"
func (s *Service) ValidateESTURL(ctx context.Context, estCA fleet.ESTProxyCA) error {
reqURL := estCA.URL + "/cacerts"
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil {
return ctxerr.Wrap(ctx, err, "creating Hydrant CA request")
return ctxerr.Wrap(ctx, err, "creating EST CA request")
}
resp, err := s.client.Do(req)
if err != nil {
return ctxerr.Wrap(ctx, err, "sending Hydrant CA request")
return ctxerr.Wrap(ctx, err, "sending EST CA request")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ctxerr.Errorf(ctx, "unexpected Hydrant CA status code: %d", resp.StatusCode)
return ctxerr.Errorf(ctx, "unexpected EST CA status code: %d", resp.StatusCode)
}
contentType := resp.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "application/pkcs7-mime") {
return ctxerr.Errorf(ctx, "unexpected Hydrant CA content type: %s", contentType)
return ctxerr.Errorf(ctx, "unexpected EST CA content type: %s", contentType)
}
// For now we are just verifying that there is a body of the reportedly correct format. We could
// possibly do more. A better implementation would be similar to Digicert's which validates the
// credentials in addition to the URL but I don't see a way to do that with Hydrant's API.
caCerts, err := io.ReadAll(resp.Body)
if err != nil {
return ctxerr.Wrap(ctx, err, "reading Hydrant CA response body")
return ctxerr.Wrap(ctx, err, "reading EST CA response body")
}
if len(caCerts) == 0 {
return ctxerr.Errorf(ctx, "no CA certificates found in Hydrant CA /cacerts response. URL may be incorrect")
return ctxerr.Errorf(ctx, "no CA certificates found in EST CA /cacerts response. URL may be incorrect")
}
return nil
}
func (s *Service) GetCertificate(ctx context.Context, hydrantCA fleet.HydrantCA, csr string) (*fleet.HydrantCertificate, error) {
reqURL, err := url.Parse(hydrantCA.URL + "/simpleenroll")
func (s *Service) GetCertificate(ctx context.Context, estCA fleet.ESTProxyCA, csr string) (*fleet.ESTCertificate, error) {
reqURL, err := url.Parse(estCA.URL + "/simpleenroll")
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "parsing Hydrant CA URL")
return nil, ctxerr.Wrap(ctx, err, "parsing EST CA URL")
}
apiCredential := hydrantCA.ClientID + ":" + hydrantCA.ClientSecret
apiCredential := estCA.Username + ":" + estCA.Password
encodedCredential := base64.StdEncoding.EncodeToString([]byte(apiCredential))
hydrantRequest, err := http.NewRequestWithContext(ctx, "POST", reqURL.String(), strings.NewReader(csr))
estRequest, err := http.NewRequestWithContext(ctx, "POST", reqURL.String(), strings.NewReader(csr))
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creating Hydrant CA request")
return nil, ctxerr.Wrap(ctx, err, "creating EST CA request")
}
hydrantRequest.Header.Set("Content-Type", "application/pkcs10")
hydrantRequest.Header.Set("Accept", "application/pkcs7-mime")
hydrantRequest.Header.Set("Authorization", "Basic "+encodedCredential)
resp, err := s.client.Do(hydrantRequest)
estRequest.Header.Set("Content-Type", "application/pkcs10")
estRequest.Header.Set("Accept", "application/pkcs7-mime")
estRequest.Header.Set("Authorization", "Basic "+encodedCredential)
resp, err := s.client.Do(estRequest)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "sending Hydrant CA request")
return nil, ctxerr.Wrap(ctx, err, "sending EST CA request")
}
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "reading Hydrant CA response body")
return nil, ctxerr.Wrap(ctx, err, "reading EST CA response body")
}
if resp.StatusCode != http.StatusOK {
bytesToLog := bytes
@ -125,11 +125,11 @@ func (s *Service) GetCertificate(ctx context.Context, hydrantCA fleet.HydrantCA,
if len(bytes) > 1000 {
bytesToLog = bytes[:1000]
}
s.logger.Log("msg", "unexpected Hydrant CA status code", "status_code", resp.StatusCode, "response_body", string(bytesToLog))
return nil, ctxerr.Errorf(ctx, "unexpected Hydrant CA status code: %d", resp.StatusCode)
s.logger.Log("msg", "unexpected EST CA status code", "status_code", resp.StatusCode, "response_body", string(bytesToLog))
return nil, ctxerr.Errorf(ctx, "unexpected EST CA status code: %d", resp.StatusCode)
}
return &fleet.HydrantCertificate{
return &fleet.ESTCertificate{
Certificate: bytes,
}, nil
}

View file

@ -28,11 +28,24 @@ func (svc *Service) RequestCertificate(ctx context.Context, p fleet.RequestCerti
if err != nil {
return nil, err
}
if ca.Type != string(fleet.CATypeHydrant) {
return nil, &fleet.BadRequestError{Message: "This API currently only supports Hydrant Certificate Authorities."}
if ca.Type != string(fleet.CATypeHydrant) && ca.Type != string(fleet.CATypeCustomESTProxy) {
return nil, &fleet.BadRequestError{Message: "This API currently only supports Hydrant and EST Certificate Authorities."}
}
if ca.ClientSecret == nil {
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a client secret configured."}
if ca.Type == string(fleet.CATypeHydrant) {
if ca.ClientID == nil {
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a username configured."}
}
if ca.ClientSecret == nil {
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a client secret configured."}
}
}
if ca.Type == string(fleet.CATypeCustomESTProxy) {
if ca.Username == nil {
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a username configured."}
}
if ca.Password == nil {
return nil, &fleet.BadRequestError{Message: "Certificate authority does not have a password configured."}
}
}
certificateRequest, err := svc.parseCSR(ctx, p.CSR)
if err != nil {
@ -86,21 +99,33 @@ func (svc *Service) RequestCertificate(ctx context.Context, p fleet.RequestCerti
csrForRequest = strings.ReplaceAll(csrForRequest, "-----END CERTIFICATE REQUEST-----", "")
csrForRequest = strings.ReplaceAll(csrForRequest, "\\n", "")
certificate, err := svc.hydrantService.GetCertificate(ctx, fleet.HydrantCA{
Name: *ca.Name,
URL: *ca.URL,
ClientID: *ca.ClientID,
ClientSecret: *ca.ClientSecret,
}, csrForRequest)
var estCA fleet.ESTProxyCA
if ca.Type == string(fleet.CATypeHydrant) {
estCA = fleet.ESTProxyCA{
Name: *ca.Name,
URL: *ca.URL,
Username: *ca.ClientID,
Password: *ca.ClientSecret,
}
} else {
estCA = fleet.ESTProxyCA{
Name: *ca.Name,
URL: *ca.URL,
Username: *ca.Username,
Password: *ca.Password,
}
}
certificate, err := svc.estService.GetCertificate(ctx, estCA, csrForRequest)
if err != nil {
level.Error(svc.logger).Log("msg", "Hydrant certificate request failed", "ca_id", ca.ID, "error", err)
level.Error(svc.logger).Log("msg", "EST certificate request failed", "ca_id", ca.ID, "error", err)
// Bad request may seem like a strange error here but there are many cases where a malformed
// CSR can cause this error and Hydrant's API often returns a 5XX error even in these cases
// so it is not always possible to distinguish between an error caused by a bad request or
// an internal CA error.
return nil, &fleet.BadRequestError{Message: fmt.Sprintf("Hydrant certificate request failed: %s", err.Error())}
return nil, &fleet.BadRequestError{Message: fmt.Sprintf("EST certificate request failed: %s", err.Error())}
}
level.Info(svc.logger).Log("msg", "Successfully retrieved a certificate from Hydrant", "ca_id", ca.ID, "idp_username", idpUsername)
level.Info(svc.logger).Log("msg", "Successfully retrieved a certificate from EST", "ca_id", ca.ID, "idp_username", idpUsername)
// Wrap the certificate in a PEM block for easier consumption by the client
return ptr.String("-----BEGIN CERTIFICATE-----\n" + string(certificate.Certificate) + "\n-----END CERTIFICATE-----\n"), nil
}

View file

@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/fleetdm/fleet/v4/ee/server/service/hydrant"
"github.com/fleetdm/fleet/v4/ee/server/service/est"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql"
@ -118,6 +118,14 @@ func TestRequestCertificate(t *testing.T) {
APIToken: ptr.String("test-api-token"),
ProfileID: ptr.String("test-profile-id"),
}
customESTCA := &fleet.CertificateAuthority{
ID: 3,
Name: ptr.String("TestCustomESTCA"),
Type: string(fleet.CATypeCustomESTProxy),
URL: &mockHydrantServer.URL,
Username: ptr.String("test-username"),
Password: ptr.String("test-password"),
}
baseSetupForTests := func() (*Service, context.Context) {
ds := new(mock.Store)
@ -125,7 +133,7 @@ func TestRequestCertificate(t *testing.T) {
// Setup DS mocks
ds.GetCertificateAuthorityByIDFunc = func(ctx context.Context, id uint, includeSecrets bool) (*fleet.CertificateAuthority, error) {
require.True(t, includeSecrets, "RequestCertificate should always fetch secrets")
for _, ca := range []*fleet.CertificateAuthority{hydrantCA, digicertCA} {
for _, ca := range []*fleet.CertificateAuthority{hydrantCA, digicertCA, customESTCA} {
if ca.ID == id {
return ca, nil
}
@ -142,9 +150,9 @@ func TestRequestCertificate(t *testing.T) {
logger: logger,
ds: ds,
authz: authorizer,
hydrantService: hydrant.NewService(
hydrant.WithTimeout(2*time.Second),
hydrant.WithLogger(logger),
estService: est.NewService(
est.WithTimeout(2*time.Second),
est.WithLogger(logger),
),
}
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
@ -210,7 +218,7 @@ func TestRequestCertificate(t *testing.T) {
IDPToken: ptr.String("test-idp-token"),
IDPClientID: ptr.String("test-client-id"), // Missing client ID
})
require.ErrorContains(t, err, "Hydrant certificate request failed")
require.ErrorContains(t, err, "EST certificate request failed")
require.Nil(t, cert)
})
@ -263,7 +271,18 @@ func TestRequestCertificate(t *testing.T) {
require.Nil(t, cert)
})
t.Run("Request certificate - non-Hydrant CA", func(t *testing.T) {
t.Run("Request a certificate - Custom EST CA", func(t *testing.T) {
svc, ctx := baseSetupForTests()
cert, err := svc.RequestCertificate(ctx, fleet.RequestCertificatePayload{
ID: customESTCA.ID,
CSR: goodCSR,
})
require.NoError(t, err)
require.NotNil(t, cert)
require.Equal(t, "-----BEGIN CERTIFICATE-----\n"+hydrantSimpleEnrollResponse+"\n-----END CERTIFICATE-----\n", *cert)
})
t.Run("Request certificate - non-Hydrant and non-EST CA", func(t *testing.T) {
svc, ctx := baseSetupForTests()
_, err := svc.RequestCertificate(ctx, fleet.RequestCertificatePayload{
ID: digicertCA.ID,
@ -272,7 +291,7 @@ func TestRequestCertificate(t *testing.T) {
IDPToken: ptr.String("test-idp-token"),
IDPClientID: ptr.String("test-idp-client-id"),
})
require.ErrorContains(t, err, "This API currently only supports Hydrant Certificate Authorities.")
require.ErrorContains(t, err, "This API currently only supports Hydrant and EST Certificate Authorities.")
})
t.Run("Request certificate - nonexistent CA", func(t *testing.T) {

View file

@ -34,7 +34,7 @@ type Service struct {
keyValueStore fleet.KeyValueStore
scepConfigService fleet.SCEPConfigService
digiCertService fleet.DigiCertService
hydrantService fleet.HydrantService
estService fleet.ESTService
}
func NewService(
@ -55,7 +55,7 @@ func NewService(
keyValueStore fleet.KeyValueStore,
scepConfigService fleet.SCEPConfigService,
digiCertService fleet.DigiCertService,
hydrantService fleet.HydrantService,
estService fleet.ESTService,
) (*Service, error) {
authorizer, err := authz.NewAuthorizer()
if err != nil {
@ -81,7 +81,7 @@ func NewService(
keyValueStore: keyValueStore,
scepConfigService: scepConfigService,
digiCertService: digiCertService,
hydrantService: hydrantService,
estService: estService,
}
// Override methods that can't be easily overriden via

View file

@ -510,6 +510,28 @@ func (ds *Datastore) generateUpdateQueryWithArgs(ctx context.Context, ca *fleet.
*args = append(*args, encryptedClientSecret)
}
case string(fleet.CATypeCustomESTProxy):
if ca.URL != nil {
updates = append(updates, "url = ?")
*args = append(*args, *ca.URL)
}
if ca.Name != nil {
updates = append(updates, "name = ?")
*args = append(*args, *ca.Name)
}
if ca.Username != nil {
updates = append(updates, "username = ?")
*args = append(*args, *ca.Username)
}
if ca.Password != nil {
updates = append(updates, "password_encrypted = ?")
encryptedPassword, err := encrypt([]byte(*ca.Password), ds.serverPrivateKey)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "encrypting password for new certificate authority")
}
*args = append(*args, encryptedPassword)
}
case string(fleet.CATypeNDESSCEPProxy):
if ca.URL != nil {
updates = append(updates, "url = ?")

View file

@ -184,6 +184,21 @@ func testCreateCertificateAuthority(t *testing.T, ds *Datastore) {
ClientSecret: ptr.String("hydrant-client-secret2"),
}
estCA1 := &fleet.CertificateAuthority{
Name: ptr.String("Custom EST CA"),
URL: ptr.String("http://customcerts.example.com"),
Type: string(fleet.CATypeCustomESTProxy),
Username: ptr.String("custom-est-username"),
Password: ptr.String("custom-est-password"),
}
estCA2 := &fleet.CertificateAuthority{
Name: ptr.String("Custom EST CA 2"),
URL: ptr.String("http://customcerts2.example.com"),
Type: string(fleet.CATypeCustomESTProxy),
Username: ptr.String("custom-est-username2"),
Password: ptr.String("custom-est-password2"),
}
// Custom SCEP CAs
customSCEPCA1 := &fleet.CertificateAuthority{
Name: ptr.String("Custom SCEP CA"),
@ -231,6 +246,8 @@ func testCreateCertificateAuthority(t *testing.T, ds *Datastore) {
digicertCA2,
hydrantCA1,
hydrantCA2,
estCA1,
estCA2,
customSCEPCA1,
customSCEPCA2,
ndesCA,
@ -385,6 +402,14 @@ func testUpdateCertificateAuthorityByID(t *testing.T, ds *Datastore) {
ClientSecret: ptr.String("hydrant-client-secret"),
}
customESTCA := &fleet.CertificateAuthority{
Name: ptr.String("Custom EST CA"),
URL: ptr.String("https://estca.example.com"),
Type: string(fleet.CATypeCustomESTProxy),
Username: ptr.String("custom-est-username"),
Password: ptr.String("custom-est-password"),
}
// Custom SCEP CAs
customSCEPCA1 := &fleet.CertificateAuthority{
Name: ptr.String("Custom SCEP CA"),
@ -416,6 +441,7 @@ func testUpdateCertificateAuthorityByID(t *testing.T, ds *Datastore) {
casToCreate := []*fleet.CertificateAuthority{
digicertCA1,
hydrantCA1,
customESTCA,
customSCEPCA1,
ndesCA1,
smallstepCA1,
@ -483,6 +509,26 @@ func testUpdateCertificateAuthorityByID(t *testing.T, ds *Datastore) {
require.Equal(t, "updated-client-secret", *updatedCA.ClientSecret)
})
t.Run("successfully updates custom est proxy CA", func(t *testing.T) {
customESTCA := caMap[fleet.CATypeCustomESTProxy]
customESTCA.Name = ptr.String("updated EST")
customESTCA.URL = ptr.String("https://coolguy.localhost")
customESTCA.Username = ptr.String("updated-username")
customESTCA.Password = ptr.String("updated-password")
err := ds.UpdateCertificateAuthorityByID(ctx, customESTCA.ID, customESTCA)
require.NoError(t, err)
updatedCA, err := ds.GetCertificateAuthorityByID(ctx, customESTCA.ID, true)
require.NoError(t, err)
require.Equal(t, "updated EST", *updatedCA.Name)
require.Equal(t, "https://coolguy.localhost", *updatedCA.URL)
require.Equal(t, "updated-username", *updatedCA.Username)
require.Equal(t, "updated-password", *updatedCA.Password)
})
t.Run("successfully updates ndes scep proxy CA", func(t *testing.T) {
ndesCA := caMap[fleet.CATypeNDESSCEPProxy]

View file

@ -0,0 +1,25 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20251104112849, Down_20251104112849)
}
func Up_20251104112849(tx *sql.Tx) error {
if _, err := tx.Exec(`
ALTER TABLE certificate_authorities
MODIFY COLUMN type
ENUM('digicert','ndes_scep_proxy','custom_scep_proxy','hydrant','smallstep','custom_est_proxy') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
`); err != nil {
return fmt.Errorf("adding custom_est_proxy to certificate_authorities.types enum: %w", err)
}
return nil
}
func Down_20251104112849(tx *sql.Tx) error {
return nil
}

File diff suppressed because one or more lines are too long

View file

@ -206,6 +206,9 @@ var ActivityDetailsList = []ActivityDetails{
ActivityAddedHydrant{},
ActivityDeletedHydrant{},
ActivityEditedHydrant{},
ActivityAddedCustomESTProxy{},
ActivityDeletedCustomESTProxy{},
ActivityEditedCustomESTProxy{},
ActivityAddedSmallstep{},
ActivityDeletedSmallstep{},
ActivityEditedSmallstep{},
@ -2598,6 +2601,51 @@ func (a ActivityEditedHydrant) Documentation() (activity string, details string,
}`
}
type ActivityAddedCustomESTProxy struct {
Name string `json:"name"`
}
func (a ActivityAddedCustomESTProxy) ActivityName() string {
return "added_custom_est_proxy"
}
func (a ActivityAddedCustomESTProxy) Documentation() (activity string, details string, detailsExample string) {
return "Generated when a custom EST certificate authority configuration is added in Fleet.", `This activity contains the following fields:
- "name": Name of the certificate authority.`, `{
"name": "EST_WIFI"
}`
}
type ActivityDeletedCustomESTProxy struct {
Name string `json:"name"`
}
func (a ActivityDeletedCustomESTProxy) ActivityName() string {
return "deleted_custom_est_proxy"
}
func (a ActivityDeletedCustomESTProxy) Documentation() (activity string, details string, detailsExample string) {
return "Generated when a custom EST certificate authority configuration is deleted in Fleet.", `This activity contains the following fields:
- "name": Name of the certificate authority.`, `{
"name": "EST_WIFI"
}`
}
type ActivityEditedCustomESTProxy struct {
Name string `json:"name"`
}
func (a ActivityEditedCustomESTProxy) ActivityName() string {
return "edited_custom_est_proxy"
}
func (a ActivityEditedCustomESTProxy) Documentation() (activity string, details string, detailsExample string) {
return "Generated when a custom EST certificate authority configuration is edited in Fleet.", `This activity contains the following fields:
- "name": Name of the certificate authority.`, `{
"name": "EST_WIFI"
}`
}
type ActivityAddedSmallstep struct {
Name string `json:"name"`
}

View file

@ -48,6 +48,7 @@ const (
CATypeDigiCert CAType = "digicert"
CATypeCustomSCEPProxy CAType = "custom_scep_proxy"
CATypeHydrant CAType = "hydrant"
CATypeCustomESTProxy CAType = "custom_est_proxy" // Enrollment over Secure Transport
CATypeSmallstep CAType = "smallstep"
)
@ -78,9 +79,9 @@ type CertificateAuthority struct {
// Smallstep
ChallengeURL *string `json:"challenge_url,omitempty" db:"challenge_url"`
// Username is stored by both Smallstep and NDES CA types
// Username is stored by Smallstep, NDES, and EST CA types
Username *string `json:"username,omitempty" db:"username"`
// Password is stored by both Smallstep and NDES CA types
// Password is stored by Smallstep, NDES, and EST CA types
Password *string `json:"password,omitempty" db:"-"`
// Custom SCEP Proxy
@ -103,6 +104,7 @@ type CertificateAuthorityPayload struct {
NDESSCEPProxy *NDESSCEPProxyCA `json:"ndes_scep_proxy,omitempty"`
CustomSCEPProxy *CustomSCEPProxyCA `json:"custom_scep_proxy,omitempty"`
Hydrant *HydrantCA `json:"hydrant,omitempty"`
CustomESTProxy *ESTProxyCA `json:"custom_est_proxy,omitempty"`
Smallstep *SmallstepSCEPProxyCA `json:"smallstep,omitempty"`
}
@ -135,6 +137,34 @@ func (d *DigiCertCA) Preprocess() {
d.ProfileID = Preprocess(d.ProfileID)
}
// Enrollment over Secure Transport Certificate Authority
type ESTProxyCA struct {
ID uint `json:"-"`
Name string `json:"name"`
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
}
func (h *ESTProxyCA) Equals(other *ESTProxyCA) bool {
return h.Name == other.Name &&
h.URL == other.URL &&
h.Username == other.Username &&
(h.Password == "" || h.Password == MaskedPassword || h.Password == other.Password)
}
func (h *ESTProxyCA) NeedToVerify(other *ESTProxyCA) bool {
return h.Name != other.Name ||
h.URL != other.URL ||
h.Username != other.Username ||
!(h.Password == "" || h.Password == MaskedPassword || h.Password == other.Password)
}
func (h *ESTProxyCA) Preprocess() {
h.Name = Preprocess(h.Name)
h.URL = Preprocess(h.URL)
}
type HydrantCA struct {
ID uint `json:"-"`
Name string `json:"name"`
@ -208,6 +238,7 @@ type CertificateAuthorityUpdatePayload struct {
*NDESSCEPProxyCAUpdatePayload `json:"ndes_scep_proxy,omitempty"`
*CustomSCEPProxyCAUpdatePayload `json:"custom_scep_proxy,omitempty"`
*HydrantCAUpdatePayload `json:"hydrant,omitempty"`
*CustomESTCAUpdatePayload `json:"custom_est_proxy,omitempty"`
*SmallstepSCEPProxyCAUpdatePayload `json:"smallstep,omitempty"`
}
@ -220,6 +251,9 @@ func (p *CertificateAuthorityUpdatePayload) ValidatePayload(privateKey string, e
if p.HydrantCAUpdatePayload != nil {
caInPayload++
}
if p.CustomESTCAUpdatePayload != nil {
caInPayload++
}
if p.NDESSCEPProxyCAUpdatePayload != nil {
caInPayload++
}
@ -375,6 +409,36 @@ func (hp *HydrantCAUpdatePayload) Preprocess() {
}
}
type CustomESTCAUpdatePayload struct {
Name *string `json:"name"`
URL *string `json:"url"`
Username *string `json:"username"`
Password *string `json:"password"`
}
// IsEmpty checks if the struct only has all empty values
func (ep CustomESTCAUpdatePayload) IsEmpty() bool {
return ep.Name == nil && ep.URL == nil && ep.Username == nil && ep.Password == nil
}
// ValidateRelatedFields verifies that fields that are related to each other are set correctly.
// For example if the Name is provided then the Username and Password must also be provided
func (ep *CustomESTCAUpdatePayload) ValidateRelatedFields(errPrefix string, certName string) error {
if ep.URL != nil && (ep.Username == nil || ep.Password == nil) {
return &BadRequestError{Message: fmt.Sprintf(`%s"password" must be set when modifying "url" of an existing certificate authority: %s.`, errPrefix, certName)}
}
return nil
}
func (ep *CustomESTCAUpdatePayload) Preprocess() {
if ep.Name != nil {
*ep.Name = Preprocess(*ep.Name)
}
if ep.URL != nil {
*ep.URL = Preprocess(*ep.URL)
}
}
type SmallstepSCEPProxyCAUpdatePayload struct {
Name *string `json:"name"`
URL *string `json:"url"`
@ -426,6 +490,7 @@ func (c *RequestCertificatePayload) AuthzType() string {
}
type GroupedCertificateAuthorities struct {
EST []ESTProxyCA `json:"custom_est_proxy"` // Enrollment over Secure Transport
Hydrant []HydrantCA `json:"hydrant"`
DigiCert []DigiCertCA `json:"digicert"`
NDESSCEP *NDESSCEPProxyCA `json:"ndes_scep_proxy"`
@ -437,6 +502,7 @@ func GroupCertificateAuthoritiesByType(cas []*CertificateAuthority) (*GroupedCer
grouped := &GroupedCertificateAuthorities{
DigiCert: []DigiCertCA{},
Hydrant: []HydrantCA{},
EST: []ESTProxyCA{},
CustomScepProxy: []CustomSCEPProxyCA{},
NDESSCEP: nil,
Smallstep: []SmallstepSCEPProxyCA{},
@ -476,6 +542,14 @@ func GroupCertificateAuthoritiesByType(cas []*CertificateAuthority) (*GroupedCer
ClientID: *ca.ClientID,
ClientSecret: *ca.ClientSecret,
})
case string(CATypeCustomESTProxy):
grouped.EST = append(grouped.EST, ESTProxyCA{
ID: ca.ID,
Name: *ca.Name,
URL: *ca.URL,
Username: *ca.Username,
Password: *ca.Password,
})
case string(CATypeCustomSCEPProxy):
grouped.CustomScepProxy = append(grouped.CustomScepProxy, CustomSCEPProxyCA{
ID: ca.ID,
@ -582,6 +656,22 @@ func ValidateCertificateAuthoritiesSpec(incoming interface{}) (*GroupedCertifica
groupedCAs.Hydrant = hydrantData
}
if ESTCA, ok := spec.(map[string]any)["custom_est_proxy"]; !ok || ESTCA == nil {
groupedCAs.EST = []ESTProxyCA{}
} else {
// We unmarshal EST CA integration into its dedicated type for additional validation
estJSON, err := json.Marshal(spec.(map[string]any)["custom_est_proxy"])
if err != nil {
return nil, fmt.Errorf("org_settings.certificate_authorities.custom_est_proxy cannot be marshalled into JSON: %w", err)
}
var estData []ESTProxyCA
err = json.Unmarshal(estJSON, &estData)
if err != nil {
return nil, fmt.Errorf("org_settings.certificate_authorities.custom_est_proxy cannot be parsed: %w", err)
}
groupedCAs.EST = estData
}
// TODO(sca): confirm this
if smallstepSCEPCA, ok := spec.(map[string]interface{})["smallstep"]; !ok || smallstepSCEPCA == nil {
groupedCAs.Smallstep = []SmallstepSCEPProxyCA{}

20
server/fleet/est_ca.go Normal file
View file

@ -0,0 +1,20 @@
package fleet
import (
"context"
)
type ESTCertificate struct {
Certificate []byte
}
type ESTService interface {
// ValidateESTURL validates that the provided URL in the ESTProxyCA is reachable via the
// /cacerts endpoint. It is not responsible for checking the credentials.
ValidateESTURL(ctx context.Context, estProxyCA ESTProxyCA) error
// GetCertificate retrieves a certificate from the EST CA using the provided CSR which must
// be in base64 encoded PKCS#10 format(i.e. PEM format without the header, footer or newlines).
// The CSR format must match the template configured on the EST server. The certificate is
// returned in a similar format as the CSR(i.e. base64 encoded PKCS#7)
GetCertificate(ctx context.Context, estProxyCA ESTProxyCA, csr string) (*ESTCertificate, error)
}

View file

@ -1,20 +0,0 @@
package fleet
import (
"context"
)
type HydrantCertificate struct {
Certificate []byte
}
type HydrantService interface {
// ValidateHydrantURL validates that the provided URL in the HydrantCA is reachable via the
// /cacerts endpoint. It is not responsible for checking the credentials.
ValidateHydrantURL(ctx context.Context, hydrantCA HydrantCA) error
// GetCertificate retrieves a certificate from the Hydrant CA using the provided CSR which must
// be in base64 encoded PKCS#10 format(i.e. PEM format without the header, footer or newlines).
// The CSR format must match the template configured on the Hydrant server. The certificate is
// returned in a similar format as the CSR(i.e. base64 encoded PKCS#7)
GetCertificate(ctx context.Context, hydrantCA HydrantCA, csr string) (*HydrantCertificate, error)
}

View file

@ -109,6 +109,13 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
ClientSecret: "client-secret",
}
goodESTCA := fleet.ESTProxyCA{
Name: "VALID_EST",
URL: mockHydrantServer.URL,
Username: "username",
Password: "password",
}
// goodSmallstepCA is a base object for testing with a valid Smallstep SCEP CA. Copy it to override specific fields in tests.
goodSmallstepCA := fleet.SmallstepSCEPProxyCA{
Name: "VALID_SMALLSTEP_SCEP",
@ -136,6 +143,13 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
},
DryRun: dryRun,
}, nil
case fleet.ESTProxyCA:
return batchApplyCertificateAuthoritiesRequest{
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
EST: []fleet.ESTProxyCA{v},
},
DryRun: dryRun,
}, nil
case fleet.NDESSCEPProxyCA:
return batchApplyCertificateAuthoritiesRequest{
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
@ -423,6 +437,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -445,6 +460,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA, testCopy},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
},
DryRun: false,
}
@ -461,6 +477,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA, testCopy},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
},
DryRun: false,
}
@ -474,6 +491,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA, testCopy},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
},
DryRun: false,
}
@ -487,6 +505,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA, testCopy},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -764,6 +783,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -786,6 +806,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA, testCopy},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -803,6 +824,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA, testCopy},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -817,6 +839,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA, testCopy},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -831,6 +854,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA, testCopy},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -984,6 +1008,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -1006,6 +1031,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA, testCopy},
},
DryRun: false,
@ -1023,6 +1049,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA, testCopy},
},
DryRun: false,
@ -1037,6 +1064,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA, testCopy},
},
DryRun: false,
@ -1051,6 +1079,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA, testCopy},
},
DryRun: false,
@ -1240,6 +1269,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -1262,6 +1292,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA, testCopy},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -1279,6 +1310,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA, testCopy},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -1293,6 +1325,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA, testCopy},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -1307,6 +1340,7 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA, testCopy},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
@ -1316,6 +1350,130 @@ func (s *integrationMDMTestSuite) TestBatchApplyCertificateAuthorities() {
// TODO(hca): hydrant happy path and other specific tests
})
t.Run("custom est", func(t *testing.T) {
// run common invalid name test cases
t.Run("invalid name", func(t *testing.T) {
for _, tc := range invalidNameTestCases {
t.Run(tc.testName, func(t *testing.T) {
testCopy := goodESTCA
testCopy.Name = tc.name
req, err := newApplyRequest(testCopy, false)
require.NoError(t, err)
res := s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", req, http.StatusUnprocessableEntity)
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, "certificate_authorities.custom_est_proxy")
require.Contains(t, errMsg, tc.errMessage)
})
}
})
// run common invalid url test cases
t.Run("invalid url", func(t *testing.T) {
for _, tc := range invalidURLTestCases {
t.Run(tc.testName, func(t *testing.T) {
testCopy := goodESTCA
testCopy.URL = tc.url
req, err := newApplyRequest(testCopy, false)
require.NoError(t, err)
res := s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", req, http.StatusUnprocessableEntity)
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, "certificate_authorities.custom_est_proxy")
if tc.errMessage == "Invalid URL" {
require.Contains(t, errMsg, "Invalid EST URL")
} else {
require.Contains(t, errMsg, tc.errMessage)
}
})
}
})
// run additional duplicate name scenarios
t.Run("duplicate names", func(t *testing.T) {
// create one of each CA
req := batchApplyCertificateAuthoritiesRequest{
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
}
_ = s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", req, http.StatusOK)
s.checkAppliedCAs(t, s.ds, req.CertificateAuthorities)
t.Cleanup(func() {
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, _ = q.ExecContext(context.Background(), "DELETE FROM certificate_authorities")
return nil
})
})
// try to create est ca proxy with same name as another est ca
testCopy := goodESTCA
testCopy.Username = "some-other-client-id"
duplicateReq := batchApplyCertificateAuthoritiesRequest{
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA, testCopy},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
}
res := s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", duplicateReq, http.StatusUnprocessableEntity)
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, "certificate_authorities.custom_est_proxy")
require.Contains(t, errMsg, "name is already used by another Custom EST Proxy certificate authority")
// try to create a custom est ca with same name as another digicert
testCopy = goodESTCA
testCopy.Name = goodDigiCertCA.Name
duplicateReq = batchApplyCertificateAuthoritiesRequest{
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA, testCopy},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
}
s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", duplicateReq, http.StatusOK)
// try to create est with same name as another custom scep
testCopy = goodESTCA
testCopy.Name = goodCustomSCEPCA.Name
duplicateReq = batchApplyCertificateAuthoritiesRequest{
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA, testCopy},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
}
s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", duplicateReq, http.StatusOK)
// try to create eset ca with same name as another smallstep
testCopy = goodESTCA
testCopy.Name = goodSmallstepCA.Name
duplicateReq = batchApplyCertificateAuthoritiesRequest{
CertificateAuthorities: fleet.GroupedCertificateAuthorities{
DigiCert: []fleet.DigiCertCA{goodDigiCertCA},
CustomScepProxy: []fleet.CustomSCEPProxyCA{goodCustomSCEPCA},
Hydrant: []fleet.HydrantCA{goodHydrantCA},
EST: []fleet.ESTProxyCA{goodESTCA, testCopy},
Smallstep: []fleet.SmallstepSCEPProxyCA{goodSmallstepCA},
},
DryRun: false,
}
s.Do("POST", "/api/v1/fleet/spec/certificate_authorities", duplicateReq, http.StatusOK)
})
})
}
func (s *integrationMDMTestSuite) checkAppliedCAs(t *testing.T, ds fleet.Datastore, expectedCAs fleet.GroupedCertificateAuthorities) {
@ -1371,6 +1529,22 @@ func (s *integrationMDMTestSuite) checkAppliedCAs(t *testing.T, ds fleet.Datasto
assert.Empty(t, gotCAs.Hydrant)
}
if len(expectedCAs.EST) != 0 {
assert.Len(t, gotCAs.EST, len(expectedCAs.EST))
wantByName := make(map[string]fleet.ESTProxyCA)
gotByName := make(map[string]fleet.ESTProxyCA)
for _, ca := range expectedCAs.EST {
wantByName[ca.Name] = ca
}
for _, ca := range gotCAs.EST {
ca.ID = 0 // ignore IDs when comparing
gotByName[ca.Name] = ca
}
assert.Equal(t, wantByName, gotByName)
} else {
assert.Empty(t, gotCAs.EST)
}
if expectedCAs.NDESSCEP != nil {
assert.NotNil(t, gotCAs.NDESSCEP)
gotCAs.NDESSCEP.ID = 0 // ignore ID when comparing

View file

@ -18,9 +18,9 @@ import (
"github.com/fleetdm/fleet/v4/ee/server/scim"
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
"github.com/fleetdm/fleet/v4/ee/server/service/est"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
"github.com/fleetdm/fleet/v4/ee/server/service/hydrant"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
@ -76,7 +76,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
c clock.Clock = clock.C
scepConfigService = eeservice.NewSCEPConfigService(logger, nil)
digiCertService = digicert.NewService(digicert.WithLogger(logger))
hydrantService = hydrant.NewService(hydrant.WithLogger(logger))
estCAService = est.NewService(est.WithLogger(logger))
conditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
mdmStorage fleet.MDMAppleStore
@ -249,7 +249,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
keyValueStore,
scepConfigService,
digiCertService,
hydrantService,
estCAService,
)
if err != nil {
panic(err)

View file

@ -12,8 +12,7 @@ import (
func main() {
// Ensure there's a make target specified.
if len(os.Args) < 2 {
fmt.Println("Usage: fdm <command> [--option=value ...] -- [make-options]")
os.Exit(1)
os.Args = append(os.Args, "help")
}
// Determine the path to the top-level directory (where the Makefile resides).