From 068ffeaf40e5cba91a69c2a5f057627fd3631e8d Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Wed, 3 Dec 2025 15:42:03 -0400 Subject: [PATCH] Use auth header for android end-points (#36594) **Related issue:** Resolves #36287 Updated 'fleetd/certificates/' and 'fleetd/certificates//status' to authenticate using the orbit_node_key provided in the 'Authentication' header. --- .../36287-android-cert-crud-use-auth-header | 1 + server/service/certificates.go | 12 +--- server/service/endpoint_middleware.go | 21 ++++++- server/service/endpoint_utils.go | 30 +++++++++- server/service/handler.go | 12 ++-- server/service/integration_core_test.go | 60 ++++++++++++++----- 6 files changed, 102 insertions(+), 34 deletions(-) create mode 100644 changes/36287-android-cert-crud-use-auth-header diff --git a/changes/36287-android-cert-crud-use-auth-header b/changes/36287-android-cert-crud-use-auth-header new file mode 100644 index 0000000000..5daa98f3ea --- /dev/null +++ b/changes/36287-android-cert-crud-use-auth-header @@ -0,0 +1 @@ +* Updated 'fleetd/certificates/' and 'fleetd/certificates//status' to authenticate using the orbit_node_key provided in the 'Authentication' header. \ No newline at end of file diff --git a/server/service/certificates.go b/server/service/certificates.go index 6ff3d12e41..0562273268 100644 --- a/server/service/certificates.go +++ b/server/service/certificates.go @@ -115,12 +115,7 @@ func (svc *Service) ListCertificateTemplates(ctx context.Context, teamID uint, o } type getDeviceCertificateTemplateRequest struct { - ID uint `url:"id"` - NodeKey string `query:"node_key"` -} - -func (r *getDeviceCertificateTemplateRequest) hostNodeKey() string { - return r.NodeKey + ID uint `url:"id"` } type getDeviceCertificateTemplateResponse struct { @@ -349,17 +344,12 @@ func (svc *Service) DeleteCertificateTemplateSpecs(ctx context.Context, certific type updateCertificateStatusRequest struct { CertificateTemplateID uint `url:"id"` - NodeKey string `json:"node_key"` Status string `json:"status"` // Detail provides additional information about the status change. // For example, it can be used to provide a reason for a failed status change. Detail *string `json:"detail,omitempty"` } -func (r *updateCertificateStatusRequest) hostNodeKey() string { - return r.NodeKey -} - type updateCertificateStatusResponse struct { Err error `json:"error,omitempty"` } diff --git a/server/service/endpoint_middleware.go b/server/service/endpoint_middleware.go index 6aadc05c00..c1edfd3bf8 100644 --- a/server/service/endpoint_middleware.go +++ b/server/service/endpoint_middleware.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/fleetdm/fleet/v4/server/contexts/certserial" "github.com/fleetdm/fleet/v4/server/contexts/logging" @@ -157,9 +158,14 @@ func authenticatedHost(svc fleet.Service, logger log.Logger, next endpoint.Endpo return middleware_log.Logged(authHostFunc) } -func authenticatedOrbitHost(svc fleet.Service, logger log.Logger, next endpoint.Endpoint) endpoint.Endpoint { +func authenticatedOrbitHost( + svc fleet.Service, + logger log.Logger, + next endpoint.Endpoint, + orbitNodeKeyGetter func(context.Context, interface{}) (string, error), +) endpoint.Endpoint { authHostFunc := func(ctx context.Context, request interface{}) (interface{}, error) { - nodeKey, err := getOrbitNodeKey(request) + nodeKey, err := orbitNodeKeyGetter(ctx, request) if err != nil { return nil, err } @@ -194,13 +200,22 @@ func authenticatedOrbitHost(svc fleet.Service, logger log.Logger, next endpoint. return middleware_log.Logged(authHostFunc) } -func getOrbitNodeKey(r interface{}) (string, error) { +func getOrbitNodeKey(ctx context.Context, r interface{}) (string, error) { if onk, err := r.(interface{ orbitHostNodeKey() string }); err { return onk.orbitHostNodeKey(), nil } return "", errors.New("error getting orbit node key") } +func authHeaderValue(prefix string) func(ctx context.Context, r interface{}) (string, error) { + return func(ctx context.Context, r interface{}) (string, error) { + if authHeader, ok := ctx.Value(kithttp.ContextKeyRequestAuthorization).(string); ok { + return strings.TrimPrefix(authHeader, prefix), nil + } + return "", nil + } +} + func getNodeKey(r interface{}) (string, error) { if hnk, ok := r.(interface{ hostNodeKey() string }); ok { return hnk.hostNodeKey(), nil diff --git a/server/service/endpoint_utils.go b/server/service/endpoint_utils.go index b2e74484e6..6719ee5a7b 100644 --- a/server/service/endpoint_utils.go +++ b/server/service/endpoint_utils.go @@ -217,11 +217,39 @@ func newHostAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts [ } } +func androidAuthenticatedEndpointer( + svc fleet.Service, + logger log.Logger, + opts []kithttp.ServerOption, + r *mux.Router, + versions ...string, +) *eu.CommonEndpointer[eu.HandlerFunc] { + // Inject the fleet.Capabilities header to the response for Orbit hosts + opts = append(opts, capabilitiesResponseFunc(fleet.GetServerOrbitCapabilities())) + // Add the capabilities reported by Orbit to the request context + opts = append(opts, capabilitiesContextFunc()) + + return &eu.CommonEndpointer[eu.HandlerFunc]{ + EP: &endpointer{ + svc: svc, + }, + MakeDecoderFn: makeDecoder, + EncodeFn: encodeResponse, + Opts: opts, + AuthFunc: func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint { + return authenticatedOrbitHost(svc, logger, next, authHeaderValue("Node key ")) + }, + FleetService: svc, + Router: r, + Versions: versions, + } +} + func newOrbitAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts []kithttp.ServerOption, r *mux.Router, versions ...string, ) *eu.CommonEndpointer[eu.HandlerFunc] { authFunc := func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint { - return authenticatedOrbitHost(svc, logger, next) + return authenticatedOrbitHost(svc, logger, next, getOrbitNodeKey) } // Inject the fleet.Capabilities header to the response for Orbit hosts diff --git a/server/service/handler.go b/server/service/handler.go index c7fc070b88..7870cb621e 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -906,10 +906,14 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC POST("/api/osquery/log", submitLogsEndpoint, submitLogsRequest{}) he.WithAltPaths("/api/v1/osquery/yara/{name}"). POST("/api/osquery/yara/{name}", getYaraEndpoint, getYaraRequest{}) - he.WithAltPaths("/api/v1/fleetd/certificates/{id:[0-9]+}"). - GET("/api/fleetd/certificates/{id:[0-9]+}", getDeviceCertificateTemplateEndpoint, getDeviceCertificateTemplateRequest{}) - he.WithAltPaths("/api/v1/fleetd/certificates/{id:[0-9]+}/status"). - PUT("/api/fleetd/certificates/{id:[0-9]+}/status", updateCertificateStatusEndpoint, updateCertificateStatusRequest{}) + + // android authenticated end-points + // Authentication is implemented using the orbit_node_key from the 'Authentication' header. + // The 'orbit_node_key' is used because it's the only thing we have available when the device gets enrolled + // after the MDM setup is complete. + androidEndpoints := androidAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...) + androidEndpoints.GET("/api/fleetd/certificates/{id:[0-9]+}", getDeviceCertificateTemplateEndpoint, getDeviceCertificateTemplateRequest{}) + androidEndpoints.PUT("/api/fleetd/certificates/{id:[0-9]+}/status", updateCertificateStatusEndpoint, updateCertificateStatusRequest{}) // orbit authenticated endpoints oe := newOrbitAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...) diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index f0513fc61e..ca32aeb215 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -8027,6 +8027,10 @@ func (s *integrationTestSuite) TestCertificatesSpecs() { }) require.NoError(t, err) + orbitNodeKey := uuid.New().String() + host.OrbitNodeKey = &orbitNodeKey + require.NoError(t, s.ds.UpdateHost(ctx, host)) + // Add an IDP user for the host err = s.ds.ReplaceHostDeviceMapping(ctx, host.ID, []*fleet.HostDeviceMapping{ { @@ -8043,10 +8047,16 @@ func (s *integrationTestSuite) TestCertificatesSpecs() { // Get certificate without node_key var getCertResp getDeviceCertificateTemplateResponse - s.DoJSON("GET", fmt.Sprintf("/api/v1/fleetd/certificates/%d", certID), nil, http.StatusBadRequest, &getCertResp) + + resp := s.DoRawWithHeaders("GET", fmt.Sprintf("/api/fleetd/certificates/%d", certID), nil, http.StatusUnauthorized, nil) + require.NoError(t, resp.Body.Close()) // Get certificate with node_key (should return replaced variables) - s.DoJSON("GET", fmt.Sprintf("/api/v1/fleetd/certificates/%d?node_key=%s", certID, *host.NodeKey), nil, http.StatusOK, &getCertResp) + resp = s.DoRawWithHeaders("GET", fmt.Sprintf("/api/fleetd/certificates/%d", certID), nil, http.StatusOK, map[string]string{ + "Authorization": fmt.Sprintf("Node key %s", orbitNodeKey), + }) + require.NoError(t, json.NewDecoder(resp.Body).Decode(&getCertResp)) + require.NoError(t, resp.Body.Close()) require.NotNil(t, getCertResp.Certificate) assert.Contains(t, getCertResp.Certificate.SubjectName, "test.user@example.com") @@ -14661,13 +14671,13 @@ func (s *integrationTestSuite) TestUpdateHostCertificateTemplate() { require.NoError(t, err) require.NotNil(t, savedTemplate) - nodeKey := uuid.New().String() + orbitNodeKey := uuid.New().String() uuid := uuid.New().String() hostName := "test-update-host-certificate-template" // Create a host host, err := s.ds.NewHost(context.Background(), &fleet.Host{ - NodeKey: &nodeKey, + NodeKey: &orbitNodeKey, UUID: uuid, Hostname: hostName, Platform: "android", @@ -14675,6 +14685,9 @@ func (s *integrationTestSuite) TestUpdateHostCertificateTemplate() { }) require.NoError(t, err) + host.OrbitNodeKey = &orbitNodeKey + require.NoError(t, s.ds.UpdateHost(ctx, host)) + certificateTemplateID := savedTemplate.ID // Delete the certificate after the test is done, so the team can be deleted. @@ -14703,32 +14716,45 @@ INSERT INTO host_certificate_templates ( cases := []struct { name string templateID uint - nodeKey string newStatus string detail *string expectedResponseStatus int expectedResponseMessage string + headers map[string]string }{ { name: "Valid Update", templateID: certificateTemplateID, - nodeKey: nodeKey, newStatus: "verified", detail: ptr.String("Certificate Verified"), expectedResponseStatus: http.StatusOK, + headers: map[string]string{ + "Authorization": fmt.Sprintf("Node key %s", orbitNodeKey), + }, }, { name: "Invalid Status", templateID: certificateTemplateID, - nodeKey: nodeKey, newStatus: "invalid_status", expectedResponseStatus: http.StatusUnprocessableEntity, expectedResponseMessage: "invalid status value", + headers: map[string]string{ + "Authorization": fmt.Sprintf("Node key %s", orbitNodeKey), + }, }, { name: "Wrong node key", templateID: certificateTemplateID, - nodeKey: "wrong-nodekey", + newStatus: "verified", + expectedResponseStatus: http.StatusUnauthorized, + expectedResponseMessage: "host certificate template not found", + headers: map[string]string{ + "Authorization": "Node key wrong-node-key", + }, + }, + { + name: "With no auth headers", + templateID: certificateTemplateID, newStatus: "verified", expectedResponseStatus: http.StatusUnauthorized, expectedResponseMessage: "host certificate template not found", @@ -14736,21 +14762,25 @@ INSERT INTO host_certificate_templates ( { name: "Wrong Template ID", templateID: 9999, - nodeKey: nodeKey, newStatus: "verified", expectedResponseStatus: http.StatusNotFound, expectedResponseMessage: "host certificate template not found", + headers: map[string]string{ + "Authorization": fmt.Sprintf("Node key %s", orbitNodeKey), + }, }, } for _, tc := range cases { t.Run(fmt.Sprintf("TestUpdateHostCertificateTemplate:%s", tc.name), func(t *testing.T) { - var resp map[string]interface{} - s.DoJSON("PUT", fmt.Sprintf("/api/fleetd/certificates/%d/status", tc.templateID), updateCertificateStatusRequest{ - NodeKey: tc.nodeKey, - Status: tc.newStatus, - Detail: tc.detail, - }, tc.expectedResponseStatus, &resp) + req, err := json.Marshal(updateCertificateStatusRequest{ + Status: tc.newStatus, + Detail: tc.detail, + }) + require.NoError(t, err) + + resp := s.DoRawWithHeaders("PUT", fmt.Sprintf("/api/fleetd/certificates/%d/status", tc.templateID), req, tc.expectedResponseStatus, tc.headers) + require.NoError(t, resp.Body.Close()) }) } }