5

I am building a REST API server using Node.js and Express, that will allow you to turn any Linux server into a NAS. I have several fileExec which basically spawns a shell and executes a BASH command (Like pvs, pvcreate, lvs, vgs, etc). Most of these commands require root access to execute correctly. In order to do that I must run Node.js as a root user with sudo.

I know I could possibly have a non-root user run the server and have a separate function with sudo in it that runs that specific command. However I would have to place and manage the sudo password in my server application to have correct credentials. I don't know if that would be much better. Also it would add more complexity to my application...

So my question is, is there a better way to secure my server application?

Steve Dodier-Lazaro
  • 6,798
  • 29
  • 45
InitEnabler
  • 53
  • 1
  • 4

4 Answers4

5

I would recommend splitting your app into multiple components communicating through standardized REST APIs.

In this case you'd have a storage manager app which runs as a user with enough privileges to manage LVM volumes (or if impossible, then as root) and exposes its services through a REST API the main app (which is unprivileged) can consume. That little app should listen on an UNIX socket instead of a TCP one, for both performance and security reasons (an UNIX socket won't be able to be accessed from anywhere where as TCP could be if the firewall gets misconfigured and the app listens on any address).

This approach will also allow you to swap the low-level components with something else without modifying the main app.

André Borie
  • 12,706
  • 3
  • 39
  • 76
2

Extending on André's answer, which is the one I think you should accept.

Privilege reduction

The first step is to identify the minimal privileges needed for your app's workflow. You need to manipulate virtual volumes, from what I understand. Can you configure a new UID to do that, without a login shell and without other root capabilities? Most Linux interfaces have config files for privilege granting, so check with the developers of the commands you need to use.

If you can create a separate UID

Then make one. Make your app run as that. Make it so that there is no bash and home for that UID, so if the app gets exploited there should be no area of your hard drive with permanent write privileges, as much as possible. Also, no opportunity for remote login on that UID. You can have a launcher script (using Upstart, Systemd, Init, whatever, not getting into those polemics) to run your app with this account ID.

If you have to use the root UID

Then understand there's more to root than it looks like. Firstly, a process that runs as root can give up some privileges which are considered dangerous to hold. Those are called Linux capabilities. So, write a wrapper for your app that drops all irrelevant capabilities. If you need to leave some capabilities, do your research to ensure they cannot be exploited! For instance, Taking Advantage of Linux Capabilities discusses how to re-gain a full capability set starting with only specific capabilities.

Compartmentalisation

See André's answer. Basically, have a tiny app do all the privileged operations so that (1) it's easy to review, you just need to validate the API and perform proper input domain validation; (2) there is less code shipped, thus less chances of exploitable bugs that completely surrender control of your app.

Hardening

Another weapon that was designed to protect you in this kind of scenarios is Mandatory Access Control Linux Security Modules. By default, Linux allows root to overwrite most Discretionary Access Control checks made on system calls. However, LSMs can still apply their own mandatory logic on calls. This allows LSMs to reduce root's privileges.

I could talk about AppArmor or TOMOY Linux but let's leave the elementary school toys to the toddlers. You need to look into SELinux, and actually understand it (damn. Yes, it's hard). You need to create a SELinux role, which you will apply to your process. Roles come on top of identities in SELinux to apply policies on all system calls. Roles mean that you can have a root user which is forbidden from doing virtually anything on the system. Here, you need to identify the system calls that you need (you can use audit2allow to this end) and grant the role access to those system calls.

Once you've got a list of privileges, make sure to review them, and if possible to only allow them on the appropriate resources. For instance a role may be allowed to write files on /var/www but not /var/log. Which means you'll probably need to label some files on your system with a specific type, and write your own scripts to help you maintain labels. You'll also need to create a new domain for your app, so that you can write what's called a Domain and Type Enforcement policy. The policy will dictate how your app in its domain can only interact with specific other types and domains.

To summarise, the domain-type policy will ensure your process is restrained, and the role policy will ensure your UID is restrained. There is some overlap but both are necessary because you might make a mistake (or have a legitimate need) leading to the app transiting to another, less restricted domain. The SELinux role you impose on your app will ensure that transitions can only occur towards domains of your chosing.

It is still your responsibility to ensure that all the domains allowed for your roles have low enough privileges not to permanently compromise your system. This is easily achieved if (1) you prevent any of the 'exploitable' Linux capabilities; (2) you do not have shared resources between the confined app and other apps; (and (3) you deny any form of permanent storage (see the memoryless property in A Note on the Confinement Problem), so that a compromised app does not persist).

If you can't achieve that, the odds are you can't secure your system because the app just requires too much privilege. That's when it's time to hire experts so they can check that you didn't miss something!

Steve Dodier-Lazaro
  • 6,798
  • 29
  • 45
1

Assuming no flaws in the program, you are safe running with root credentials. Unfortunately, this is not always the case, so you should never run a publicly accessible service with root credentials.

If the service is not publicly accessible (i.e. LAN only, or you are using an IP address access list to limit visitors) then you should judge the likelihood of someone being able to access your service through these means to determine whether the lower security of a root service is acceptable.

Otherwise, you need to run the listener as non-root. Depending on the hacker, and the non-root account setup, this is easily the difference between a no-damage hack and complete+covert takeover.

One option is to have a parent process running as root, and then have a child process perform the main application functions including the HTTP Listener, or openssl-based HTTPS listener. Call setuid to lower its own credentials to noaccess, nobody, webservd, or any account you create/deem appropriate for this purpose. Use stdin/stdout to communicate with the parent process.

But really, I think the simplest solution for you is to set the No Password option in your sudoers file. This should only be done for the one user account your service runs under, (i.e. webservd), and should have a specific whitelist of commands. If sudo access is granted for 'any' command, then you have defeated the purpose of running as non-root.

700 Software
  • 13,807
  • 3
  • 52
  • 82
1

It is usualy considered a bad idea to run externally accessible applications as root

The correct way to do this would be to either:

  • create a new user like node-data give it only the permissions it needs (aka permissions to the the root of the NAS file system NOT / as well as specific permissions to the executable needed)

and/or

  • create 2 node servers one externally facing with low level user access, and one with an internal only api via something like ZMQ specifically for running root access commands.
CaffeineAddiction
  • 7,517
  • 2
  • 20
  • 40