Building and deploying secure web applications is a full stack effort. This guide focuses on the server side end of things for a web application in your scripting language of choice, more specifically securing containerized node.js applications. Typically this involves: reducing attack surfaces, keeping everything up to date, and sticking to the principle of least privilege.
This isn’t surprising or new – and in fact operating systems have security oriented capabilities and tools baked in – but the introduction of containers makes it possible to benefit from these tools in a mostly automated zero-configuration way. A tool that is too hard to configure or is too inaccessible ends up not getting used at all. The ability to automate the tedious or error prone tasks results in an application that is more secure.
This guide will be using a sample node.js application and a real vulnerability in one of its dependencies to make everything a bit more concrete.
Leveraging OS built-in security features
Linux has all of the building blocks necessary to lock down applications: file permissions, SELinux / AppArmor / seccomp. Docker even provides default profiles for AppArmor and seccomp. This should help us with the principle of least privilege but unfortunately those tools aren’t pleasant to work with.
Fortunately we can find out a lot about our application when it is properly containerized.
It’s all about the metadata
If our application is containerized we get the benefit of lots of metadata about app, just by virtue of using the containers:
- Container type: We can deduce the role of our container by inspecting it and the base images it uses.
- Access to resources: We know which OS resources our container needs access to: filesystem paths, networking ports, and processes.
- Static code analysis: We can detect the language our application is written in and determine which system calls our application should be using. Or, more importantly, which ones it shouldn’t.
- Dynamic runtime monitoring: We can monitor our application when it starts running and build a behavior baseline so that we know when it deviates from this behavior.
And the best part? We don’t need to configure anything for all of the above. We just need to use containers as they were intended to be used: single purpose, defined in a Dockerfile, and based on existing and trusted images.
Behind the scenes with a real exploit
I made a simple node.js application using a vulnerable version of the JS-YAML package. It was patched after the vulnerability was disclosed but this example will use an unpatched older version. The application will run alongside Twistlock and will then be exploited using metasploit to simulate a real world scenario.
Twistlock Runtime will use all of the above metadata sources to automatically build a model of all of the resources each container should be using. The static and dynamic analysis of my node.js container results in a whitelist for processes, network access, file system, and system calls that is custom tailored for the current build of my application. This model will be rebuilt every time I deploy a new version.
Here is a snapshot of the whitelisted system calls for my node.js container:
Twistlock will show me that I’m using a vulnerable version as soon as I build my application image. I will ignore this warning for the demo and pretend we are in a world of this exploit still undisclosed but it is a good example of how twistlock helps us keep everything up to date.
When Twistlock is integrated into a Continuous Integration process this error prevents the image from being created. Our app would never even get deployed with a known exploit in one if its’ dependencies.
Let’s ignore that warning for now and see what Twistlock Runtime catches with an out-of-the-box installation.
Running the shellcode
The metasploit shellcode I’m using spawns a shell process and connects back to the attacker box, creating a reverse shell. Once the compromised application communicated with metasploit I sent a “ps” command and got back a list of running processes.
Twistlock Runtime catches the exploit both by detecting that a shell was spawned as well as intercepting unexpected system calls. Processes and system calls that aren’t in our whitelisted model are suspicious and should be blocked. Notice the process monitor also picked up on the “ps” command I ran through the reverse shell:
And the unexpected system calls:
This is an example of how it is possible to automatically enforce security around OS resources using the rich metadata we can get from a containerized application. Whitelisting system calls and processes can go a long way towards preventing malicious activity on a compromised server, especially when no explicit configuration is required.
If you are interested in finding out more about Twistlock technology and securing your node.js applications, request a free trial of our product here.
Follow us on Twitter
Follow us on Twitter for real time updates on the cloud native ecosystem, Twistlock product, and cloud native security threats.
Taming the Complexity Monster in a Cloud-Native WorldRead the Blog
How My Company (Teckro) Uses ContainersRead the Blog
Mitigating CVE-2019-5736 Impacting RunC and DockerRead the Blog
From Agile to DevSecOps and DevOps SecurityRead the Blog
What’s Next for Cloud-Native Infrastructure Technology?Read the Blog