Building binaries on compiled languages like Rust and Go as Static or dynamic linking makes a huge step on code security, I mean no one can access your code, reading it !, but in many use cases they can by doing some complicated reverse engineering methods ... actually it's hard! it becomes harder when you built your app as a static linking and bundle all the shit ๐ฉ together. Nodejs applications can be read it easily ... all your source code is accessible, In this post, we will hide our node app and "COMPILE" it! seems awesome right! let's go folks ๐ !
Get the app
You need to have nodejs installed to test the app locally, I tested the app on node 12.2.0
. You might have some problems with the database so make sure to use the appropriate mongoose
version that is compatible with MongoDB
, I have mongoose 5.10.10
that is compatible with mongo 4.4
. Let's run the app ๐ค !
Clone the code
Make sure you have Git installed!
git clone https://github.com/hatembentayeb/node-rest-api-mongo.git
cd node-rest-api-mongo
npm install
Run a mongo instance
make sure you have docker installed, otherwise install mongo directly.
docker run -p 27017:27017 --name mymongo -d mongo:latest
Modify the .env file
On the mongo uri we have the ip of the mongo container, you can use localhost:27017
instead, but we need the IP when we run the app inside the container.
PORT=3000
MONGODB_URI=mongodb://172.17.0.3:27017
DB_NAME=hatem
Run the app
You can run it with node app.js
npm start
Insert data
Using curl with POST
request to send data inside the products.json
. the jq
command is just for formatting JSON.
curl -X POST -H "Content-Type: application/json" "http://localhost:3000/products" -d @products.json | jq
Get data
Simple GET
request to get all requested data
curl http://localhost:3000/products | jq
Why we need a binary
This approach! i mean building binary can save us especially to get rid of the node_modules
inside our container! we will save space and enhance the image size! besides we don't need any base node image like FROM node: latest
because it's just a binary that needs a shell to run, also you have to make a choice of your build platform or architecture, you can find the full list of the available architectures for nexe
on this link full list.
In this tutorial, we need a minimal docker image ... like 5MB
of size ! , that's awesome ! yes, it's is the alpine
image, the smallest one! now we can deploy a docker image with a low size! it will be extremely fast believe me, if you have a small server with limited os storage or an on-premise docker registry with limited storage it will be wonderful to store images with small size !!
If you try to build the project with nexe
and then use it on a docker container based on alpine, it will work !, only in your dreams ๐คฃ. To be able to run it on alpine we need to set the right architecture platform before the build.
In terms of security, you are almost secure they can't get to your source code! it's a binary! dude, but they can access your container if they succeeded to create shell access with the RCE (remote code execution) attack
... they can do it by attacking your application and exploit it, so they are on your container now! they can't read your code but they can crash your container and access your host and here is the disaster ๐ณ ! so I will try to limit the privileges on the container by disabling the root access and create a non-root user and remove some of the default Linux commands like ls, cat and mv
๐.
Anyways, let's start building binaries ๐
Let's build it
In order to build the binary, we need a special package called nexe
with over then 9k
stars on GitHub ๐ง !!
To install it run this command npm install -g nexe
and check the package version nexe -v
, I have the 4.0.0-beta.16
version.
To build the application just run this command! :
next app.js -t alpine-x86-12.2.0 -o mybinary
let's breakdown the nexe options :
app.js
: is your application entry pointalpine-x86-12.2.0
: is the node version that is compatible with the alpine architecture platform with isx86
mybinary
: is your binary output file that we will copy to the container.
In some cases you need to bundle some HTML
files to your binary or any some directories, you can do that by adding the --recursive/-r
option and specify the path like this :
nexe app.js -t alpine-x86-12.2.0 -r static/**/*.html -o mybinary
Alright let's run the binary :
$ ./mybinary
Server started on port 3000...
Mongoose connected to db...
Mongodb connected....
And here we go! it's much like a command! try to move it to another location on your file system and run it again! awesome it works smoothly, congrats! you have a standalone binary for your node app ๐ !
Move it to docker
let's run the app with docker, we need a dockerfile to build the image, here is an example:
FROM alpine
WORKDIR app
RUN adduser --disabled-password btx
RUN rm -f /bin/cat /bin/ls /bin/mv /bin/find /bin/cd
COPY hatem .
COPY .env .
EXPOSE 3000
RUN chown -R btx:btx /app
USER btx
CMD ["./mybinary"]
We will base the container on a lightweight docker image: Alpine it's about 5MB in size! then we define our working directory /app to be the default directory for the rest of all commands and when you access the container via exec. Adding a non-root user called btx without a password and remove some of the basic Linux commands like ls , cat , mv , find and cd. Now no need to copy the whole project ๐ฅฒ, just that tiny binary! and that's it. We know that the app depends on a .env file so we copy it inside the container.
Exposing the container port is dependent on the port number on the .env file. Now all artifacts are in the right place, so now let's change the directory ownership to the btx user and activate that user to be the default one when we access the container via exec. The final command is to start the app as a single process on the container.
Time to cook ๐
Build the docker image :
$ docker build -t node-api-crud:v1 . --no-cache
Run the docker image :
$ docker run -p 3000:3000 --name node-rest node-api-crud:v1
Server started on port 3000...
Mongoose connected to db...
Mongodb connected....
We can now test the app and start inserting data and getting them via curl via localhost:3000
Final thoughts
To ensure more security test try to use :
- npm audit fix to fix any dependency vulnerability.
- Anshore for scanning the docker image and check if there is any critical CVE on the system packages.
- Trusted images from official docker hub repos, or make your own.
- Sonarqube to make some SAST and discover potential security issues on your code.