Node.js
Node.js is a cross platform, open source, JavaScript server environment.
Installation
USE flags
USE flags for net-libs/nodejs A JavaScript runtime built on Chrome's V8 JavaScript engine
+icu
|
Enable ICU (Internationalization Components for Unicode) support, using dev-libs/icu |
+npm
|
Enable NPM package manager |
+snapshot
|
Enable snapshot creation for faster startup |
+ssl
|
Add support for SSL/TLS connections (Secure Socket Layer / Transport Layer Security) |
+system-icu
|
Use system dev-libs/icu instead of the bundled version |
+system-ssl
|
Use system OpenSSL instead of the bundled one |
corepack
|
Enable the experimental corepack package management tool |
debug
|
Enable extra debug codepaths, like asserts and extra output. If you want to get meaningful backtraces see https://wiki.gentoo.org/wiki/Project:Quality_Assurance/Backtraces |
doc
|
Add extra documentation (API, Javadoc, etc). It is recommended to enable per package instead of globally |
inspector
|
Enable V8 inspector |
lto
|
Enable Link-Time Optimization (LTO) to optimize the build |
pax-kernel
|
Enable building under a PaX enabled kernel |
systemtap
|
Enable SystemTap/DTrace tracing |
test
|
Enable dependencies and/or preparations necessary to run tests (usually controlled by FEATURES=test but can be toggled independently) |
npm
Node.js has a USE flag to include npm, the Node.js package manager. npm is necessary to install a Node.js application's dependencies, which are defined in a file named package.json. The USE can be disabled if npm is not necessary locally, or prefer to only install an alternative, for example, sys-apps/yarn.
Don't let anything except Portage install stuff to /usr - for safe coliving for Portage:
~/.config/bash/bashrc
export NPM_CONFIG_PREFIX=$HOME/.local/
export PATH="/home/$USER/go/bin:/home/$USER/.local/bin:$NPM_CONFIG_PREFIX/bin:$PATH"
# We have this against messing with Portage files.
# Bonus: now you can `npm install -g` without root.
# According to
# https://wiki.g.o/wiki/Node.js#npm
# https://stackoverflow.com/a/63026107/1879101
# https://www.reddit.com/r/Gentoo/comments/ydzkml/nodejs_is_it_ok_to_install_global_packages/
Standalone Node.js server
Node.js and Express.js suggest to run Node.js behind a reverse proxy to mitigate DoS attacks and improve performance. [1][2][3] This section goes against these recommendations and even includes an example of using Node.js itself as a reverse proxy. Not following these recommendations is allowed if the server will not be public. With caution, the methods provided here can be used for small homelab projects. It is also important to understand that reverse proxies are not a panacea. They may contain vulnerabilities that could lead to the execution of arbitrary code (e.g. bug #CVE-2021-23017) or root privilege escalation (e.g. bug #CVE-2016-1247). Not using reverse proxies limits the attack vector to Node.js only.
Node.js can be run as a standalone HTTP server. It does not require root privileges and can be accessed from the Internet, for example on port 3000.
To launch the official example, the hostname
(a variable in the example) must be set to a public IPv6 or IPv4 address (localhost
will not work). The modified example can be executed from the user space as following:
user $
node modified-downloaded-example.js
The only problem is the inability to connect to well-known ports (e.g. 80) from unprivileged user space. But this problem can be solved with port redirection.
SELinux policy
The policy provided in this section is created using
audit2allow
. Review the policy before using it.As of March 26, 2024, the Node.js package does not come with a SELinux policy, so creating a custom policy is required. The following custom policy assumes that Node.js will be executed from unprivileged user space. The policy was tested with Nodejs v. 20.6.1 on the default/linux/arm64/17.0/musl/hardened/selinux
profile.
nodejs-workaround.te
module nodejs-workaround 1.0;
require {
type unreserved_port_t;
type ntop_port_t;
type node_t;
type user_t;
class process execmem;
class tcp_socket { name_bind node_bind };
}
#============= user_t ==============
#!!!! This avc can be allowed using the boolean 'user_tcp_server'
allow user_t node_t:tcp_socket node_bind;
allow user_t ntop_port_t:tcp_socket name_bind;
#!!!! This avc can be allowed using the boolean 'allow_execmem'
allow user_t self:process execmem;
allow user_t unreserved_port_t:tcp_socket name_bind;
Delete the following lines if there are no plans to use Node.js on ports other than
3000
:
type unreserved_port_t;
and
allow user_t unreserved_port_t:tcp_socket name_bind;
To compile and install the policy module, run the commands:
root #
make -f /usr/share/selinux/strict/include/Makefile nodejs-workaround.pp
root #
semodule --install nodejs-workaround.pp
Node.js should immediately become functional.
To remove the policy, run the command:
root #
semodule --remove nodejs-workaround
The policy is created based on the following AVC:
root #
cat /var/log/audit/audit.log
audit: type=1400 audit(1710592438.060:241): avc: denied { execmem } for pid=1795 comm="node" scontext=user_u:user_r:user_t tcontext=user_u:user_r:user_t tclass=process permissive=0 audit: type=1400 audit(1710592130.148:234): avc: denied { name_bind } for pid=1705 comm="node" src=3005 scontext=user_u:user_r:user_t tcontext=system_u:object_r:unreserved_port_t tclass=tcp_socket permissive=0 audit: type=1400 audit(1710592738.512:259): avc: denied { node_bind } for pid=1877 comm="node" saddr=7777:777:7777:7777::1 src=3004 scontext=user_u:user_r:user_t tcontext=system_u:object_r:node_t tclass=tcp_socket permissive=0 audit: type=1400 audit(1710592851.496:274): avc: denied { name_bind } for pid=1920 comm="node" src=3000 scontext=user_u:user_r:user_t tcontext=system_u:object_r:ntop_port_t tclass=tcp_socket permissive=0
Other AVC messages caused by Node.js are also present (and will be present after policy definition) in the log. But they do not affect the use of Node.js. The above messages are the only ones that cause Node.js to fail.
Port redirection
This section describes a way to redirect ports using the legacy iptables approach or the modern nftables approach. Choose one.
iptables
The redirection requires the following option to be enabled in the kernel:
[*] Networking support --->
--- Networking support
Networking options --->
[*] Network packet filtering framework (Netfilter) --->
--- Network packet filtering framework (Netfilter)
Core Netfilter Configuration --->
[*] REDIRECT target support
The examples below assume that the server is running on a public IPv6 address. For an IPv4 address use
iptables
instead of ip6tables
, the syntax is the same. IPv4 and IPv6 have separate NAT tables.Assuming the Node.js server is running on port 3000, run the following command to redirect port 80 to 3000:
root #
ip6tables --table nat --append PREROUTING --protocol tcp --dport 80 --jump REDIRECT --to-port 3000
The server should be immediately accessible via port 80.
The created rule will disappear after a reboot.
To see the modified NAT table, run the command:
root #
ip6tables --table nat --list
Chain PREROUTING (policy ACCEPT) target prot opt source destination REDIRECT tcp -- anywhere anywhere tcp dpt:http redir ports 3000 Chain INPUT (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain POSTROUTING (policy ACCEPT) target prot opt source destination
To remove the added rule (to change the port or because of a mistake), run the command:
Make sure the rule is the first (
1
) in the PREROUTING
chain, otherwise specify the correct number.root #
ip6tables --table nat --delete PREROUTING 1
nftables
The redirection requires the following options to be enabled in the kernel:
[*] Networking support --->
--- Networking support
Networking options --->
[*] Network packet filtering framework (Netfilter) --->
--- Network packet filtering framework (Netfilter)
Core Netfilter Configuration --->
[*] Netfilter nf_tables support
[*] Netfilter nf_tables redirect support
[*] Netfilter nf_tables nat module
[*] Networking support --->
--- Networking support
Networking options --->
[*] Network packet filtering framework (Netfilter) --->
--- Network packet filtering framework (Netfilter)
IPv6: Netfilter Configuration --->
[*] IPv6 nf_tables support
[*] Networking support --->
--- Networking support
Networking options --->
[*] Network packet filtering framework (Netfilter) --->
--- Network packet filtering framework (Netfilter)
IP: Netfilter Configuration --->
[*] IPv4 nf_tables support
The examples below assume that the server is running on a public IPv6 address. For an IPv4 address use
inet
or ip
instead of ip6
Create the NAT table and chain:
root #
nft add table ip6 nat
root #
nft add chain ip6 nat prerouting '{ type nat hook prerouting priority 0; }'
Redirect port 80
to port 3000
:
root #
nft add rule ip6 nat prerouting tcp dport 80 counter redirect to 3000
The server should be immediately accessible via port 80.
The created rule will disappear after a reboot.
To see the prerouting chain, run the command:
root #
nft --handle list chain ip6 nat prerouting
table ip6 nat { chain prerouting { # handle 1 type nat hook prerouting priority filter; policy accept; tcp dport 80 counter packets 0 bytes 0 redirect to :3000 # handle 2 } }
To remove the added rule (to change the port or because of a mistake), run the command:
Make sure the rule is matched as
# handle 2
in the above output, otherwise specify the correct number.root #
nft delete rule ip6 nat prerouting handle 2
HTTPS
This section relies on Express.js because it provides a simple way to host static files that appear dynamically. All paths match the acme-tiny configuration guide, but there are no strict requirements, the files can be anywhere.
Certificate issuance (Let's Encrypt)
First, it is necessary to create and run a server script that will host the Let's Encrypt token for the HTTP-01 challenge:
http-server.js
const express = require('express');
const PORT = 3000;
const ACME_CHALLENGE_PATH = '/var/www/localhost/acme-challenge';
const app = express();
app.use('/.well-known/acme-challenge', express.static(ACME_CHALLENGE_PATH));
app.listen(PORT);
Then redirect port 80
to port 3000
as described above. Install acme-tiny as described here and issue the certificate as described here.
Certificate usage
Once the certificate has been issued, the server script needs to be replaced with this one:
https-server.js
const fs = require('node:fs');
const https = require('node:https');
const express = require('express');
const PORT = 3000;
const CERTIFICATE_PATH = '/var/lib/letsencrypt/chained.pem';
const PRIVATE_KEY_PATH = '/var/lib/letsencrypt/domain.key';
const app = express();
const options = {
cert: fs.readFileSync(CERTIFICATE_PATH),
key: fs.readFileSync(PRIVATE_KEY_PATH)
};
https.createServer(options, app).listen(PORT);
Redirect port 443
to 3000
as described above. The connection should now be encrypted. The above script doesn't actually require Express.js anymore, but it's left as an example, an example of pure Node.js can be found here. The ACME challenge is not required either, even for renewals.
Node.js as a reverse proxy for Forgejo\Gitea (or anything else)
The simplest way to set up a reverse proxy is to use Express.js with express-http-proxy.
The following example shows a way to redirect all requests coming to http://<DOMAIN>/projects
to Forgejo (or Gitea). The example assumes that port 80
is redirected to port 3000
as described above.
The minimal Forgejo configuration:
<Forgejo\Gitea root directory>/custom/conf/app.ini
[server]
ROOT_URL = http://<DOMAIN GOES HERE>/projects/
HTTP_PORT = 3001
Replace
http://
with https://
if HTTPS is used.The minimal HTTP server:
server.js
const express = require('express');
const proxy = require('express-http-proxy');
const PORT = 3000;
const app = express();
app.use('/projects', proxy('localhost:3001'));
app.listen(PORT);
To use HTTPS, just inject the following lines in the script provided here:
const proxy = require('express-http-proxy');
app.use('/projects', proxy('localhost:3001'));
Web application daemons with nginx and monit
This section will walk through installing Node.js behind nginx and using Monit to keep Node instances alive. Since Node.js is a single-process application, the goal is to launch multiple instances of the application and load balance using nginx.
Packages
Use app-admin/monit for spawning Node.js servers.
root #
emerge --ask monit nginx nodejs
Configure Monit
/etc/monit.d/my-app
Auto restart NodeJS Appcheck process mysql with pidfile /var/run/my-app/mysqld.pid
start program = "/bin/bash -c 'rc-service mysql start'"
stop program = "/bin/bash -c 'rc-service mysql stop'"
Configure Nginx
/etc/nginx/nginx.conf
Nginx Confighttp {
upstream myapp1 {
least_conn;
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://myapp1;
}
}
}
Web application with openrc runscript
/etc/init.d/nodejs-server
Sample init.d file for a Node.js daemon#!/sbin/openrc-run
user="nobody"
group="nobody"
command="/usr/bin/node"
directory="/opt/${RC_SVCNAME}"
command_args="httpd.js"
command_user="${user}:${group}"
command_background="yes"
pidfile="/run/${RC_SVCNAME}.pid"
output_log="/var/log/${RC_SVCNAME}.log"
error_log="${output_log}"
depend() {
use net
}