Skip to content

Commit 91af65a

Browse files
authored
Ensure that the connecting Elastic Agent is a supported version. (#239)
* Ensure that the connecting Elastic Agent is a supported version. * Revert change to fleet-server.yml * Fix added space in fleet-server.yml. * Fixes from code review. * Update go.mod.
1 parent b171295 commit 91af65a

File tree

10 files changed

+2254
-2050
lines changed

10 files changed

+2254
-2050
lines changed

NOTICE.txt

Lines changed: 2038 additions & 2038 deletions
Large diffs are not rendered by default.

cmd/fleet/handleCheckin.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ import (
2626
"github.com/elastic/fleet-server/v7/internal/pkg/policy"
2727
"github.com/elastic/fleet-server/v7/internal/pkg/smap"
2828
"github.com/elastic/fleet-server/v7/internal/pkg/sqn"
29-
"github.com/miolini/datacounter"
3029

30+
"github.com/hashicorp/go-version"
3131
"github.com/julienschmidt/httprouter"
32+
"github.com/miolini/datacounter"
3233
"github.com/rs/zerolog"
3334
"github.com/rs/zerolog/log"
3435
)
@@ -67,6 +68,7 @@ func (rt Router) handleCheckin(w http.ResponseWriter, r *http.Request, ps httpro
6768
}
6869

6970
type CheckinT struct {
71+
verCon version.Constraints
7072
cfg *config.Server
7173
cache cache.Cache
7274
bc *BulkCheckin
@@ -79,6 +81,7 @@ type CheckinT struct {
7981
}
8082

8183
func NewCheckinT(
84+
verCon version.Constraints,
8285
cfg *config.Server,
8386
c cache.Cache,
8487
bc *BulkCheckin,
@@ -96,6 +99,7 @@ func NewCheckinT(
9699
Msg("Checkin install limits")
97100

98101
ct := &CheckinT{
102+
verCon: verCon,
99103
cfg: cfg,
100104
cache: c,
101105
bc: bc,
@@ -124,6 +128,11 @@ func (ct *CheckinT) _handleCheckin(w http.ResponseWriter, r *http.Request, id st
124128
return err
125129
}
126130

131+
err = validateUserAgent(r, ct.verCon)
132+
if err != nil {
133+
return err
134+
}
135+
127136
// Metrics; serenity now.
128137
dfunc := cntCheckin.IncStart()
129138
defer dfunc()

cmd/fleet/handleEnroll.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/elastic/go-elasticsearch/v8"
2626
"github.com/gofrs/uuid"
27+
"github.com/hashicorp/go-version"
2728
"github.com/julienschmidt/httprouter"
2829
"github.com/miolini/datacounter"
2930
"github.com/rs/zerolog/log"
@@ -41,18 +42,20 @@ var (
4142
)
4243

4344
type EnrollerT struct {
45+
verCon version.Constraints
4446
bulker bulk.Bulk
4547
cache cache.Cache
4648
limit *limit.Limiter
4749
}
4850

49-
func NewEnrollerT(cfg *config.Server, bulker bulk.Bulk, c cache.Cache) (*EnrollerT, error) {
51+
func NewEnrollerT(verCon version.Constraints, cfg *config.Server, bulker bulk.Bulk, c cache.Cache) (*EnrollerT, error) {
5052

5153
log.Info().
5254
Interface("limits", cfg.Limits.EnrollLimit).
5355
Msg("Enroller install limits")
5456

5557
return &EnrollerT{
58+
verCon: verCon,
5659
limit: limit.NewLimiter(&cfg.Limits.EnrollLimit),
5760
bulker: bulker,
5861
cache: c,
@@ -113,6 +116,11 @@ func (et *EnrollerT) handleEnroll(r *http.Request) ([]byte, error) {
113116
return nil, err
114117
}
115118

119+
err = validateUserAgent(r, et.verCon)
120+
if err != nil {
121+
return nil, err
122+
}
123+
116124
// Metrics; serenity now.
117125
dfunc := cntEnroll.IncStart()
118126
defer dfunc()

cmd/fleet/main.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131

3232
"github.com/elastic/elastic-agent-client/v7/pkg/client"
3333
"github.com/elastic/elastic-agent-client/v7/pkg/proto"
34+
"github.com/hashicorp/go-version"
3435
"github.com/rs/zerolog"
3536
"github.com/rs/zerolog/log"
3637
"github.com/spf13/cobra"
@@ -348,7 +349,8 @@ func (a *AgentMode) OnError(err error) {
348349
}
349350

350351
type FleetServer struct {
351-
version string
352+
ver string
353+
verCon version.Constraints
352354
policyId string
353355

354356
cfg *config.Config
@@ -358,9 +360,14 @@ type FleetServer struct {
358360
}
359361

360362
// NewFleetServer creates the actual fleet server service.
361-
func NewFleetServer(cfg *config.Config, c cache.Cache, version string, reporter status.Reporter) (*FleetServer, error) {
363+
func NewFleetServer(cfg *config.Config, c cache.Cache, verStr string, reporter status.Reporter) (*FleetServer, error) {
364+
verCon, err := buildVersionConstraint(verStr)
365+
if err != nil {
366+
return nil, err
367+
}
362368
return &FleetServer{
363-
version: version,
369+
ver: verStr,
370+
verCon: verCon,
364371
cfg: cfg,
365372
cfgCh: make(chan *config.Config, 1),
366373
cache: c,
@@ -513,7 +520,7 @@ func (f *FleetServer) runServer(ctx context.Context, cfg *config.Config) (err er
513520
}
514521

515522
g.Go(loggedRunFunc(ctx, "Policy index monitor", pim.Run))
516-
cord := coordinator.NewMonitor(cfg.Fleet, f.version, bulker, pim, coordinator.NewCoordinatorZero)
523+
cord := coordinator.NewMonitor(cfg.Fleet, f.ver, bulker, pim, coordinator.NewCoordinatorZero)
517524
g.Go(loggedRunFunc(ctx, "Coordinator policy monitor", cord.Run))
518525

519526
// Policy monitor
@@ -545,8 +552,8 @@ func (f *FleetServer) runServer(ctx context.Context, cfg *config.Config) (err er
545552
bc := NewBulkCheckin(bulker)
546553
g.Go(loggedRunFunc(ctx, "Bulk checkin", bc.Run))
547554

548-
ct := NewCheckinT(&f.cfg.Inputs[0].Server, f.cache, bc, pm, am, ad, tr, bulker)
549-
et, err := NewEnrollerT(&f.cfg.Inputs[0].Server, bulker, f.cache)
555+
ct := NewCheckinT(f.verCon, &f.cfg.Inputs[0].Server, f.cache, bc, pm, am, ad, tr, bulker)
556+
et, err := NewEnrollerT(f.verCon, &f.cfg.Inputs[0].Server, bulker, f.cache)
550557
if err != nil {
551558
return err
552559
}

cmd/fleet/metrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ var (
3636

3737
func (f *FleetServer) initMetrics(ctx context.Context, cfg *config.Config) (*api.Server, error) {
3838
registry := monitoring.GetNamespace("info").GetRegistry()
39-
monitoring.NewString(registry, "version").Set(f.version)
39+
monitoring.NewString(registry, "version").Set(f.ver)
4040
monitoring.NewString(registry, "name").Set("fleet-server")
4141
metrics.SetupMetrics("fleet-server")
4242

cmd/fleet/server_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@ func TestRunServer(t *testing.T) {
3333
cfg.Host = "localhost"
3434
cfg.Port = port
3535

36+
verCon := mustBuildConstraints("8.0.0")
3637
c, err := cache.New(cache.Config{NumCounters: 100, MaxCost: 100000})
3738
require.NoError(t, err)
3839
bulker := ftesting.MockBulk{}
3940
pim := mock.NewMockIndexMonitor()
4041
pm := policy.NewMonitor(bulker, pim, 5*time.Millisecond)
4142
bc := NewBulkCheckin(nil)
42-
ct := NewCheckinT(cfg, c, bc, pm, nil, nil, nil, nil)
43-
et, err := NewEnrollerT(cfg, nil, c)
43+
ct := NewCheckinT(verCon, cfg, c, bc, pm, nil, nil, nil, nil)
44+
et, err := NewEnrollerT(verCon, cfg, nil, c)
4445
require.NoError(t, err)
4546

4647
router := NewRouter(bulker, ct, et, nil, nil, nil)

cmd/fleet/userAgent.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package fleet
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"math"
11+
"net/http"
12+
"strconv"
13+
"strings"
14+
15+
"github.com/hashicorp/go-version"
16+
)
17+
18+
const (
19+
// MinVersion is the minimum version an Elastic Agent must be to communicate
20+
MinVersion = "7.13"
21+
22+
userAgentPrefix = "elastic agent "
23+
)
24+
25+
var (
26+
ErrInvalidUserAgent = errors.New("user-agent is invalid")
27+
ErrUnsupportedVersion = errors.New("version is not supported")
28+
)
29+
30+
// buildVersionConstraint turns the version into a constraint to ensure that the connecting Elastic Agent's are
31+
// a supported version.
32+
func buildVersionConstraint(verStr string) (version.Constraints, error) {
33+
ver, err := version.NewVersion(verStr)
34+
if err != nil {
35+
return nil, err
36+
}
37+
verStr = maximizePatch(ver)
38+
return version.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinVersion, verStr))
39+
}
40+
41+
// maximizePatch turns the version into a string that has the patch value set to the maximum integer.
42+
//
43+
// Used to allow the Elastic Agent to be at a higher patch version than the Fleet Server, but require that the
44+
// Elastic Agent is not higher in MAJOR or MINOR.
45+
func maximizePatch(ver *version.Version) string {
46+
segments := ver.Segments()
47+
if len(segments) > 2 {
48+
segments = segments[:2]
49+
}
50+
segments = append(segments, math.MaxInt32)
51+
segStrs := make([]string, 0, len(segments))
52+
for _, segment := range segments {
53+
segStrs = append(segStrs, strconv.Itoa(segment))
54+
}
55+
return strings.Join(segStrs, ".")
56+
}
57+
58+
// validateUserAgent validates that the User-Agent of the connecting Elastic Agent is valid and that the version is
59+
// supported for this Fleet Server.
60+
func validateUserAgent(r *http.Request, verConst version.Constraints) error {
61+
userAgent := r.Header.Get("User-Agent")
62+
if userAgent == "" {
63+
return ErrInvalidUserAgent
64+
}
65+
userAgent = strings.ToLower(userAgent)
66+
if !strings.HasPrefix(userAgent, userAgentPrefix) {
67+
return ErrInvalidUserAgent
68+
}
69+
verStr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(userAgent, userAgentPrefix), "-snapshot"))
70+
ver, err := version.NewVersion(verStr)
71+
if err != nil {
72+
return ErrInvalidUserAgent
73+
}
74+
if !verConst.Check(ver) {
75+
return ErrUnsupportedVersion
76+
}
77+
return nil
78+
}

cmd/fleet/userAgent_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package fleet
6+
7+
import (
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/hashicorp/go-version"
12+
)
13+
14+
func TestValidateUserAgent(t *testing.T) {
15+
tests := []struct {
16+
userAgent string
17+
verCon version.Constraints
18+
err error
19+
}{
20+
{
21+
userAgent: "",
22+
verCon: nil,
23+
err: ErrInvalidUserAgent,
24+
},
25+
{
26+
userAgent: "bad value",
27+
verCon: nil,
28+
err: ErrInvalidUserAgent,
29+
},
30+
{
31+
userAgent: "eLaStIc AGeNt",
32+
verCon: nil,
33+
err: ErrInvalidUserAgent,
34+
},
35+
{
36+
userAgent: "eLaStIc AGeNt v7.10.0",
37+
verCon: mustBuildConstraints("7.13.0"),
38+
err: ErrUnsupportedVersion,
39+
},
40+
{
41+
userAgent: "eLaStIc AGeNt v7.11.1",
42+
verCon: mustBuildConstraints("7.13.0"),
43+
err: ErrUnsupportedVersion,
44+
},
45+
{
46+
userAgent: "eLaStIc AGeNt v7.12.5",
47+
verCon: mustBuildConstraints("7.13.0"),
48+
err: ErrUnsupportedVersion,
49+
},
50+
{
51+
userAgent: "eLaStIc AGeNt v7.13.0",
52+
verCon: mustBuildConstraints("7.13.0"),
53+
err: nil,
54+
},
55+
{
56+
userAgent: "eLaStIc AGeNt v7.13.0",
57+
verCon: mustBuildConstraints("7.13.1"),
58+
err: nil,
59+
},
60+
{
61+
userAgent: "eLaStIc AGeNt v7.13.1",
62+
verCon: mustBuildConstraints("7.13.0"),
63+
err: nil,
64+
},
65+
{
66+
userAgent: "eLaStIc AGeNt v7.14.0",
67+
verCon: mustBuildConstraints("7.13.0"),
68+
err: ErrUnsupportedVersion,
69+
},
70+
{
71+
userAgent: "eLaStIc AGeNt v8.0.0",
72+
verCon: mustBuildConstraints("7.13.0"),
73+
err: ErrUnsupportedVersion,
74+
},
75+
{
76+
userAgent: "eLaStIc AGeNt v7.13.0",
77+
verCon: mustBuildConstraints("8.0.0"),
78+
err: nil,
79+
},
80+
}
81+
for _, tr := range tests {
82+
t.Run(tr.userAgent, func(t *testing.T) {
83+
req := httptest.NewRequest("GET", "/", nil)
84+
req.Header.Set("User-Agent", tr.userAgent)
85+
res := validateUserAgent(req, tr.verCon)
86+
if tr.err != res {
87+
t.Fatalf("err mismatch: %v != %v", tr.err, res)
88+
}
89+
})
90+
}
91+
}
92+
93+
func mustBuildConstraints(verStr string) version.Constraints {
94+
con, err := buildVersionConstraint(verStr)
95+
if err != nil {
96+
panic(err)
97+
}
98+
return con
99+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/gofrs/uuid v3.3.0+incompatible
1313
github.com/google/go-cmp v0.4.0
1414
github.com/hashicorp/go-cleanhttp v0.5.1
15+
github.com/hashicorp/go-version v1.3.0
1516
github.com/hashicorp/golang-lru v0.5.2-0.20190520140433-59383c442f7d
1617
github.com/julienschmidt/httprouter v1.3.0
1718
github.com/miolini/datacounter v1.0.2

go.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,9 @@ github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP
449449
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
450450
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
451451
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
452-
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
453452
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
453+
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
454+
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
454455
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
455456
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
456457
github.com/hashicorp/golang-lru v0.5.2-0.20190520140433-59383c442f7d h1:Ft6PtvobE9vwkCsuoNO5DZDbhKkKuktAlSsiOi1X5NA=

0 commit comments

Comments
 (0)