Hiding my nodejs application code within a docker container
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
Using curl with
POST request to send data inside 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 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
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 point
alpine-x86-12.2.0: is the node version that is compatible with the alpine architecture platform with is
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
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.