Skip to content

Commit

Permalink
ndsudo - a helper to run privileged commands (netdata#16614)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
ktsaou and ilyam8 authored Dec 15, 2023
1 parent 488dc77 commit 0c8b46c
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 2 deletions.
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1478,6 +1478,14 @@ if(ENABLE_PLUGIN_CUPS)
endif()
endif()

set(NDSUDO_FILES collectors/plugins.d/ndsudo.c)

add_executable(ndsudo ${NDSUDO_FILES})

install(TARGETS ndsudo
COMPONENT ndsudo
DESTINATION usr/libexec/netdata/plugins.d)

if(ENABLE_PLUGIN_CGROUP_NETWORK)
set(CGROUP_NETWORK_FILES collectors/cgroups.plugin/cgroup-network.c)

Expand Down
305 changes: 305 additions & 0 deletions collectors/plugins.d/ndsudo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>

#define MAX_SEARCH 2
#define MAX_PARAMETERS 128
#define ERROR_BUFFER_SIZE 1024

struct command {
const char *name;
const char *params;
const char *search[MAX_SEARCH];
} allowed_commands[] = {
{
.name = "nvme-list",
.params = "list --output-format=json",
.search = {
[0] = "nvme",
[1] = NULL,
},
},
{
.name = "nvme-smart-log",
.params = "smart-log {{device}} --output-format=json",
.search = {
[0] = "nvme",
[1] = NULL,
},
},
{
.name = "megacli-disk-info",
.params = "-LDPDInfo -aAll -NoLog",
.search = {
[0] = "megacli",
[1] = "MegaCli",
},
},
{
.name = "megacli-battery-info",
.params = "-AdpBbuCmd -aAll -NoLog",
.search = {
[0] = "megacli",
[1] = "MegaCli",
},
},
{
.name = "arcconf-ld-info",
.params = "GETCONFIG 1 LD",
.search = {
[0] = "arcconf",
[1] = NULL,
},
},
{
.name = "arcconf-pd-info",
.params = "GETCONFIG 1 PD",
.search = {
[0] = "arcconf",
[1] = NULL,
},
}
};

bool command_exists_in_dir(const char *dir, const char *cmd, char *dst, size_t dst_size) {
snprintf(dst, dst_size, "%s/%s", dir, cmd);
return access(dst, X_OK) == 0;
}

bool command_exists_in_PATH(const char *cmd, char *dst, size_t dst_size) {
if(!dst || !dst_size)
return false;

char *path = getenv("PATH");
if(!path)
return false;

char *path_copy = strdup(path);
if (!path_copy)
return false;

char *dir;
bool found = false;
dir = strtok(path_copy, ":");
while(dir && !found) {
found = command_exists_in_dir(dir, cmd, dst, dst_size);
dir = strtok(NULL, ":");
}

free(path_copy);
return found;
}

struct command *find_command(const char *cmd) {
size_t size = sizeof(allowed_commands) / sizeof(allowed_commands[0]);
for(size_t i = 0; i < size ;i++) {
if(strcmp(cmd, allowed_commands[i].name) == 0)
return &allowed_commands[i];
}

return NULL;
}

bool check_string(const char *str, size_t index, char *err, size_t err_size) {
const char *s = str;
while(*s) {
char c = *s++;
if(!((c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') ||
c == ' ' || c == '_' || c == '-' || c == '/' || c == '.')) {
snprintf(err, err_size, "command line argument No %zu includes invalid character '%c'", index, c);
return false;
}
}

return true;
}

bool check_params(int argc, char **argv, char *err, size_t err_size) {
for(int i = 0 ; i < argc ;i++)
if(!check_string(argv[i], i, err, err_size))
return false;

return true;
}

char *find_variable_in_argv(const char *variable, int argc, char **argv, char *err, size_t err_size) {
for (int i = 1; i < argc - 1; i++) {
if (strcmp(argv[i], variable) == 0)
return strdup(argv[i + 1]);
}

snprintf(err, err_size, "variable '%s' is required, but was not provided in the command line parameters", variable);

return NULL;
}

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) {
if (!cmd || !params || !max_params) {
snprintf(err, err_size, "search_and_replace_params() internal error");
return false;
}

const char *delim = " ";
char *token;
char *temp_params = strdup(cmd->params);
if (!temp_params) {
snprintf(err, err_size, "search_and_replace_params() cannot allocate memory");
return false;
}

size_t param_count = 0;
params[param_count++] = strdup(filename);

token = strtok(temp_params, delim);
while (token && param_count < max_params - 1) {
size_t len = strlen(token);

char *value = NULL;

if (strncmp(token, "{{", 2) == 0 && strncmp(token + len - 2, "}}", 2) == 0) {
token[0] = '-';
token[1] = '-';
token[len - 2] = '\0';

value = find_variable_in_argv(token, argc, argv, err, err_size);
}
else
value = strdup(token);

if(!value)
goto cleanup;

params[param_count++] = value;
token = strtok(NULL, delim);
}

params[param_count] = NULL; // Null-terminate the params array
free(temp_params);
return true;

cleanup:
if(!err[0])
snprintf(err, err_size, "memory allocation failure");

free(temp_params);
for (size_t i = 0; i < param_count; ++i) {
free(params[i]);
params[i] = NULL;
}
return false;
}

void show_help() {
fprintf(stdout, "\n");
fprintf(stdout, "ndsudo\n");
fprintf(stdout, "\n");
fprintf(stdout, "(C) Netdata Inc.\n");
fprintf(stdout, "\n");
fprintf(stdout, "A helper to allow Netdata run privileged commands.\n");
fprintf(stdout, "\n");
fprintf(stdout, " --test\n");
fprintf(stdout, " print the generated command that will be run, without running it.\n");
fprintf(stdout, "\n");
fprintf(stdout, " --help\n");
fprintf(stdout, " print this message.\n");
fprintf(stdout, "\n");

fprintf(stdout, "The following commands are supported:\n\n");

size_t size = sizeof(allowed_commands) / sizeof(allowed_commands[0]);
for(size_t i = 0; i < size ;i++) {
fprintf(stdout, "- Command : %s\n", allowed_commands[i].name);
fprintf(stdout, " Executables: ");
for(size_t j = 0; j < MAX_SEARCH && allowed_commands[i].search[j] ;j++) {
fprintf(stdout, "%s ", allowed_commands[i].search[j]);
}
fprintf(stdout, "\n");
fprintf(stdout, " Parameters : %s\n\n", allowed_commands[i].params);
}

fprintf(stdout, "The program searches for executables in the system path.\n");
fprintf(stdout, "\n");
fprintf(stdout, "Variables given as {{variable}} are expected on the command line as:\n");
fprintf(stdout, " --variable VALUE\n");
fprintf(stdout, "\n");
fprintf(stdout, "VALUE can include space, A-Z, a-z, 0-9, _, -, /, and .\n");
fprintf(stdout, "\n");
}

int main(int argc, char *argv[]) {
char error_buffer[ERROR_BUFFER_SIZE] = "";

if (argc < 2) {
fprintf(stderr, "at least 2 parameters are needed, but %d were given.\n", argc);
return 1;
}

if(!check_params(argc, argv, error_buffer, sizeof(error_buffer))) {
fprintf(stderr, "invalid characters in parameters: %s\n", error_buffer);
return 2;
}

bool test = false;
const char *cmd = argv[1];
if(strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) {
show_help();
exit(0);
}
else if(strcmp(cmd, "--test") == 0) {
cmd = argv[2];
test = true;
}

struct command *command = find_command(cmd);
if(!command) {
fprintf(stderr, "command not recognized: %s\n", cmd);
return 3;
}

bool found = false;
char filename[FILENAME_MAX];

for(size_t i = 0; i < MAX_SEARCH && !found ;i++) {
if(command->search[i]) {
found = command_exists_in_PATH(command->search[i], filename, sizeof(filename));
if(!found) {
size_t len = strlen(error_buffer);
snprintf(&error_buffer[len], sizeof(error_buffer) - len, "%s ", command->search[i]);
}
}
}

if(!found) {
fprintf(stderr, "%s: not available in PATH.\n", error_buffer);
return 4;
}
else
error_buffer[0] = '\0';

char *params[MAX_PARAMETERS];
if(!search_and_replace_params(command, params, MAX_PARAMETERS, filename, argc, argv, error_buffer, sizeof(error_buffer))) {
fprintf(stderr, "command line parameters are not satisfied: %s\n", error_buffer);
return 5;
}

if(test) {
fprintf(stderr, "Command to run: \n");

for(size_t i = 0; i < MAX_PARAMETERS && params[i] ;i++)
fprintf(stderr, "'%s' ", params[i]);

fprintf(stderr, "\n");

exit(0);
}
else {
char *clean_env[] = {NULL};
execve(filename, params, clean_env);
perror("execve"); // execve only returns on error
return 6;
}
}
1 change: 1 addition & 0 deletions contrib/debian/netdata.postinst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ case "$1" in

grep /usr/libexec/netdata /var/lib/dpkg/info/netdata.list | xargs -n 30 chown root:netdata

chmod 4750 /usr/libexec/netdata/plugins.d/ndsudo
chmod 4750 /usr/libexec/netdata/plugins.d/cgroup-network
chmod 4750 /usr/libexec/netdata/plugins.d/local-listeners

Expand Down
5 changes: 5 additions & 0 deletions netdata-installer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,11 @@ if [ "$(id -u)" -eq 0 ]; then
run chmod 4750 "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/local-listeners"
fi

if [ -f "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo" ]; then
run chown "root:${NETDATA_GROUP}" "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo"
run chmod 4750 "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo"
fi

else
# non-privileged user installation
run chown "${NETDATA_USER}:${NETDATA_GROUP}" "${NETDATA_LOG_DIR}"
Expand Down
3 changes: 3 additions & 0 deletions netdata.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,9 @@ rm -rf "${RPM_BUILD_ROOT}"
# local-listeners detects the local processes that are listening for connections
%attr(4750,root,netdata) %{_libexecdir}/%{name}/plugins.d/local-listeners

# ndsudo a helper to run privileged commands
%attr(4750,root,netdata) %{_libexecdir}/%{name}/plugins.d/ndsudo

# Enforce 0644 for files and 0755 for directories
# for the netdata web directory
%defattr(0644,root,root,0755)
Expand Down
1 change: 1 addition & 0 deletions packaging/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ RUN addgroup --gid ${NETDATA_GID} --system "${DOCKER_GRP}" && \
freeipmi.plugin \
go.d.plugin \
perf.plugin \
ndsudo \
slabinfo.plugin \
systemd-journal.plugin; do \
[ -f "/usr/libexec/netdata/plugins.d/$name" ] && chmod 4755 "/usr/libexec/netdata/plugins.d/$name"; \
Expand Down
4 changes: 2 additions & 2 deletions packaging/makeself/install-or-update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ fi

progress "changing plugins ownership and permissions"

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
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
f="usr/libexec/netdata/plugins.d/${x}"
if [ -f "${f}" ]; then
run chown root:${NETDATA_GROUP} "${f}"
Expand All @@ -192,7 +192,7 @@ if command -v setcap >/dev/null 2>&1; then

run setcap "cap_net_admin,cap_net_raw=eip" "usr/libexec/netdata/plugins.d/go.d.plugin"
else
for x in apps.plugin perf.plugin slabinfo.plugin debugfs.plugin; do
for x in ndsudo apps.plugin perf.plugin slabinfo.plugin debugfs.plugin; do
f="usr/libexec/netdata/plugins.d/${x}"
run chmod 4750 "${f}"
done
Expand Down

0 comments on commit 0c8b46c

Please sign in to comment.