feat(scripts.d/nagios): validate plugin path security (#22160)
Ilya Mashchenko committed
Apr 7, 2026 at 20:23 UTC
f6f0b6f137905cdec4d7b79300dc767bc5ece84a
10 files changed
+148
-152
src/go/plugin/go.d/collector/azure_monitor/azureprofiles/default_catalog_test.go
+1
-1
@@ -59,7 +59,7 @@ func TestLoadFromDefaultDirs_StockProfilesUseSelectorShorthand(t *testing.T) {
59
data, err := os.ReadFile(filepath.Join(dir, name))
60
require.NoError(t, err)
61
62
- for _, line := range strings.Split(string(data), "\n") {
62
+ for line := range strings.SplitSeq(string(data), "\n") {
63
line = strings.TrimSpace(line)
64
if !strings.HasPrefix(line, "- selector:") {
65
continue
src/go/plugin/go.d/pkg/pathvalidate/validate_unix.go
+21
-16
@@ -56,24 +56,29 @@ func ValidateBinaryPath(path string) (string, error) {
56
return "", fmt.Errorf("binary at %s must be executable", absPath)
57
}
58
59
- // Step 7: Check parent directory
60
- dir := filepath.Dir(absPath)
61
- dirInfo, err := os.Stat(dir)
62
- if err != nil {
63
- return "", fmt.Errorf("directory stat error for %s: %w", dir, err)
64
- }
59
+ // Step 7: Check all ancestor directories up to and including root
60
+ for dir := filepath.Dir(absPath); ; dir = filepath.Dir(dir) {
61
+ dirInfo, err := os.Stat(dir)
62
+ if err != nil {
63
+ return "", fmt.Errorf("directory stat error for %s: %w", dir, err)
64
+ }
65
66
- dirStat, ok := dirInfo.Sys().(*syscall.Stat_t)
67
- if !ok {
68
- return "", fmt.Errorf("unable to get directory stat information for %s", dir)
69
- }
70
- if dirStat.Uid != 0 {
71
- return "", fmt.Errorf("directory %s must be owned by root (current uid: %d)", dir, dirStat.Uid)
72
- }
66
+ dirStat, ok := dirInfo.Sys().(*syscall.Stat_t)
67
+ if !ok {
68
+ return "", fmt.Errorf("unable to get directory stat information for %s", dir)
69
+ }
70
+ if dirStat.Uid != 0 {
71
+ return "", fmt.Errorf("directory %s must be owned by root (current uid: %d)", dir, dirStat.Uid)
72
+ }
73
+
74
+ if perm := dirInfo.Mode().Perm(); perm&0022 != 0 {
75
+ return "", fmt.Errorf("directory %s must not be writable by group/others (current permissions: %s / %04o)",
76
+ dir, dirInfo.Mode().String(), perm)
77
+ }
78
74
- if perm := dirInfo.Mode().Perm(); perm&0022 != 0 {
75
- return "", fmt.Errorf("directory %s must not be writable by group/others (current permissions: %s / %04o)",
76
- dir, dirInfo.Mode().String(), perm)
79
+ if dir == filepath.Dir(dir) {
80
+ break
81
+ }
82
}
83
84
return absPath, nil
src/go/plugin/scripts.d/collector/nagios/collector.go
+12
-9
@@ -10,6 +10,7 @@ import (
10
"github.com/netdata/netdata/go/plugins/pkg/metrix"
11
"github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
12
"github.com/netdata/netdata/go/plugins/plugin/framework/vnodes"
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/pathvalidate"
14
"github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/timeperiod"
15
)
16
@@ -45,11 +46,12 @@ type Collector struct {
46
collectorapi.Base
47
Config `yaml:",inline" json:",inline"`
48
48
- store metrix.CollectorStore
49
- router *perfdataRouter
50
- runner checkRunner
51
- now func() time.Time
52
- vnode vnodes.VirtualNode
49
+ store metrix.CollectorStore
50
+ router *perfdataRouter
51
+ runner checkRunner
52
+ validatePlugin func(string) (string, error)
53
+ now func() time.Time
54
+ vnode vnodes.VirtualNode
55
56
job compiledJob
57
state collectState
@@ -63,10 +65,11 @@ func New() *Collector {
65
UpdateEvery: defaultCollectorUpdateEvery,
66
JobConfig: defaultedJobConfig(JobConfig{}),
67
},
66
- store: metrix.NewCollectorStore(),
67
- router: newPerfdataRouter(defaultPerfdataMetricKeyBudget),
68
- runner: systemCheckRunner{},
69
- now: time.Now,
68
+ store: metrix.NewCollectorStore(),
69
+ router: newPerfdataRouter(defaultPerfdataMetricKeyBudget),
70
+ runner: systemCheckRunner{},
71
+ validatePlugin: pathvalidate.ValidateBinaryPath,
72
+ now: time.Now,
73
}
74
}
75
src/go/plugin/scripts.d/collector/nagios/collector_test.go
+33
-73
@@ -168,26 +168,6 @@ func TestCollector_Check(t *testing.T) {
168
},
169
wantErr: false,
170
},
171
- "plugin path does not exist": {
172
- config: Config{
173
- JobConfig: JobConfig{
174
- Name: "missing-plugin",
175
- Plugin: filepath.Join(t.TempDir(), "missing-check"),
176
- },
177
- },
178
- wantErr: true,
179
- errMatch: "stat error",
180
- },
181
- "plugin path must be a regular file": {
182
- config: Config{
183
- JobConfig: JobConfig{
184
- Name: "plugin-dir",
185
- Plugin: t.TempDir(),
186
- },
187
- },
188
- wantErr: true,
189
- errMatch: "must be a regular file",
190
- },
171
"valid config": {
172
config: Config{
173
UpdateEvery: 1,
@@ -203,7 +183,7 @@ func TestCollector_Check(t *testing.T) {
183
184
for name, tc := range tests {
185
t.Run(name, func(t *testing.T) {
206
- coll := New()
186
+ coll := newTestCollector()
187
coll.runner = &fakeRunner{}
188
coll.Config = tc.config
189
@@ -220,59 +200,33 @@ func TestCollector_Check(t *testing.T) {
200
}
201
}
202
223
-func TestCollector_CheckRevalidatesConfiguredPlugin(t *testing.T) {
224
- dir := t.TempDir()
225
- pluginPath := filepath.Join(dir, "check_mock.sh")
226
- mode := os.FileMode(0o644)
227
- if runtime.GOOS != "windows" {
228
- mode = 0o755
229
- }
230
- require.NoError(t, os.WriteFile(pluginPath, []byte("#!/bin/sh\nexit 0\n"), mode))
231
-
232
- coll := New()
233
- coll.runner = &fakeRunner{}
234
- coll.Config = Config{
235
- UpdateEvery: 1,
236
- JobConfig: JobConfig{
237
- Name: "revalidate",
238
- Plugin: pluginPath,
239
- CheckInterval: confDuration(5 * time.Second),
240
- RetryInterval: confDuration(5 * time.Second),
241
- },
242
- }
243
-
244
- require.NoError(t, coll.Init(context.Background()))
245
- require.NoError(t, os.Remove(pluginPath))
246
-
247
- err := coll.Check(context.Background())
248
- require.Error(t, err)
249
- assert.Contains(t, err.Error(), "stat error")
250
-}
251
-
252
-func TestCollector_CheckRejectsNonExecutablePlugin(t *testing.T) {
253
- if runtime.GOOS == "windows" {
254
- t.Skip("windows does not use unix executable bits")
203
+func TestIsKnownInterpreter(t *testing.T) {
204
+ tests := map[string]struct {
205
+ path string
206
+ want bool
207
+ }{
208
+ "bash": {path: "/bin/bash", want: true},
209
+ "sh": {path: "/bin/sh", want: true},
210
+ "python3": {path: "/usr/bin/python3", want: true},
211
+ "python3 versioned": {path: "/usr/bin/python3.11", want: true},
212
+ "powershell": {path: "/usr/bin/powershell", want: true},
213
+ "powershell.exe": {path: "/powershell.exe", want: true},
214
+ "pwsh": {path: "/usr/bin/pwsh", want: true},
215
+ "env": {path: "/usr/bin/env", want: true},
216
+ "node": {path: "/usr/bin/node", want: true},
217
+ "cmd.exe": {path: "/cmd.exe", want: true},
218
+ "php": {path: "/usr/bin/php", want: true},
219
+ "check_ping": {path: "/usr/lib/nagios/plugins/check_ping", want: false},
220
+ "check_http": {path: "/usr/lib/nagios/plugins/check_http", want: false},
221
+ "custom script": {path: "/opt/netdata/checks/check_api.sh", want: false},
222
+ "custom exe": {path: "/opt/checks/check_service.exe", want: false},
223
}
224
257
- dir := t.TempDir()
258
- pluginPath := filepath.Join(dir, "check_mock.sh")
259
- require.NoError(t, os.WriteFile(pluginPath, []byte("#!/bin/sh\nexit 0\n"), 0o644))
260
-
261
- coll := New()
262
- coll.runner = &fakeRunner{}
263
- coll.Config = Config{
264
- UpdateEvery: 1,
265
- JobConfig: JobConfig{
266
- Name: "not-executable",
267
- Plugin: pluginPath,
268
- CheckInterval: confDuration(5 * time.Second),
269
- RetryInterval: confDuration(5 * time.Second),
270
- },
225
+ for name, tc := range tests {
226
+ t.Run(name, func(t *testing.T) {
227
+ assert.Equal(t, tc.want, isKnownInterpreter(tc.path))
228
+ })
229
}
272
-
273
- err := coll.Check(context.Background())
274
- require.Error(t, err)
275
- assert.Contains(t, err.Error(), "must be executable")
230
}
231
232
func TestCompileCollectorConfig_CadenceWarning(t *testing.T) {
@@ -351,7 +305,7 @@ func TestCollector_Init(t *testing.T) {
305
306
for name, tc := range tests {
307
t.Run(name, func(t *testing.T) {
354
- coll := New()
308
+ coll := newTestCollector()
309
coll.runner = &fakeRunner{}
310
coll.Config = tc.config
311
require.NoError(t, coll.Init(context.Background()))
@@ -930,7 +884,7 @@ func TestCollector_Collect(t *testing.T) {
884
t.Run(name, func(t *testing.T) {
885
now := time.Date(2026, 3, 21, 12, 0, 0, 0, time.UTC)
886
runner := &fakeRunner{results: tc.results}
933
- coll := New()
887
+ coll := newTestCollector()
888
coll.runner = runner
889
coll.now = func() time.Time { return now }
890
coll.Config = tc.config
@@ -1270,6 +1224,12 @@ func writeTestPluginFile(t *testing.T, name string) string {
1224
return path
1225
}
1226
1227
+func newTestCollector() *Collector {
1228
+ coll := New()
1229
+ coll.validatePlugin = func(path string) (string, error) { return path, nil }
1230
+ return coll
1231
+}
1232
+
1233
func confDuration(d time.Duration) confopt.Duration { return confopt.Duration(d) }
1234
1235
func findChartDimensionByContext(specYAML *charttpl.Spec, context string) (charttpl.Dimension, bool) {
src/go/plugin/scripts.d/collector/nagios/config_schema.json
+13
-5
@@ -20,12 +20,12 @@
20
},
21
"check_name": {
22
"title": "Check name",
23
- "description": "Name that identifies this check type for chart grouping and metric naming. Charts appear under `Perfdata/<check_name>` in the dashboard. When `plugin` points to an interpreter, set this to the actual check identity — otherwise all interpreter-based jobs share a single chart section. If omitted, derived from the `plugin` basename.",
23
+ "description": "Name that identifies this check type for chart grouping and metric naming. Charts appear under `Perfdata/<check_name>` in the dashboard. If omitted, derived from the `plugin` basename.",
24
"type": "string"
25
},
26
"plugin": {
27
"title": "Plugin path",
28
- "description": "Absolute path to the Nagios-compatible command entry point. This can be a packaged plugin, your own executable, or an interpreter. When `plugin` points to an interpreter, pass the script path in `args` and set `check_name` to the actual check identity. The command should return exit code 0, 1, 2, or 3 and may print performance data after `|`.",
28
+ "description": "Absolute path to the Nagios-compatible check command. On Linux/macOS, must be root-owned and not writable by group or others. The command should return exit code 0, 1, 2, or 3 and may print performance data after `|`.",
29
"type": "string"
30
},
31
"args": {
@@ -258,13 +258,15 @@
258
},
259
"check_name": {
260
"ui:placeholder": "check_ping",
261
- "ui:help": "Controls the chart section name and metric context for performance data. Required when plugin is an interpreter."
261
+ "ui:help": "**Controls chart grouping and metric naming for performance data.**\n\nCharts appear under `Synthetic > Nagios > Perfdata > <check_name>` in the dashboard.\n\nIf omitted, Netdata derives it from the `plugin` basename (e.g. `/usr/lib/nagios/plugins/check_ping` → `check_ping`).\n\n### When to set it\n\n- When multiple jobs run the same plugin (e.g. `check_nrpe`) against different remote checks — set `check_name` to distinguish them (`check_disk`, `check_load`, etc.)\n- **Windows:** Always set it when using `powershell.exe` as `plugin` — otherwise all PowerShell jobs share `Perfdata/powershell`\n\n### Example metric names\n\nWith `check_name: check_memory` and a script that outputs `caches=2380912KB`:\n\n- `nagios.perfdata.check_memory.job.execution_state`\n- `nagios.perfdata.check_memory.bytes_caches`\n- `nagios.perfdata.check_memory.bytes_caches_threshold_state`"
262
},
263
"plugin": {
264
- "ui:placeholder": "/usr/lib/nagios/plugins/check_ping"
264
+ "ui:placeholder": "/usr/lib/nagios/plugins/check_ping",
265
+ "ui:help": "**Absolute path to the check command.**\n\n### Linux / macOS\n\nThe executable must meet these security requirements:\n- Owned by **root**\n- Not writable by group or others\n- All ancestor directories owned by root and not group/other-writable\n- Must be a regular, executable file\n- Symlinks are resolved before validation\n\nExample:\n```\n/usr/lib/nagios/plugins/check_ping\n```\n\n### Windows\n\nSince `.ps1` scripts cannot be executed directly, point `plugin` to `powershell.exe` and pass the script in `args`:\n```\nC:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\n```\nPath validation is not enforced on Windows — ensure scripts are stored in directories with appropriate ACLs.\n\n### Output format\n\nThe command should return exit code `0` (OK), `1` (WARNING), `2` (CRITICAL), or `3` (UNKNOWN) and may print performance data after `|`:\n```\nSTATUS TEXT | label=value;warn;crit;min;max\n```"
266
},
267
"args": {
267
- "ui:listFlavour": "list"
268
+ "ui:listFlavour": "list",
269
+ "ui:help": "**Arguments passed to the check command after macro expansion.**\n\nYou can use Nagios-style macros like `$ARG1$`, `$HOSTADDRESS$`, `$_SERVICEDBHOST$` — they are expanded before execution.\n\n### Windows (PowerShell)\n\nWhen `plugin` is `powershell.exe`, pass the script path here:\n```\n-NoProfile\n-ExecutionPolicy\nBypass\n-File\nC:\\Netdata\\checks\\check_service.ps1\n```"
270
},
271
"arg_values": {
272
"ui:listFlavour": "list",
@@ -285,6 +287,12 @@
287
"time_periods": {
288
"ui:listFlavour": "list"
289
},
290
+ "environment": {
291
+ "ui:help": "**Extra environment variables for the check process.**\n\nChecks run with a limited baseline environment (only `PATH`, `HOME`, `USER`, `TZ`, `TMPDIR`, `SHELL`, `LC_ALL` on Linux/macOS). If your script depends on additional variables, set them here.\n\n### Example\n```yaml\nenvironment:\n ORACLE_HOME: /opt/oracle/product/19c\n LD_LIBRARY_PATH: /opt/oracle/product/19c/lib\n```"
292
+ },
293
+ "custom_vars": {
294
+ "ui:help": "**Custom service variables exposed as Nagios-style macros.**\n\nEach entry is available as:\n- Environment variable: `NAGIOS__SERVICE<UPPERCASE_KEY>`\n- Macro in `args`: `$_SERVICE<KEY>$`\n\n### Example\n```yaml\ncustom_vars:\n DBHOST: db.example.com\n DBNAME: production\n```\nUse in args: `[\"-H\", \"$_SERVICEDBHOST$\", \"-d\", \"$_SERVICEDBNAME$\"]`"
295
+ },
296
"vnode": {
297
"ui:placeholder": "To use this option, first create a Virtual Node and then reference its name here."
298
},
src/go/plugin/scripts.d/collector/nagios/init.go
+32
-15
@@ -4,8 +4,10 @@ package nagios
4
5
import (
6
"fmt"
7
- "os"
7
+ "path/filepath"
8
"runtime"
9
+ "slices"
10
+ "strings"
11
)
12
13
func (c *Collector) initCollector() error {
@@ -28,9 +30,20 @@ func (c *Collector) compileConfiguredJob() (compiledJob, error) {
30
if err != nil {
31
return compiledJob{}, err
32
}
31
- if err := validateConfiguredPlugin(job.config); err != nil {
32
- return compiledJob{}, err
33
+
34
+ validatedPath, err := c.validatePlugin(job.config.Plugin)
35
+ if err != nil {
36
+ return compiledJob{}, fmt.Errorf("job '%s': %w", job.config.Name, err)
37
}
38
+ job.config.Plugin = validatedPath
39
+
40
+ if runtime.GOOS != "windows" && isKnownInterpreter(job.config.Plugin) {
41
+ c.Warningf("job '%s': plugin '%s' appears to be an interpreter; "+
42
+ "Netdata validates the interpreter binary but cannot verify scripts passed in args — "+
43
+ "ensure scripts are root-owned and not writable by group/others",
44
+ job.config.Name, job.config.Plugin)
45
+ }
46
+
47
c.warnCadenceResolution(job)
48
return job, nil
49
}
@@ -43,16 +56,20 @@ func (c *Collector) warnCadenceResolution(job compiledJob) {
56
c.cadenceWarning = job.cadenceWarning
57
}
58
46
-func validateConfiguredPlugin(job JobConfig) error {
47
- info, err := os.Stat(job.Plugin)
48
- if err != nil {
49
- return fmt.Errorf("job '%s': plugin path '%s' stat error: %w", job.Name, job.Plugin, err)
50
- }
51
- if !info.Mode().IsRegular() {
52
- return fmt.Errorf("job '%s': plugin path '%s' must be a regular file", job.Name, job.Plugin)
53
- }
54
- if runtime.GOOS != "windows" && info.Mode().Perm()&0o111 == 0 {
55
- return fmt.Errorf("job '%s': plugin path '%s' must be executable", job.Name, job.Plugin)
56
- }
57
- return nil
59
+var knownInterpreters = []string{
60
+ "bash", "sh", "dash", "zsh", "ksh", "csh", "tcsh", "fish",
61
+ "python", "python2", "python3",
62
+ "perl", "ruby", "lua",
63
+ "powershell", "pwsh",
64
+ "cmd",
65
+ "node",
66
+ "env",
67
+ "php",
68
+ "tclsh", "wish", "expect",
69
+}
70
+
71
+func isKnownInterpreter(pluginPath string) bool {
72
+ base := filepath.Base(pluginPath)
73
+ name := strings.TrimSuffix(base, filepath.Ext(base))
74
+ return slices.Contains(knownInterpreters, strings.ToLower(name))
75
}
src/go/plugin/scripts.d/collector/nagios/job_v2_integration_test.go
+2
-2
@@ -49,7 +49,7 @@ func TestNagiosCollectorJobV2(t *testing.T) {
49
},
50
},
51
}
52
- coll := New()
52
+ coll := newTestCollector()
53
coll.runner = runner
54
coll.now = func() time.Time { return now }
55
coll.Config.JobConfig = JobConfig{
@@ -89,7 +89,7 @@ func TestNagiosCollectorJobV2(t *testing.T) {
89
scriptPath := filepath.Join(dir, "check_slow.sh")
90
writeExecutable(t, scriptPath, "#!/bin/sh\nset -eu\nstarted_file=\"$1\"\necho started > \"$started_file\"\ntrap 'exit 0' TERM INT\nsleep 30\n")
91
92
- coll := New()
92
+ coll := newTestCollector()
93
coll.Config.JobConfig = JobConfig{
94
Name: "cancel_job",
95
Plugin: scriptPath,
src/go/plugin/scripts.d/collector/nagios/metadata.yaml
+33
-21
@@ -167,6 +167,28 @@ modules:
167
setup:
168
prerequisites:
169
list:
170
+ - title: Security requirements for plugin executables
171
+ description: |
172
+ Netdata validates the `plugin` path before execution. On Linux/macOS, the executable must meet these requirements:
173
+
174
+ - Must be a regular file (not a directory or device node)
175
+ - Must be executable (at least one execute bit set)
176
+ - Owned by **root**
177
+ - Not writable by group or others (no `g+w` or `o+w`)
178
+ - All ancestor directories (up to and including `/`) owned by **root**
179
+ - All ancestor directories not writable by group or others
180
+ - Symlinks are resolved — the target must meet these rules
181
+
182
+ On Windows, path validation is not enforced. Ensure executables are stored in directories with appropriate ACLs.
183
+
184
+ This prevents local privilege escalation through a modified check script. If validation fails, the job will not start and an error is logged.
185
+
186
+ :::caution
187
+
188
+ - **Linux/macOS:** Using an interpreter (e.g. `/bin/bash`) as `plugin` with a script path in `args` is **discouraged**. Netdata validates the interpreter binary but **cannot verify scripts passed via `args`**. A writable script in `args` is a privilege escalation vector. Instead, make scripts directly executable and point `plugin` to the script itself.
189
+ - **Windows:** Since `.ps1` scripts cannot be executed directly, use `powershell.exe` as `plugin` and pass the script in `args`. Path validation is not enforced on Windows — ensure scripts are stored in directories with appropriate ACLs.
190
+
191
+ :::
192
- title: Install check commands
193
description: |
194
Install the Nagios plugins or other Nagios-compatible scripts that you want Netdata to run.
@@ -181,13 +203,13 @@ modules:
203
dnf install nagios-plugins-all
204
```
205
184
- Make sure the configured command path exists and is executable by the `netdata` user.
206
+ Packaged Nagios plugins are typically installed as root-owned executables, which satisfies the security requirements above.
207
- title: Prepare custom check scripts
208
description: |
209
If you are writing your own check scripts instead of using packaged Nagios plugins:
210
189
- - Place scripts anywhere accessible to the `netdata` user (e.g., `/usr/local/lib/netdata/checks/`)
190
- - Make scripts executable: `chmod +x /path/to/script.sh`
211
+ - Place scripts in a root-owned directory (e.g., `/usr/local/lib/netdata/checks/`)
212
+ - Set ownership and permissions: `sudo chown root:root /path/to/script.sh && sudo chmod 755 /path/to/script.sh`
213
- Test as the `netdata` user to verify permissions and environment: `sudo -u netdata /path/to/script.sh`
214
- Verify the exit code: `echo $?` (must be 0, 1, 2, or 3)
215
- Verify the output matches the Nagios plugin output format described in the Overview above
@@ -209,7 +231,7 @@ modules:
231
232
- name: check_name
233
description: |
212
- Name that identifies this check type for chart grouping and metric naming. If omitted, Netdata derives it from the basename of `plugin` (removing any file extension). Set this when `plugin` points to an interpreter (e.g. `powershell.exe`, `/bin/bash`) — otherwise all jobs using the same interpreter share the same chart section.
234
+ Name that identifies this check type for chart grouping and metric naming. If omitted, Netdata derives it from the basename of `plugin` (removing any file extension). Use this when the plugin filename is generic and you want a more descriptive chart section — for example, when multiple jobs run `check_nrpe` against different remote checks, set `check_name` to distinguish them (`check_disk`, `check_load`, etc.).
235
236
In the dashboard, charts appear under `Synthetic > Nagios > Perfdata > <check_name>`. For example, with `check_name: check_memory` and a script that outputs `caches=2380912KB`, Netdata creates:
237
@@ -220,7 +242,7 @@ modules:
242
required: false
243
group: Target
244
- name: plugin
223
- description: Absolute path to the Nagios-compatible command entry point to run. This can be a packaged Nagios plugin, your own executable, or an interpreter executable. When `plugin` points to an interpreter, pass the script path in `args` and set `check_name` to the actual check identity — otherwise all jobs using the same interpreter share the same chart section. The command should return exit code `0`, `1`, `2`, or `3` and may print performance data after <code>|</code>.
245
+ description: Absolute path to the Nagios-compatible check command to run. This can be a packaged Nagios plugin or your own executable script. The executable must be root-owned and not writable by group or others (see prerequisites). The command should return exit code `0`, `1`, `2`, or `3` and may print performance data after <code>|</code>.
246
default_value: ""
247
required: true
248
group: Target
@@ -366,10 +388,11 @@ modules:
388
exit 0
389
```
390
369
- **2. Make it executable and test it:**
391
+ **2. Set ownership, permissions, and test it:**
392
393
```bash
372
- chmod +x /usr/local/lib/netdata/checks/check_api.sh
394
+ sudo chown root:root /usr/local/lib/netdata/checks/check_api.sh
395
+ sudo chmod 755 /usr/local/lib/netdata/checks/check_api.sh
396
sudo -u netdata /usr/local/lib/netdata/checks/check_api.sh
397
echo "Exit code: $?"
398
```
@@ -391,9 +414,9 @@ modules:
414
plugin: /opt/netdata/check_memory.sh
415
timeout: 5s
416
check_interval: 1m
394
- - name: End-to-end custom script (Windows PowerShell)
417
+ - name: Windows PowerShell check
418
description: |
396
- Write a PowerShell check script, then configure Netdata to run it. Because `plugin` points to the PowerShell interpreter, `check_name` is required to keep charts grouped under the script identity instead of `powershell`.
419
+ On Windows, `.ps1` scripts cannot be executed directly. Point `plugin` to `powershell.exe` and pass the script in `args`. Set `check_name` so that charts are grouped under the script identity instead of `powershell`.
420
421
**1. Create the script** (e.g., `C:\Netdata\checks\check_service.ps1`):
422
@@ -433,17 +456,6 @@ modules:
456
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "C:\\Netdata\\checks\\check_service.ps1"]
457
timeout: 10s
458
check_interval: 1m
436
- - name: Interpreter-launched script (Bash)
437
- description: |
438
- Run a Bash script through an interpreter. Without `check_name`, charts would be grouped under `bash`.
439
- config: |
440
- jobs:
441
- - name: db_health
442
- check_name: check_postgres
443
- plugin: /bin/bash
444
- args: ["/opt/netdata/checks/check_postgres.sh", "--host", "db.example.com"]
445
- timeout: 10s
446
- check_interval: 5m
459
- name: Remote check via NRPE
460
description: |
461
Run a check on a remote host using `check_nrpe`. This works exactly like a Nagios NRPE configuration — install `nagios-nrpe-plugin` and point to the remote NRPE agent. Increase `timeout` if the remote host is slow to respond.
@@ -520,7 +532,7 @@ modules:
532
The Netdata Agent runs as the `netdata` user. If a check needs to read protected files, access SNMP, or connect to local sockets, it must be accessible to the `netdata` user. Test as that user first: `sudo -u netdata /path/to/check`. Common fixes include adding the `netdata` user to the required system group or using `sudo` with a specific NOPASSWD rule for the check command.
533
- name: Windows checks need an executable entry point
534
description: |
523
- The collector runs the command named in `plugin` directly. On Windows, point `plugin` to the absolute path of a compiled executable or an interpreter (e.g. `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`). When using an interpreter, pass the script path in `args` and always set `check_name` — without it, all jobs using the same interpreter share the same chart section (e.g. `Perfdata/powershell`).
535
+ The collector runs the command named in `plugin` directly. On Windows, `.ps1` scripts cannot be executed directly — point `plugin` to the absolute path of `powershell.exe` and pass the script in `args`. Always set `check_name` when using an interpreter so charts are grouped under the script identity instead of `powershell`. Ensure scripts are stored in directories with appropriate ACLs.
536
alerts:
537
- name: nagios_job_execution_state_warn
538
metric: nagios.job.execution_state
src/go/plugin/scripts.d/collector/nagios/v2_gate_test.go
+1
-1
@@ -433,7 +433,7 @@ func assertPlanHasNoRemoveForTarget(t *testing.T, plan chartengine.Plan, removeM
433
}
434
435
func TestV2Gate_SmokeCollect(t *testing.T) {
436
- coll := New()
436
+ coll := newTestCollector()
437
coll.runner = &fakeRunner{}
438
coll.Config.JobConfig.Plugin = writeTestPluginFile(t, "true")
439
coll.Config.JobConfig.Name = "smoke"
src/go/plugin/scripts.d/config/scripts.d/nagios.conf
-9
@@ -17,12 +17,3 @@
17
# - type: weekly
18
# days: [sunday, monday, tuesday, wednesday, thursday, friday, saturday]
19
# ranges: ["00:00-24:00"]
20
-#
21
-# # When plugin is an interpreter, set check_name to identify the actual check.
22
-# # Without it, charts are grouped under the interpreter name (e.g. "powershell").
23
-# - name: service_health_win
24
-# check_name: check_service
25
-# plugin: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
26
-# args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "C:\\Netdata\\checks\\check_service.ps1"]
27
-# timeout: 10s
28
-# check_interval: 1m