Full stack JavaScript developer.
Independent contractor.
Node.js is famous for the abundance of 3rd-party modules that can be added to any project in a matter of seconds.
The ease of expanding program capabilities helps to develop complex software faster. However, the more modules you add, the more trust you put in code written by someone else. Each module has its own dependencies, which in turn have their dependencies, and so on. As a result, even fairly simple projects can easily contain thousands of lines of code written by hundreds of other developers.
While it is unavoidable to give some trust to others when it comes to using external libraries, it does not feel safe to rely on hundreds of people to ensure that their modules are impossible to compromise.
This post describes a way to configure Linux environment to make it safer to develop Node.js projects.
In production environment it is relatively easy to secure a Node.js process. Running it under a non-privileged user or virtualizing the whole environment are common techniques.
In development environment it gets a bit trickier. For the sake of convenience, it often makes sense to run the process on the same computer where developer runs his IDE, browser, and other tools. When application is started via plain “node index.js”, the process (and all packages used in the project) has access to user files.
Similar to how it’s done in production, a separate user with restricted access can be introduced into the development environment. The solution is viable, but not without drawbacks:
Fortunately, there is another way to restrict what a program can do. Ubuntu comes preinstalled with a module called AppArmor, and its purpose is precisely what we need: confining programs.
To restrict a program with AppArmor, a profile must be created, then the AppArmor service configuration must be reloaded.
First, create a new file in /etc/apparmor.d directory. The convention is to use file path with "/" replaced with ".", but any name can be used, especially if there are multiple Node.js versions in the system:
$ sudo nano /etc/apparmor.d/node
The file should have the following structure:
PATH_TO_PROGRAM {
RULE,
RULE,
}
Each rule must be terminated with a comma (yes, even the last one). AppArmor denies any action not explicitly permitted by rules, so you have to list everything that a program should be able to do.
Below is the profile that works for my environment.
# Variables
#include <tunables/global>
/opt/nvs/** {
# Access to basic OS functions
#include <abstractions/base>
# Communicate with terminals
#include <abstractions/consoles>
# Work with history in CLI mode
/home/*/.node_repl_history rw,
# Access network
network,
# Read DNS resolver parameters
/run/systemd/resolve/stub-resolv.conf r,
# Read procfs
/proc/** r,
# Read OS configuration
/etc/** r,
# Execute OS binaries
/bin/** ix,
/usr/bin/** ix,
# Work with npm and yarn
/usr/share/yarn/** rix,
/home/*/.npm/** rwl,
/home/*/.yarn/** rwl,
/home/*/.cache/yarn/ rwl,
/home/*/.cache/yarn/** rwl,
/home/*/.npmrc rw,
/home/*/.yarnrc rw,
# Work with tmp files
/tmp/ rw,
/tmp/** rw,
# Work with NVS
/opt/nvs/ r,
/opt/nvs/** rwixlm,
# Do anything with projects
/srv/node/** rwixlkm,
# Work with module configuration stored in user directory
/home/*/.config/configstore/ rw,
/home/*/.config/configstore/** rw,
# Read WebStorm plugin configuration
/home/*/.local/share/JetBrains/Toolbox/apps/WebStorm/ch-0/*/plugins/** r,
# Enable Node.js assist in WebStorm
/home/**/node-typings/ rwixlkm,
/home/**/node-typings/** rwixlkm,
}
I use NVS to run multiple Node.js versions. Normally, the tool is installed in ~/.nvs, but the point was to forbid Node.js to read the home directory, so it went into /opt/nvs. AppArmor supports wildcards, so “/opt/nvs/**” allowed to assign all existing and future node/npm/global module binaries the same rules.
Reload the service to apply the profile:
$ sudo systemctl reload apparmor
Check service status:
$ sudo aa-status
/opt/nvs/** should be listed under “profiles in enforce mode”.
Verify that Node.js can only do what’s allowed:
$ node
require('fs').readdirSync('/home');
This should produce an error:
{ Error: EACCES: permission denied, scandir '/home'
at Object.readdirSync (fs.js:785:3) errno: -13, syscall: 'scandir', code: 'EACCES', path: '/home' }
Check syslog for details (it’s quite helpful when creating the profile):
$ tail -f /var/log/syslog
DATE COMPUTER_NAME kernel: [888192.041639] audit: type=1400 audit(1555433516.029:9761): apparmor="DENIED"
operation="open" profile="/opt/nvs/**" name="/home/" pid=6845 comm="node" requested_mask="r" denied_mask="r"
fsuid=1000 ouid=0
Securing Node.js with AppArmor is a relatively simple solution that allows to develop applications without giving 3rd-party code unrestricted access to private files.
I hope this brief article was helpful.