@cryptotaxi247 / netdata-1 / commits / 0c8b46cbf

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