Node.js

From Gentoo Wiki
Jump to:navigation Jump to:search
This article is a stub. Please help out by expanding it - how to get started.

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

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
icu Enable ICU (Internationalization Components for Unicode) support, using dev-libs/icu
inspector Enable V8 inspector
lto Enable Link-Time Optimization (LTO) to optimize the build
npm Enable NPM package manager
pax-kernel Enable building under a PaX enabled kernel
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
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.

Standalone Node.js server

Warning
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

Warning
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.

FILE 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;
Important
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:

KERNEL Enable redirections
[*] Networking support  --->
  --- Networking support
    Networking options  --->
      [*] Network packet filtering framework (Netfilter)  --->
        --- Network packet filtering framework (Netfilter)
          Core Netfilter Configuration  --->
            [*] REDIRECT target support
Important
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.

Note
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:

Important
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:

KERNEL Enable redirections
[*] 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
KERNEL IPv6
[*] Networking support  --->
  --- Networking support
    Networking options  --->
      [*] Network packet filtering framework (Netfilter)  --->
        --- Network packet filtering framework (Netfilter)
          IPv6: Netfilter Configuration  --->
            [*] IPv6 nf_tables support
KERNEL IPv4
[*] Networking support  --->
  --- Networking support
    Networking options  --->
      [*] Network packet filtering framework (Netfilter)  --->
        --- Network packet filtering framework (Netfilter)
          IP: Netfilter Configuration  --->
            [*] IPv4 nf_tables support
Important
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.

Note
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:

Important
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:

FILE 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:

FILE 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:

FILE <Forgejo\Gitea root directory>/custom/conf/app.ini
[server]
ROOT_URL = http://<DOMAIN GOES HERE>/projects/
HTTP_PORT = 3001
Note
Replace http:// with https:// if HTTPS is used.

The minimal HTTP server:

FILE 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:

CODE
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

FILE /etc/monit.d/my-appAuto restart NodeJS App
check 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

FILE /etc/nginx/nginx.confNginx Config
http {
    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

FILE /etc/init.d/nodejs-serverSample 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
}

References