ndsudo - a helper to run privileged commands (#16614)
* ndsudo command * added help * make ndsudo setuid to root * fix megacli binary name on FreeBSD * move ndsudo to collectors/plugins.d/ * address PR comments * do not print the command line argument, instead print its index --------- Co-authored-by: Ilya Mashchenko <ilya@netdata.cloud>
Costa Tsaousis committed
Dec 15, 2023 at 16:09 UTC
0c8b46cbfd05109a45ee4de27f034567569fa3fa
7 files changed
+325
-2
CMakeLists.txt
+8
@@ -1478,6 +1478,14 @@ if(ENABLE_PLUGIN_CUPS)
1478
endif()
1479
endif()
1480
1481
+set(NDSUDO_FILES collectors/plugins.d/ndsudo.c)
1482
+
1483
+add_executable(ndsudo ${NDSUDO_FILES})
1484
+
1485
+install(TARGETS ndsudo
1486
+ COMPONENT ndsudo
1487
+ DESTINATION usr/libexec/netdata/plugins.d)
1488
+
1489
if(ENABLE_PLUGIN_CGROUP_NETWORK)
1490
set(CGROUP_NETWORK_FILES collectors/cgroups.plugin/cgroup-network.c)
1491
collectors/plugins.d/ndsudo.c
new
+305
@@ -0,0 +1,305 @@
1
+#include <stdio.h>
2
+#include <stdlib.h>
3
+#include <string.h>
4
+#include <unistd.h>
5
+#include <stdbool.h>
6
+
7
+#define MAX_SEARCH 2
8
+#define MAX_PARAMETERS 128
9
+#define ERROR_BUFFER_SIZE 1024
10
+
11
+struct command {
12
+ const char *name;
13
+ const char *params;
14
+ const char *search[MAX_SEARCH];
15
+} allowed_commands[] = {
16
+ {
17
+ .name = "nvme-list",
18
+ .params = "list --output-format=json",
19
+ .search = {
20
+ [0] = "nvme",
21
+ [1] = NULL,
22
+ },
23
+ },
24
+ {
25
+ .name = "nvme-smart-log",
26
+ .params = "smart-log {{device}} --output-format=json",
27
+ .search = {
28
+ [0] = "nvme",
29
+ [1] = NULL,
30
+ },
31
+ },
32
+ {
33
+ .name = "megacli-disk-info",
34
+ .params = "-LDPDInfo -aAll -NoLog",
35
+ .search = {
36
+ [0] = "megacli",
37
+ [1] = "MegaCli",
38
+ },
39
+ },
40
+ {
41
+ .name = "megacli-battery-info",
42
+ .params = "-AdpBbuCmd -aAll -NoLog",
43
+ .search = {
44
+ [0] = "megacli",
45
+ [1] = "MegaCli",
46
+ },
47
+ },
48
+ {
49
+ .name = "arcconf-ld-info",
50
+ .params = "GETCONFIG 1 LD",
51
+ .search = {
52
+ [0] = "arcconf",
53
+ [1] = NULL,
54
+ },
55
+ },
56
+ {
57
+ .name = "arcconf-pd-info",
58
+ .params = "GETCONFIG 1 PD",
59
+ .search = {
60
+ [0] = "arcconf",
61
+ [1] = NULL,
62
+ },
63
+ }
64
+};
65
+
66
+bool command_exists_in_dir(const char *dir, const char *cmd, char *dst, size_t dst_size) {
67
+ snprintf(dst, dst_size, "%s/%s", dir, cmd);
68
+ return access(dst, X_OK) == 0;
69
+}
70
+
71
+bool command_exists_in_PATH(const char *cmd, char *dst, size_t dst_size) {
72
+ if(!dst || !dst_size)
73
+ return false;
74
+
75
+ char *path = getenv("PATH");
76
+ if(!path)
77
+ return false;
78
+
79
+ char *path_copy = strdup(path);
80
+ if (!path_copy)
81
+ return false;
82
+
83
+ char *dir;
84
+ bool found = false;
85
+ dir = strtok(path_copy, ":");
86
+ while(dir && !found) {
87
+ found = command_exists_in_dir(dir, cmd, dst, dst_size);
88
+ dir = strtok(NULL, ":");
89
+ }
90
+
91
+ free(path_copy);
92
+ return found;
93
+}
94
+
95
+struct command *find_command(const char *cmd) {
96
+ size_t size = sizeof(allowed_commands) / sizeof(allowed_commands[0]);
97
+ for(size_t i = 0; i < size ;i++) {
98
+ if(strcmp(cmd, allowed_commands[i].name) == 0)
99
+ return &allowed_commands[i];
100
+ }
101
+
102
+ return NULL;
103
+}
104
+
105
+bool check_string(const char *str, size_t index, char *err, size_t err_size) {
106
+ const char *s = str;
107
+ while(*s) {
108
+ char c = *s++;
109
+ if(!((c >= 'A' && c <= 'Z') ||
110
+ (c >= 'a' && c <= 'z') ||
111
+ (c >= '0' && c <= '9') ||
112
+ c == ' ' || c == '_' || c == '-' || c == '/' || c == '.')) {
113
+ snprintf(err, err_size, "command line argument No %zu includes invalid character '%c'", index, c);
114
+ return false;
115
+ }
116
+ }
117
+
118
+ return true;
119
+}
120
+
121
+bool check_params(int argc, char **argv, char *err, size_t err_size) {
122
+ for(int i = 0 ; i < argc ;i++)
123
+ if(!check_string(argv[i], i, err, err_size))
124
+ return false;
125
+
126
+ return true;
127
+}
128
+
129
+char *find_variable_in_argv(const char *variable, int argc, char **argv, char *err, size_t err_size) {
130
+ for (int i = 1; i < argc - 1; i++) {
131
+ if (strcmp(argv[i], variable) == 0)
132
+ return strdup(argv[i + 1]);
133
+ }
134
+
135
+ snprintf(err, err_size, "variable '%s' is required, but was not provided in the command line parameters", variable);
136
+
137
+ return NULL;
138
+}
139
+
140
+bool search_and_replace_params(struct command *cmd, char **params, size_t max_params, const char *filename, int argc, char **argv, char *err, size_t err_size) {
141
+ if (!cmd || !params || !max_params) {
142
+ snprintf(err, err_size, "search_and_replace_params() internal error");
143
+ return false;
144
+ }
145
+
146
+ const char *delim = " ";
147
+ char *token;
148
+ char *temp_params = strdup(cmd->params);
149
+ if (!temp_params) {
150
+ snprintf(err, err_size, "search_and_replace_params() cannot allocate memory");
151
+ return false;
152
+ }
153
+
154
+ size_t param_count = 0;
155
+ params[param_count++] = strdup(filename);
156
+
157
+ token = strtok(temp_params, delim);
158
+ while (token && param_count < max_params - 1) {
159
+ size_t len = strlen(token);
160
+
161
+ char *value = NULL;
162
+
163
+ if (strncmp(token, "{{", 2) == 0 && strncmp(token + len - 2, "}}", 2) == 0) {
164
+ token[0] = '-';
165
+ token[1] = '-';
166
+ token[len - 2] = '\0';
167
+
168
+ value = find_variable_in_argv(token, argc, argv, err, err_size);
169
+ }
170
+ else
171
+ value = strdup(token);
172
+
173
+ if(!value)
174
+ goto cleanup;
175
+
176
+ params[param_count++] = value;
177
+ token = strtok(NULL, delim);
178
+ }
179
+
180
+ params[param_count] = NULL; // Null-terminate the params array
181
+ free(temp_params);
182
+ return true;
183
+
184
+cleanup:
185
+ if(!err[0])
186
+ snprintf(err, err_size, "memory allocation failure");
187
+
188
+ free(temp_params);
189
+ for (size_t i = 0; i < param_count; ++i) {
190
+ free(params[i]);
191
+ params[i] = NULL;
192
+ }
193
+ return false;
194
+}
195
+
196
+void show_help() {
197
+ fprintf(stdout, "\n");
198
+ fprintf(stdout, "ndsudo\n");
199
+ fprintf(stdout, "\n");
200
+ fprintf(stdout, "(C) Netdata Inc.\n");
201
+ fprintf(stdout, "\n");
202
+ fprintf(stdout, "A helper to allow Netdata run privileged commands.\n");
203
+ fprintf(stdout, "\n");
204
+ fprintf(stdout, " --test\n");
205
+ fprintf(stdout, " print the generated command that will be run, without running it.\n");
206
+ fprintf(stdout, "\n");
207
+ fprintf(stdout, " --help\n");
208
+ fprintf(stdout, " print this message.\n");
209
+ fprintf(stdout, "\n");
210
+
211
+ fprintf(stdout, "The following commands are supported:\n\n");
212
+
213
+ size_t size = sizeof(allowed_commands) / sizeof(allowed_commands[0]);
214
+ for(size_t i = 0; i < size ;i++) {
215
+ fprintf(stdout, "- Command : %s\n", allowed_commands[i].name);
216
+ fprintf(stdout, " Executables: ");
217
+ for(size_t j = 0; j < MAX_SEARCH && allowed_commands[i].search[j] ;j++) {
218
+ fprintf(stdout, "%s ", allowed_commands[i].search[j]);
219
+ }
220
+ fprintf(stdout, "\n");
221
+ fprintf(stdout, " Parameters : %s\n\n", allowed_commands[i].params);
222
+ }
223
+
224
+ fprintf(stdout, "The program searches for executables in the system path.\n");
225
+ fprintf(stdout, "\n");
226
+ fprintf(stdout, "Variables given as {{variable}} are expected on the command line as:\n");
227
+ fprintf(stdout, " --variable VALUE\n");
228
+ fprintf(stdout, "\n");
229
+ fprintf(stdout, "VALUE can include space, A-Z, a-z, 0-9, _, -, /, and .\n");
230
+ fprintf(stdout, "\n");
231
+}
232
+
233
+int main(int argc, char *argv[]) {
234
+ char error_buffer[ERROR_BUFFER_SIZE] = "";
235
+
236
+ if (argc < 2) {
237
+ fprintf(stderr, "at least 2 parameters are needed, but %d were given.\n", argc);
238
+ return 1;
239
+ }
240
+
241
+ if(!check_params(argc, argv, error_buffer, sizeof(error_buffer))) {
242
+ fprintf(stderr, "invalid characters in parameters: %s\n", error_buffer);
243
+ return 2;
244
+ }
245
+
246
+ bool test = false;
247
+ const char *cmd = argv[1];
248
+ if(strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) {
249
+ show_help();
250
+ exit(0);
251
+ }
252
+ else if(strcmp(cmd, "--test") == 0) {
253
+ cmd = argv[2];
254
+ test = true;
255
+ }
256
+
257
+ struct command *command = find_command(cmd);
258
+ if(!command) {
259
+ fprintf(stderr, "command not recognized: %s\n", cmd);
260
+ return 3;
261
+ }
262
+
263
+ bool found = false;
264
+ char filename[FILENAME_MAX];
265
+
266
+ for(size_t i = 0; i < MAX_SEARCH && !found ;i++) {
267
+ if(command->search[i]) {
268
+ found = command_exists_in_PATH(command->search[i], filename, sizeof(filename));
269
+ if(!found) {
270
+ size_t len = strlen(error_buffer);
271
+ snprintf(&error_buffer[len], sizeof(error_buffer) - len, "%s ", command->search[i]);
272
+ }
273
+ }
274
+ }
275
+
276
+ if(!found) {
277
+ fprintf(stderr, "%s: not available in PATH.\n", error_buffer);
278
+ return 4;
279
+ }
280
+ else
281
+ error_buffer[0] = '\0';
282
+
283
+ char *params[MAX_PARAMETERS];
284
+ if(!search_and_replace_params(command, params, MAX_PARAMETERS, filename, argc, argv, error_buffer, sizeof(error_buffer))) {
285
+ fprintf(stderr, "command line parameters are not satisfied: %s\n", error_buffer);
286
+ return 5;
287
+ }
288
+
289
+ if(test) {
290
+ fprintf(stderr, "Command to run: \n");
291
+
292
+ for(size_t i = 0; i < MAX_PARAMETERS && params[i] ;i++)
293
+ fprintf(stderr, "'%s' ", params[i]);
294
+
295
+ fprintf(stderr, "\n");
296
+
297
+ exit(0);
298
+ }
299
+ else {
300
+ char *clean_env[] = {NULL};
301
+ execve(filename, params, clean_env);
302
+ perror("execve"); // execve only returns on error
303
+ return 6;
304
+ }
305
+}
contrib/debian/netdata.postinst
+1
@@ -41,6 +41,7 @@ case "$1" in
41
42
grep /usr/libexec/netdata /var/lib/dpkg/info/netdata.list | xargs -n 30 chown root:netdata
43
44
+ chmod 4750 /usr/libexec/netdata/plugins.d/ndsudo
45
chmod 4750 /usr/libexec/netdata/plugins.d/cgroup-network
46
chmod 4750 /usr/libexec/netdata/plugins.d/local-listeners
47
netdata-installer.sh
+5
@@ -1553,6 +1553,11 @@ if [ "$(id -u)" -eq 0 ]; then
1553
run chmod 4750 "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/local-listeners"
1554
fi
1555
1556
+ if [ -f "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo" ]; then
1557
+ run chown "root:${NETDATA_GROUP}" "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo"
1558
+ run chmod 4750 "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo"
1559
+ fi
1560
+
1561
else
1562
# non-privileged user installation
1563
run chown "${NETDATA_USER}:${NETDATA_GROUP}" "${NETDATA_LOG_DIR}"
netdata.spec.in
+3
@@ -715,6 +715,9 @@ rm -rf "${RPM_BUILD_ROOT}"
715
# local-listeners detects the local processes that are listening for connections
716
%attr(4750,root,netdata) %{_libexecdir}/%{name}/plugins.d/local-listeners
717
718
+# ndsudo a helper to run privileged commands
719
+%attr(4750,root,netdata) %{_libexecdir}/%{name}/plugins.d/ndsudo
720
+
721
# Enforce 0644 for files and 0755 for directories
722
# for the netdata web directory
723
%defattr(0644,root,root,0755)
packaging/docker/Dockerfile
+1
@@ -122,6 +122,7 @@ RUN addgroup --gid ${NETDATA_GID} --system "${DOCKER_GRP}" && \
122
freeipmi.plugin \
123
go.d.plugin \
124
perf.plugin \
125
+ ndsudo \
126
slabinfo.plugin \
127
systemd-journal.plugin; do \
128
[ -f "/usr/libexec/netdata/plugins.d/$name" ] && chmod 4755 "/usr/libexec/netdata/plugins.d/$name"; \
packaging/makeself/install-or-update.sh
+2
-2
@@ -172,7 +172,7 @@ fi
172
173
progress "changing plugins ownership and permissions"
174
175
-for x in apps.plugin perf.plugin slabinfo.plugin debugfs.plugin freeipmi.plugin ioping cgroup-network local-listeners ebpf.plugin nfacct.plugin xenstat.plugin python.d.plugin charts.d.plugin go.d.plugin ioping.plugin cgroup-network-helper.sh; do
175
+for x in ndsudo apps.plugin perf.plugin slabinfo.plugin debugfs.plugin freeipmi.plugin ioping cgroup-network local-listeners ebpf.plugin nfacct.plugin xenstat.plugin python.d.plugin charts.d.plugin go.d.plugin ioping.plugin cgroup-network-helper.sh; do
176
f="usr/libexec/netdata/plugins.d/${x}"
177
if [ -f "${f}" ]; then
178
run chown root:${NETDATA_GROUP} "${f}"
@@ -192,7 +192,7 @@ if command -v setcap >/dev/null 2>&1; then
192
193
run setcap "cap_net_admin,cap_net_raw=eip" "usr/libexec/netdata/plugins.d/go.d.plugin"
194
else
195
- for x in apps.plugin perf.plugin slabinfo.plugin debugfs.plugin; do
195
+ for x in ndsudo apps.plugin perf.plugin slabinfo.plugin debugfs.plugin; do
196
f="usr/libexec/netdata/plugins.d/${x}"
197
run chmod 4750 "${f}"
198
done