Harden production workflows and agent admission
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -14,19 +15,24 @@ import (
|
||||
var idPattern = regexp.MustCompile(`^[A-Za-z0-9._:-]+$`)
|
||||
|
||||
const (
|
||||
maxIDLen = 128
|
||||
maxLabelKeyLen = 64
|
||||
maxLabelValueLen = 256
|
||||
maxLabels = 32
|
||||
maxIDLen = 128
|
||||
maxLabelKeyLen = 64
|
||||
maxLabelValueLen = 256
|
||||
maxLabels = 32
|
||||
// PH-019: bounded check inventory keeps {check_id} metric cardinality
|
||||
// predictable. ValidateID already restricts each check_id to a stable
|
||||
// identifier alphabet; this cap bounds the total. Override via
|
||||
// MONLET_AGENT_MAX_CHECKS for fleets that genuinely need more.
|
||||
defaultMaxChecks = 256
|
||||
AgentVersionLabel = "monlet_agent_version"
|
||||
defaultHeartbeat = 30 * time.Second
|
||||
defaultHeartbeat = 10 * time.Second
|
||||
defaultBatch = 10 * time.Second
|
||||
defaultMetricsAddr = "127.0.0.1:9465"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AgentID string `toml:"agent_id"`
|
||||
Hostname string `toml:"hostname"`
|
||||
Hostname string `toml:"-"`
|
||||
StateDir string `toml:"state_dir"`
|
||||
Labels map[string]string `toml:"labels"`
|
||||
Server ServerConfig `toml:"server"`
|
||||
@@ -37,7 +43,6 @@ type Config struct {
|
||||
type ServerConfig struct {
|
||||
Enabled *bool `toml:"enabled"`
|
||||
URL string `toml:"url"`
|
||||
Token string `toml:"token"`
|
||||
HeartbeatInterval Duration `toml:"heartbeat_interval"`
|
||||
BatchInterval Duration `toml:"batch_interval"`
|
||||
}
|
||||
@@ -124,11 +129,8 @@ func (c *Config) Validate() error {
|
||||
if c.Server.URL == "" {
|
||||
return fmt.Errorf("server.url is required when server.enabled = true")
|
||||
}
|
||||
if c.Server.Token == "" {
|
||||
return fmt.Errorf("server.token is required when server.enabled = true")
|
||||
}
|
||||
} else if c.Server.URL != "" || c.Server.Token != "" {
|
||||
return fmt.Errorf("server.url/token require server.enabled = true")
|
||||
} else if c.Server.URL != "" {
|
||||
return fmt.Errorf("server.url requires server.enabled = true")
|
||||
}
|
||||
if !c.PushesToServer() && !c.ExposesMetrics() {
|
||||
return fmt.Errorf("server.enabled or metrics.enabled must be true")
|
||||
@@ -136,6 +138,17 @@ func (c *Config) Validate() error {
|
||||
if len(c.Checks) == 0 {
|
||||
return fmt.Errorf("at least one check is required")
|
||||
}
|
||||
maxChecks := defaultMaxChecks
|
||||
if v, ok := os.LookupEnv("MONLET_AGENT_MAX_CHECKS"); ok {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n <= 0 {
|
||||
return fmt.Errorf("MONLET_AGENT_MAX_CHECKS must be a positive integer, got %q", v)
|
||||
}
|
||||
maxChecks = n
|
||||
}
|
||||
if len(c.Checks) > maxChecks {
|
||||
return fmt.Errorf("too many checks: %d (max %d) — bounded for metric cardinality (PH-019)", len(c.Checks), maxChecks)
|
||||
}
|
||||
seen := make(map[string]struct{}, len(c.Checks))
|
||||
for i := range c.Checks {
|
||||
ch := &c.Checks[i]
|
||||
|
||||
@@ -24,7 +24,6 @@ state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
@@ -57,7 +56,6 @@ state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
@@ -87,7 +85,6 @@ state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
@@ -100,6 +97,36 @@ timeout = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyCommandRejected(t *testing.T) {
|
||||
for name, cmd := range map[string]string{
|
||||
"empty": `""`,
|
||||
"whitespace": `" \t "`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := Load(writeTmp(t, `
|
||||
agent_id = "host-1"
|
||||
state_dir = "/tmp/x"
|
||||
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = `+cmd+`
|
||||
interval = "10s"
|
||||
timeout = "5s"
|
||||
`))
|
||||
if err == nil {
|
||||
t.Fatal("expected empty command to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "command is required") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadLabels(t *testing.T) {
|
||||
c, err := Load(writeTmp(t, `
|
||||
agent_id = "host-1"
|
||||
@@ -112,7 +139,6 @@ role = "api"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
@@ -148,7 +174,6 @@ state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
@@ -167,7 +192,6 @@ state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
@@ -188,7 +212,6 @@ state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
@@ -268,7 +291,28 @@ interval = "10s"
|
||||
timeout = "5s"
|
||||
`))
|
||||
if err == nil {
|
||||
t.Fatal("push_only must require server.url/token")
|
||||
t.Fatal("push_only must require server.url")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerTokenRejected(t *testing.T) {
|
||||
_, err := Load(writeTmp(t, `
|
||||
state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "obsolete"
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
interval = "10s"
|
||||
timeout = "5s"
|
||||
`))
|
||||
if err == nil {
|
||||
t.Fatal("expected obsolete server.token to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "server.token") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +323,6 @@ state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
@@ -291,6 +334,27 @@ timeout = "5s"
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostnameConfigKeyRejected(t *testing.T) {
|
||||
_, err := Load(writeTmp(t, `
|
||||
hostname = "host-1"
|
||||
state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
interval = "10s"
|
||||
timeout = "5s"
|
||||
`))
|
||||
if err == nil {
|
||||
t.Fatal("expected hostname key to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "hostname") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationsEnabledCanDisable(t *testing.T) {
|
||||
c, err := Load(writeTmp(t, minimal+`
|
||||
notifications_enabled = false
|
||||
@@ -329,7 +393,6 @@ state_dir = "/tmp/x"
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
token = "t"
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
|
||||
Reference in New Issue
Block a user