diff --git a/internal/pkg/api/error.go b/internal/pkg/api/error.go index e8e530f57..a4661e862 100644 --- a/internal/pkg/api/error.go +++ b/internal/pkg/api/error.go @@ -101,14 +101,32 @@ func NewHTTPErrResp(err error) HTTPErrResp { }, }, { - ErrAgentIDInAnotherPolicy, + ErrAgentReplaceTokenInvalid, HTTPErrResp{ http.StatusBadRequest, - "AgentIDInAnotherPolicy", - "agent with ID is already enrolled into another policy", + "AgentReplaceTokenInvalid", + "replace token is invalid", + zerolog.InfoLevel, + }, + }, + { + ErrAgentNotReplaceable, + HTTPErrResp{ + http.StatusForbidden, + "AgentNotReplaceable", + "existing agent cannot be replaced", zerolog.WarnLevel, }, }, + { + ErrAgentInAnotherPolicy, + HTTPErrResp{ + http.StatusBadRequest, + "AgentInAnotherPolicy", + "agent with ID is already enrolled into another policy", + zerolog.InfoLevel, + }, + }, { ErrInvalidUserAgent, HTTPErrResp{ diff --git a/internal/pkg/api/handleEnroll.go b/internal/pkg/api/handleEnroll.go index 679bfe5e5..dc1fc7d1c 100644 --- a/internal/pkg/api/handleEnroll.go +++ b/internal/pkg/api/handleEnroll.go @@ -31,6 +31,7 @@ import ( "github.com/hashicorp/go-version" "github.com/miolini/datacounter" "github.com/rs/zerolog" + "golang.org/x/crypto/bcrypt" ) const ( @@ -55,10 +56,12 @@ const kFleetAccessRolesJSON = ` ` var ( - ErrUnknownEnrollType = errors.New("unknown enroll request type") - ErrInactiveEnrollmentKey = errors.New("inactive enrollment key") - ErrPolicyNotFound = errors.New("policy not found") - ErrAgentIDInAnotherPolicy = errors.New("existing agent with ID in another policy") + ErrUnknownEnrollType = errors.New("unknown enroll request type") + ErrInactiveEnrollmentKey = errors.New("inactive enrollment key") + ErrPolicyNotFound = errors.New("policy not found") + ErrAgentReplaceTokenInvalid = errors.New("replace token is invalid") + ErrAgentNotReplaceable = errors.New("existing agent cannot be replaced") + ErrAgentInAnotherPolicy = errors.New("existing agent with ID in another policy") ) type EnrollerT struct { @@ -198,10 +201,22 @@ func (et *EnrollerT) _enroll( ) (*EnrollResponse, error) { var agent model.Agent var enrollmentID string + var replaceToken string span, ctx := apm.StartSpan(ctx, "enroll", "process") defer span.End() + now := time.Now() + + if req.ReplaceToken != nil && *req.ReplaceToken != "" { + replaceTokenBytes, err := bcrypt.GenerateFromPassword([]byte(*req.ReplaceToken), bcrypt.DefaultCost) + if err != nil { + // not a valid replace token + return nil, ErrAgentReplaceTokenInvalid + } + replaceToken = string(replaceTokenBytes) + } + if req.EnrollmentId != nil { vSpan, vCtx := apm.StartSpan(ctx, "checkEnrollmentID", "validate") enrollmentID = *req.EnrollmentId @@ -218,7 +233,6 @@ func (et *EnrollerT) _enroll( } vSpan.End() } - now := time.Now() // only delete existing agent if it never checked in if agent.Id != "" && agent.LastCheckin == "" { @@ -266,14 +280,51 @@ func (et *EnrollerT) _enroll( vSpan.End() return nil, err } + } else if !agent.Active { + // inactive agent has been unenrolled and the API key has already been invalidated + // delete the current record as the new enrollment will overwrite this one + zlog.Debug(). + Str("ID", agentID). + Msg("Inactive agent with ID found") + err = deleteAgent(ctx, zlog, et.bulker, agent.Id) + if err != nil { + zlog.Error().Err(err). + Str("AgentId", agent.Id). + Msg("Error when trying to delete old agent with same id") + return nil, err + } + // deleted, so clear the ID so code below knows it needs to be created + agent.Id = "" } else { zlog.Debug(). Str("ID", agentID). - Msg("Agent with ID found") + Msg("Active agent with ID found") } vSpan.End() if agent.Id != "" { + // confirm that this agent has a set replace token + // one is required or replacement of this already enrolled and active + // agent is not allowed + if agent.ReplaceToken == "" { + zlog.Warn(). + Str("AgentId", agent.Id). + Msg("Existing agent with same ID already enrolled without a replace token set") + return nil, ErrAgentNotReplaceable + } + if req.ReplaceToken == nil || *req.ReplaceToken == "" { + zlog.Warn(). + Str("AgentId", agent.Id). + Msg("Existing agent with same ID already enrolled; no replace token given during enrollment") + return nil, ErrAgentNotReplaceable + } + err = bcrypt.CompareHashAndPassword([]byte(agent.ReplaceToken), []byte(*req.ReplaceToken)) + if err != nil { + // not the same, cannot replace + // provides no real reason as that would expose to much information + return nil, ErrAgentNotReplaceable + } + // confirm that its on the same policy // it is not supported to have it the same ID enroll into different policies if agent.PolicyID != policyID { @@ -282,11 +333,11 @@ func (et *EnrollerT) _enroll( Str("PolicyId", policyID). Str("CurrentPolicyId", agent.PolicyID). Msg("Existing agent with same ID already enrolled into another policy") - return nil, ErrAgentIDInAnotherPolicy + return nil, ErrAgentInAnotherPolicy } // invalidate the previous api key - // this has to be done because its not possible to get the previous token + // this has to be done because it's not possible to get the previous token // so the other is invalidated and a new one is generated zlog.Debug(). Str("AgentId", agent.Id). @@ -346,7 +397,6 @@ func (et *EnrollerT) _enroll( // clears state of policy revision, as this agent needs to get the latest policy // clears state of unenrollment, as this is a new enrollment doc := bulk.UpdateFields{ - dl.FieldActive: true, dl.FieldNamespaces: namespaces, dl.FieldLocalMetadata: json.RawMessage(localMeta), dl.FieldAccessAPIKeyID: accessAPIKey.ID, @@ -379,6 +429,7 @@ func (et *EnrollerT) _enroll( }, Tags: removeDuplicateStr(req.Metadata.Tags), EnrollmentID: enrollmentID, + ReplaceToken: replaceToken, } err = createFleetAgent(ctx, et.bulker, agentID, agent) diff --git a/internal/pkg/api/handleEnroll_test.go b/internal/pkg/api/handleEnroll_test.go index eaa32e40e..99fbef98c 100644 --- a/internal/pkg/api/handleEnroll_test.go +++ b/internal/pkg/api/handleEnroll_test.go @@ -9,6 +9,8 @@ package api import ( "context" "encoding/json" + "errors" + "fmt" "reflect" "strings" "testing" @@ -17,6 +19,7 @@ import ( "github.com/elastic/fleet-server/v7/internal/pkg/bulk" "github.com/elastic/fleet-server/v7/internal/pkg/cache" "github.com/elastic/fleet-server/v7/internal/pkg/config" + "github.com/elastic/fleet-server/v7/internal/pkg/dl" "github.com/elastic/fleet-server/v7/internal/pkg/es" "github.com/elastic/fleet-server/v7/internal/pkg/model" "github.com/elastic/fleet-server/v7/internal/pkg/rollback" @@ -24,6 +27,7 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "golang.org/x/crypto/bcrypt" ) func TestRemoveDuplicateStr(t *testing.T) { @@ -139,6 +143,268 @@ func TestEnrollWithAgentID(t *testing.T) { } } +func TestEnrollWithAgentIDExistingNonActive(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + rb := &rollback.Rollback{} + zlog := zerolog.Logger{} + agentID := "1234" + req := &EnrollRequest{ + Type: "PERMANENT", + Id: &agentID, + Metadata: EnrollMetadata{ + UserProvided: []byte("{}"), + Local: []byte("{}"), + }, + } + verCon := mustBuildConstraints("8.9.0") + cfg := &config.Server{} + c, _ := cache.New(config.Cache{NumCounters: 100, MaxCost: 100000}) + bulker := ftesting.NewMockBulk() + et, _ := NewEnrollerT(verCon, cfg, bulker, c) + + bulker.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&es.ResultT{ + HitsT: es.HitsT{ + Hits: []es.HitT{{ + ID: "1234", + Index: dl.FleetAgents, + Source: []byte(`{"active":false,"agent":{"id":"1234","version":"8.9.0"},"type":"PERMANENT","policy_id":"1234"}`), + }}, + }, + }, nil) + bulker.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + bulker.On("APIKeyCreate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + &apikey.APIKey{ + ID: "1234", + Key: "1234", + }, nil) + bulker.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + "", nil) + resp, _ := et._enroll(ctx, rb, zlog, req, "1234", []string{}, "8.9.0") + + if resp.Action != "created" { + t.Fatal("enroll failed") + } + if resp.Item.Id != agentID { + t.Fatalf("agent ID should have been %s (not %s)", agentID, resp.Item.Id) + } +} + +func TestEnrollWithAgentIDExistingActive_NotReplaceable(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + rb := &rollback.Rollback{} + zlog := zerolog.Logger{} + agentID := "1234" + req := &EnrollRequest{ + Type: "PERMANENT", + Id: &agentID, + Metadata: EnrollMetadata{ + UserProvided: []byte("{}"), + Local: []byte("{}"), + }, + } + verCon := mustBuildConstraints("8.9.0") + cfg := &config.Server{} + c, _ := cache.New(config.Cache{NumCounters: 100, MaxCost: 100000}) + bulker := ftesting.NewMockBulk() + et, _ := NewEnrollerT(verCon, cfg, bulker, c) + + bulker.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&es.ResultT{ + HitsT: es.HitsT{ + Hits: []es.HitT{{ + ID: "1234", + Index: dl.FleetAgents, + Source: []byte(`{"active":true,"agent":{"id":"1234","version":"8.9.0"},"type":"PERMANENT","policy_id":"1234"}`), + }}, + }, + }, nil) + _, err := et._enroll(ctx, rb, zlog, req, "1234", []string{}, "8.9.0") + if !errors.Is(err, ErrAgentNotReplaceable) { + t.Fatal("should have got error ErrAgentNotReplaceable") + } +} + +func TestEnrollWithAgentIDExistingActive_InvalidReplaceToken_Missing(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + rb := &rollback.Rollback{} + zlog := zerolog.Logger{} + agentID := "1234" + replaceToken, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost) + if err != nil { + t.Fatalf("error generating bcrypt hash: %v", err) + } + req := &EnrollRequest{ + Type: "PERMANENT", + Id: &agentID, + Metadata: EnrollMetadata{ + UserProvided: []byte("{}"), + Local: []byte("{}"), + }, + } + verCon := mustBuildConstraints("8.9.0") + cfg := &config.Server{} + c, _ := cache.New(config.Cache{NumCounters: 100, MaxCost: 100000}) + bulker := ftesting.NewMockBulk() + et, _ := NewEnrollerT(verCon, cfg, bulker, c) + + source := fmt.Sprintf(`{"active":true,"agent":{"id":"1234","version":"8.9.0"},"type":"PERMANENT","policy_id":"1234","replace_token":"%s"}`, string(replaceToken)) + bulker.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&es.ResultT{ + HitsT: es.HitsT{ + Hits: []es.HitT{{ + ID: "1234", + Index: dl.FleetAgents, + Source: []byte(source), + }}, + }, + }, nil) + _, err = et._enroll(ctx, rb, zlog, req, "1234", []string{}, "8.9.0") + if !errors.Is(err, ErrAgentNotReplaceable) { + t.Fatal("should have got error ErrAgentNotReplaceable") + } +} + +func TestEnrollWithAgentIDExistingActive_InvalidReplaceToken_Mismatch(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + rb := &rollback.Rollback{} + zlog := zerolog.Logger{} + agentID := "1234" + replaceToken, err := bcrypt.GenerateFromPassword([]byte("replace_token"), bcrypt.DefaultCost) + if err != nil { + t.Fatalf("error generating bcrypt hash: %v", err) + } + wrongToken := "wrong_token" + req := &EnrollRequest{ + Type: "PERMANENT", + Id: &agentID, + Metadata: EnrollMetadata{ + UserProvided: []byte("{}"), + Local: []byte("{}"), + }, + ReplaceToken: &wrongToken, + } + verCon := mustBuildConstraints("8.9.0") + cfg := &config.Server{} + c, _ := cache.New(config.Cache{NumCounters: 100, MaxCost: 100000}) + bulker := ftesting.NewMockBulk() + et, _ := NewEnrollerT(verCon, cfg, bulker, c) + + source := fmt.Sprintf(`{"active":true,"agent":{"id":"1234","version":"8.9.0"},"type":"PERMANENT","policy_id":"1234","replace_token":"%s"}`, string(replaceToken)) + bulker.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&es.ResultT{ + HitsT: es.HitsT{ + Hits: []es.HitT{{ + ID: "1234", + Index: dl.FleetAgents, + Source: []byte(source), + }}, + }, + }, nil) + _, err = et._enroll(ctx, rb, zlog, req, "1234", []string{}, "8.9.0") + if !errors.Is(err, ErrAgentNotReplaceable) { + t.Fatal("should have got error ErrAgentNotReplaceable") + } +} + +func TestEnrollWithAgentIDExistingActive_WrongPolicy(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + rb := &rollback.Rollback{} + zlog := zerolog.Logger{} + agentID := "1234" + replaceToken := "replace_token" + replaceTokenHash, err := bcrypt.GenerateFromPassword([]byte(replaceToken), bcrypt.DefaultCost) + if err != nil { + t.Fatalf("error generating bcrypt hash: %v", err) + } + req := &EnrollRequest{ + Type: "PERMANENT", + Id: &agentID, + Metadata: EnrollMetadata{ + UserProvided: []byte("{}"), + Local: []byte("{}"), + }, + ReplaceToken: &replaceToken, + } + verCon := mustBuildConstraints("8.9.0") + cfg := &config.Server{} + c, _ := cache.New(config.Cache{NumCounters: 100, MaxCost: 100000}) + bulker := ftesting.NewMockBulk() + et, _ := NewEnrollerT(verCon, cfg, bulker, c) + + source := fmt.Sprintf(`{"active":true,"agent":{"id":"1234","version":"8.9.0"},"type":"PERMANENT","policy_id":"1234","replace_token":"%s"}`, string(replaceTokenHash)) + bulker.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&es.ResultT{ + HitsT: es.HitsT{ + Hits: []es.HitT{{ + ID: "1234", + Index: dl.FleetAgents, + Source: []byte(source), + }}, + }, + }, nil) + _, err = et._enroll(ctx, rb, zlog, req, "5678", []string{}, "8.9.0") + if !errors.Is(err, ErrAgentInAnotherPolicy) { + t.Fatal("should have got error ErrAgentInAnotherPolicy") + } +} + +func TestEnrollWithAgentIDExistingActive(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + rb := &rollback.Rollback{} + zlog := zerolog.Logger{} + agentID := "1234" + replaceToken := "replace_token" + replaceTokenHash, err := bcrypt.GenerateFromPassword([]byte(replaceToken), bcrypt.DefaultCost) + if err != nil { + t.Fatalf("error generating bcrypt hash: %v", err) + } + req := &EnrollRequest{ + Type: "PERMANENT", + Id: &agentID, + Metadata: EnrollMetadata{ + UserProvided: []byte("{}"), + Local: []byte("{}"), + }, + ReplaceToken: &replaceToken, + } + verCon := mustBuildConstraints("8.9.0") + cfg := &config.Server{} + c, _ := cache.New(config.Cache{NumCounters: 100, MaxCost: 100000}) + bulker := ftesting.NewMockBulk() + et, _ := NewEnrollerT(verCon, cfg, bulker, c) + + source := fmt.Sprintf(`{"active":true,"agent":{"id":"1234","version":"8.9.0"},"type":"PERMANENT","policy_id":"1234","replace_token":"%s"}`, string(replaceTokenHash)) + bulker.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&es.ResultT{ + HitsT: es.HitsT{ + Hits: []es.HitT{{ + ID: "1234", + Index: dl.FleetAgents, + Source: []byte(source), + }}, + }, + }, nil) + bulker.On("APIKeyRead", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + &apikey.APIKeyMetadata{ID: "1234"}, nil) + bulker.On("APIKeyInvalidate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + bulker.On("APIKeyCreate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + &apikey.APIKey{ + ID: "1234", + Key: "1234", + }, nil) + bulker.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + nil) + resp, _ := et._enroll(ctx, rb, zlog, req, "1234", []string{}, "8.9.0") + + if resp.Action != "created" { + t.Fatal("enroll failed") + } + if resp.Item.Id != agentID { + t.Fatalf("agent ID should have been %s (not %s)", agentID, resp.Item.Id) + } +} + func TestEnrollerT_retrieveStaticTokenEnrollmentToken(t *testing.T) { bulkerBuilder := func(policies ...model.Policy) func() bulk.Bulk { return func() bulk.Bulk { diff --git a/internal/pkg/api/openapi.gen.go b/internal/pkg/api/openapi.gen.go index bac6a0fdd..825e1954c 100644 --- a/internal/pkg/api/openapi.gen.go +++ b/internal/pkg/api/openapi.gen.go @@ -408,6 +408,11 @@ type EnrollRequest struct { // Metadata Metadata associated with the agent that is enrolling to fleet. Metadata EnrollMetadata `json:"metadata"` + // ReplaceToken The replacement token of the agent. + // Provided when an agent could replace an existing agent. This token must match the original enrollment of + // that agent otherwise it will not be able to enroll. + ReplaceToken *string `json:"replace_token,omitempty"` + // SharedId The shared ID of the agent. // To support pre-existing installs. // diff --git a/internal/pkg/model/schema.go b/internal/pkg/model/schema.go index c5eaef2af..7991bd3aa 100644 --- a/internal/pkg/model/schema.go +++ b/internal/pkg/model/schema.go @@ -189,6 +189,9 @@ type Agent struct { // The current policy revision_idx for the Elastic Agent PolicyRevisionIdx int64 `json:"policy_revision_idx,omitempty"` + // hash of token provided during enrollment that allows replacement by another enrollment with same ID + ReplaceToken string `json:"replace_token,omitempty"` + // Shared ID SharedID string `json:"shared_id,omitempty"` diff --git a/internal/pkg/server/fleet_integration_test.go b/internal/pkg/server/fleet_integration_test.go index c8e186c56..f79b522dc 100644 --- a/internal/pkg/server/fleet_integration_test.go +++ b/internal/pkg/server/fleet_integration_test.go @@ -836,10 +836,106 @@ func Test_Agent_Enrollment_Id_Invalidated_API_key(t *testing.T) { } } +func Test_Agent_Id_No_ReplaceToken(t *testing.T) { + enrollBodyWID := `{ + "type": "PERMANENT", + "id": "123456", + "metadata": { + "user_provided": {}, + "local": {}, + "tags": [] + } + }` + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start test server + srv, err := startTestServer(t, ctx, policyData) + require.NoError(t, err) + ctx = testlog.SetLogger(t).WithContext(ctx) + + t.Log("Enroll the first agent with id") + firstEnroll := EnrollAgent(t, ctx, srv, enrollBodyWID) + + // cleanup + defer func() { + err := srv.bulker.Delete(ctx, dl.FleetAgents, firstEnroll.Item.Id) + if err != nil { + t.Log("could not clean up agent") + } + }() + + t.Log("Enroll the second agent with the same id") + req, err := http.NewRequestWithContext(ctx, "POST", srv.baseURL()+"/api/fleet/agents/enroll", strings.NewReader(enrollBodyWID)) + require.NoError(t, err) + req.Header.Set("Authorization", "ApiKey "+srv.enrollKey) + req.Header.Set("User-Agent", "elastic agent "+serverVersion) + req.Header.Set("Content-Type", "application/json") + + cli := cleanhttp.DefaultClient() + res, err := cli.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusForbidden, res.StatusCode) +} + +func Test_Agent_Id_ReplaceToken_Mismatch(t *testing.T) { + enrollBodyWID := `{ + "type": "PERMANENT", + "id": "123456", + "replace_token": "replaceable", + "metadata": { + "user_provided": {}, + "local": {}, + "tags": [] + } + }` + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start test server + srv, err := startTestServer(t, ctx, policyData) + require.NoError(t, err) + ctx = testlog.SetLogger(t).WithContext(ctx) + + t.Log("Enroll the first agent with id") + firstEnroll := EnrollAgent(t, ctx, srv, enrollBodyWID) + + // cleanup + defer func() { + err := srv.bulker.Delete(ctx, dl.FleetAgents, firstEnroll.Item.Id) + if err != nil { + t.Log("could not clean up agent") + } + }() + + t.Log("Enroll the second agent with the same id") + enrollBodyBadReplaceToken := `{ + "type": "PERMANENT", + "id": "123456", + "replace_token": "replaceable_wrong", + "metadata": { + "user_provided": {}, + "local": {}, + "tags": [] + } + }` + req, err := http.NewRequestWithContext(ctx, "POST", srv.baseURL()+"/api/fleet/agents/enroll", strings.NewReader(enrollBodyBadReplaceToken)) + require.NoError(t, err) + req.Header.Set("Authorization", "ApiKey "+srv.enrollKey) + req.Header.Set("User-Agent", "elastic agent "+serverVersion) + req.Header.Set("Content-Type", "application/json") + + cli := cleanhttp.DefaultClient() + res, err := cli.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusForbidden, res.StatusCode) +} + func Test_Agent_Id(t *testing.T) { enrollBodyWID := `{ "type": "PERMANENT", "id": "123456", + "replace_token": "replaceable", "metadata": { "user_provided": {}, "local": {}, diff --git a/model/openapi.yml b/model/openapi.yml index 2b0e3117c..9256959cc 100644 --- a/model/openapi.yml +++ b/model/openapi.yml @@ -154,6 +154,12 @@ components: type: string enum: - PERMANENT + replace_token: + type: string + description: | + The replacement token of the agent. + Provided when an agent could replace an existing agent. This token must match the original enrollment of + that agent otherwise it will not be able to enroll. enrollment_id: type: string description: | diff --git a/model/schema.json b/model/schema.json index e979b2e6a..6fb03c87c 100644 --- a/model/schema.json +++ b/model/schema.json @@ -685,6 +685,10 @@ "upgrade_details": { "description": "Additional upgrade status details.", "type": "object" + }, + "replace_token": { + "description": "hash of token provided during enrollment that allows replacement by another enrollment with same ID", + "type": "string" } }, "required": ["_id", "type", "active", "enrolled_at", "status"] diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 7edd450b7..8eb68cc16 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -405,6 +405,11 @@ type EnrollRequest struct { // Metadata Metadata associated with the agent that is enrolling to fleet. Metadata EnrollMetadata `json:"metadata"` + // ReplaceToken The replacement token of the agent. + // Provided when an agent could replace an existing agent. This token must match the original enrollment of + // that agent otherwise it will not be able to enroll. + ReplaceToken *string `json:"replace_token,omitempty"` + // SharedId The shared ID of the agent. // To support pre-existing installs. //