Setuid Script Wrapper Example

Written by Austin Keeley

The problem? I have a Node.js script that needs to run as root and Linux doesn’t like me to shoot myself in the foot.

I have a bunch of sysadmin utilities that I like to use to do various clean up tasks on my projects. I call them “agents” and they are just short Node.js scripts that run forever using PM2 (PM2 is one of my favorite utilities ever because it lets you wrap up all your microservices into one frontend, no matter what language you wrote them in – also you’re allowed to say microservices and not sound like a complete tool).

One of these agents was ping-agent.js, a Node script that uses net-ping and node-schedule to periodically send ping packets to a list of hosts and write the results back to a Redis cache. The problem is that doing a ping requires privileged access to the OS and using sudo put it in an entirely different PM2 daemon so it wasn’t appearing with my normal pm2 list results. Sad!

Most of the time this would be a job for setuid, but setuid doesn’t work for interpreted scripts for security reasons. One solution is to code up a quick-and-dirty wrapper in C. This is usually a Bad Idea™ since it’s a perfect way to do privilege escalation on a compromised system. My version of this is to make it as narrowly defined as possible and useable for only a specific case, while still being kind of portable when I need to re-deploy code in a different situation.

Here’s my solution:

The C preprocessor here requires that you explicitly give a path to the script to run but the Makefile figures that out for you so you don’t need to hardcode something.

make
pm2 start wrapper --name ping-agent
[PM2] Spawning PM2 daemon with pm2_home=/home/austin/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/austin/projects/ping-agent/wrapper in fork_mode (1 instance)
[PM2] Done.
┌────────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────────┐
│ App name   │ id │ mode │ pid   │ status │ restart │ uptime │ cpu │ mem       │ watching │
├────────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────────┤
│ ping-agent │ 0  │ fork │ 14568 │ online │ 0       │ 0s     │ 3%  │ 24.0 MB   │ disabled │
└────────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────────┘
 Use `pm2 show <id|name>` to get more details about an app

Woo! Now I can run my Node.js stuff in PM2 without needing a spearate PM2 daemon for root-ish activity. One big caveat is that you need to have a shebang line on your Node.js script (e.g. #!/usr/bin/env node).

Final warning: setuid is still pretty nasty. It’s probably not a good idea to use it for anything that gets input from the outside world.

Posted April 29, 2017