Introduction
This tutorial will walk you through the steps to install a private Node.js registry using Verdaccio on a secure Fedora instance.
Using a private Node.js registry can become really handy while developing your own packages e.g. doing test-deploys. But apart from enhancing your development and deployment pipeline's security significantly, you can also utilize this approach in a production environment, where developers work with proprietary tools crafted directly for their needs by dev-ops engineers.
NOTE: All commands below assume you have root privileges, so you will have to either run the commands with the
sudo
prefix, or act in the name of root user with the command$ su root -
.
Prerequisites
- Secure Fedora cloud instance
- Nginx installed with custom configuration
- Node.js runtime installed
- Basic understanding of Node.js packages and scopes
NOTE: This tutorial builds on the setup we created in Setting Up a Secure Fedora Webserver and Nginx authentication preflight request with Node.js backend. The Nginx setup will heavily back-reference the latter.
Step 0 - Ensure Dependencies
$ sestatus # Ensure SELinux is running in 'enforcing' mode (if not, you can edit /etc/selinux/config and reboot)
$ service nginx status # Ensure nginx is running
$ dnf repolist | grep --color nodesource # Ensure we have `nodesource` repository present
$ node --version # Check Node.js version
$ npm --version # Check npm version
Basic Nginx and Node.js installation steps for a quick start
Install Nginx
dnf upgrade --refresh dnf install nginx systemctl enable nginx --now systemctl status nginx
Install Node.js
curl -sL https://rpm.nodesource.com/setup_lts.x | bash - yum install -y nodejs
Step 1 - Install Verdaccio
Verdaccio comes as an MIT licensed Node.js public package. To install Verdaccio as a global package (as preferred by the documentation), run:
$ npm install --location global verdaccio
$ npm ls --location global --depth 0 # To ensure global package is present
Step 2 - Create Systemd Service
We want the registry to start with the server and recover if something goes sideway. We will use systemd
for that. To create a systemd
service, create the file /etc/systemd/system/verdaccio.service
with this content:
[Unit]
Description=Verdaccio Node.js repository
After=network-online.target
[Service]
Type=simple
Restart=on-failure
RestartSec=5
User=nobody
ExecStart=verdaccio --config /usr/local/etc/verdaccio.yml
[Install]
WantedBy=multi-user.target
This is a simple configuration which tells systemd
, that the service needs to be started after the network is online, and we restart on failures with a 5 second delay. We also hardcoded the Verdaccio configuration path which we will create next.
NOTE: We will start the service in a later step, when all configuration is done.
Step 3 - Verdaccio Configuration
Next, we need to create the before mentioned yaml
configuration file, which systemd
has permission to read (granted by SELinux). Create the file /usr/local/etc/verdaccio.yml
with this content:
listen: localhost:4873 # Listen on localhost only
storage: /usr/tmp/verdaccio-storage # Where to store the database file
web:
enable: false # We do not want to use the web frontend for now
auth:
htpasswd:
file: /usr/local/etc/verdaccio.htpasswd # The htpasswd file location
algorithm: bcrypt # Set the cryptographic algorithm
rounds: 8
max_users: -1 # Do not allow registering users
security:
api:
jwt:
sign:
expiresIn: 15d
notBefore: 0
packages:
'@<scope>/*': # Packages with this scope will only exist locally
access: "@authenticated"
publish: "@authenticated"
unpublish: "@authenticated"
'**': {} # Any other packages will be blocked by default
In this configuration we:
- Specify that we only want to run on localhost, as we will proxy this service with Nginx later.
- Define where the registry's storage is located, noting SELinux permissions.
- Set up basic authentication.
- Restrict the registry to only accept and distribute packages from predefined Node.js scope(s). This is defined in the
packages
section.- If we'd allow all other packages (
**
) to be taken from e.g. annpmjs
uplink, all requested packages would be mirrored in the storage. This could come handy in security-critical scenarios, where only specific users / groups are allowed to bump version in the private registry from the uplink. But for this demonstration, it is just unnecessary waste of space.
- If we'd allow all other packages (
- For
access
,publish
, andunpublish
values we could also use space-delimited groups instead of the reserved@authenticated
directive.
NOTE: Here you might decide to use a 3rd party authentication plugin, or even write your own in a production scenario, depending on your needs of sophisticated user - and group management. Keep in mind, though, that the
npm
package manager does not authenticate with each call, it only verifies your token. Also,npm
recently dropped support of thealways-auth
flag and you might have to come up with a whole different token management system.
SEE: Verdaccio documentation:
Step 4 - Adding Users
Now we need to create the htpasswd
file. For that, we'll need to install httpd-tools
with the command:
$ yum install -y httpd-tools
After successful install, we can invoke the htpasswd
command to create the initial file, and to add any more users:
$ htpasswd -c -B -b -C 8 /usr/local/etc/verdaccio.htpasswd <user> <password> # Create new file with first user
$ htpasswd -B -b -C 8 /usr/local/etc/verdaccio.htpasswd <user> <password> # Add more user(s) to the existing file
Our used options:
-c
: Create a new file-B
: Use bcrypt encryption, just like our server will-b
: Use password from the command line-C
: Bcrypt algorithm computing time
SEE:
htpasswd
documentation
Step 5 - Finalizing the Registry Setup
We also need to tell npm
about our intentions to lock the @<scope>
scoped packages to our private registry running on localhost
(for the moment):
$ npm config set @<scope>:registry http://localhost:4873
$ cat ~/.npmrc
Now that we set up everything, it is time to fire up the Verdaccio server with the commands:
$ systemctl daemon-reload
$ systemctl enable --now verdaccio.service
$ service verdaccio status
$ journalctl -xeu verdaccio.service
Step 6 - Logging in
To acquire a login token, run the following command:
$ npm login --scope @<scope>
NOTE: At login time,
npm
can't decide between the repositories based on a package's scope, so we need to explicitly tell the scope to the command. Do a login for all scopes you set up!
Enter the login credentials you specified in the htpasswd
command in "Step 4" and check if the login token was successfully added:
$ cat ~/.npmrc
Step 7 - Publishing a Private Package
Create a Node.js package in your file system as described below. Be sure to give it a name starting with your predefined @<scope>/
.
-
Create a directory and the package file:
mkdir /usr/local/lib/private-test cd /usr/local/lib/private-test npm init
When it asks you for a name, remember to add your predefined
@<scope>
at the beginning. You can click on the "Example" below to view an examplepackage.json
file.Example
{ "name": "@<scope>/<private-test>", "version": "1.0.0", "description": "This is an example 'package.json' file.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }
-
Add an
index.js
file with the following content and the correct permissions:#!/usr/bin/env node console.log("@<scope>/<package-name> working");
NOTE: We need the shebang to be the first line of the application, since we aim for creating an executable.
chmod +x index.js
-
Create a lockfile:
npm install
-
Edit the newly created
/usr/local/lib/private-test/package.json
, and add the following lines under themain
entry:"bin": { "private-test-cli": "index.js" },
NOTE: This will assign a global executable with the package. So when installing it globally, we will be able to execute the
private-test-cli
command.
Next we can try a dry-run
publish in the /usr/local/lib/private-test
directory (which means it will only emulate the publishing process, but not send the actual package to the registry). If everything looks fine, we can try a real one too:
$ npm publish --dry-run
$ npm publish
NOTE: Here, npm will automatically select our private registry based on the scope of the package being published, so no need to add it as configuration.
Just ensure that the published package is present in Verdaccio's storage:
$ find /usr/tmp/verdaccio-storage | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/"
Step 8 - Installing our Private Package
Now we can try to install the locally existing package from the registry. We will add it globally, so we can test out the executable functionality as well:
$ npm install --location global @<scope>/<package-name>
$ npm ls --location global --depth 0
If we can see our @<scope>/<package-name>
listed, we can be sure, that it was installed from the local registry. Thus, we secured the @<scope>
scope.
NOTE: Here npm will automatically select the private registry based on the scope of the package to be installed, so no need to add it as configuration.
We can try out our executable as well:
$ private-test-cli # Should print '@<scope>/<package-name> working'
Step 9 - Cleanup
If everything went well, we can remove our test resources (assuming we are still in the /usr/local/lib/private-test
directory):
$ npm uninstall --location global @<scope>/<package-name>
$ npm ls --location global --depth 0
$ npm unpublish --force # Unpublishes the current working directory's package
$ find /usr/tmp/verdaccio-storage | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/"
$ cd ..
$ rm -rf private-test
Step 10 - Publishing the Registry to the Web
We will now create a registry.<your-domain>
subdomain, and proxy the Verdaccio registry through its /api/
endpoint.
NOTE: Most of these steps were already covered in a previous article, so this time, only bullet-points will be listed. Feel free to use the other article as reference.
-
Create a DNS
A
record containingregistry
in the<your-domain>
zone with your server's IP address, for example in Hetzner DNS Console. -
Create an Nginx server block in
/etc/nginx/sites-available/registry.<your-domain>.conf
:server { include /etc/nginx/shared.d/server/http.conf; server_name registry.<your-domain>; } server { include /etc/nginx/site.d/<your-project>/server/https.conf; include /etc/nginx/shared.d/server/https.conf; server_name registry.<your-domain>; location /api/ { include /etc/nginx/site.d/<your-project>/location/cors.conf; include /etc/nginx/shared.d/location/proxy.conf; # auth_request off; proxy_pass http://localhost:3340/; } }
-
Create static content (optional):
$ mkdir -p /var/www/registry.<your-domain>/html $ semanage fcontext -a -t httpd_sys_content_t "/var/www/registry.<your-domain>/html(/.*)?" $ touch /var/www/registry.<your-domain>/html/index.html ... $ restorecon -Rv /var/www/registry.<your-domain>/html
-
Enable the subdomain:
$ ln -s /etc/nginx/sites-available/registry.<your-domain>.conf /etc/nginx/sites-enabled/registry.<your-domain>.conf $ nginx -t $ service nginx restart
Step 11 - Accessing the Registry
From now on you can access your secure Node.js registry from any of your devices (just be sure to properly set up scope based registries in .npmrc
):
$ npm config set @<scope>:registry https://registry.<your-domain>/api/
$ npm login --scope @<scope>
Conclusion
Now you can publish and unpublish your @<scope>
scoped packages to / from your private npm registry running on your webserver, without having to worry about accidentally publishing them publicly to npmjs
.