APRIL 12, 2023

As software development has become increasingly complex, developers have come to rely heavily on third-party dependencies to speed up the development process. However, these dependencies also bring potential security vulnerabilities that can be exploited by attackers. One of the most significant vulnerabilities is Remote Code Execution (RCE), which can lead to a complete compromise of the system, including access to sensitive data and the ability to execute malicious commands. Therefore, securing the development environment against such attacks has become a critical task for every developer.

npm packages colors and faker supply chain attack

npm packages colors and faker supply chain attack

One solution to this problem is to use container development environments. Containerization provides an isolated environment for the application to run in, which can help to prevent the spread of malicious code. By using containerization, we can mitigate the risk of RCE vulnerabilities and significantly increase the security of our development environment.

One of the primary benefits of containerization is that it provides an good enough reproducible build, using tools such as Docker. This reproducibility and portability are essential for maintaining a cross-platform development environment that is consistent across projects. Additionally, containerization allows us to build and deploy applications more quickly and efficiently, which is critical for modern software development.

# Dockerfile

# Make this Dockerfile modular by using a configurable base image
ARG BASE_IMAG
FROM ${BASE_IMAGE}

# For CUDA profiling, TensorFlow requires CUPTI.
ENV LD_LIBRARY_PATH /usr/local/cuda/extras/CUPTI/lib64:/usr/local/cuda/lib64:$LD_LIBRARY_PATH

# Link the libcuda stub to the location where Tensorflow is searching for it and reconfigure
# dynamic linker run-time bindings
RUN ln -s /usr/local/cuda/lib64/stubs/libcuda.so /usr/local/cuda/lib64/stubs/libcuda.so.1 \\
  && echo "/usr/local/cuda/lib64/stubs" > /etc/ld.so.conf.d/z-cuda-stubs.conf \\
  && ldconfig

However, in the process of building a completely isolated environment, we may miss a crucial thing which is developer experience, a simple shell terminal would not be enough while having the burden to think about cache ordering between your oh-my-zsh plugins and your git config aliases is exhausting. Fortunately, there is a solution for that: devcontainer. This is a project from the Visual Studio Code team that powers GitHub Codespaces, which makes the plumbing work much easier, including mounting your repository, installing the remote code server, and injecting your SSH agent in a simple and compressible JSON file !

{
  "image": "mcr.microsoft.com/vscode/devcontainers/base:jammy",
  "features": {
    "ghcr.io/devcontainers/features/node:1": {},
    "ghcr.io/devcontainers/features/python:1": {},
  }
}

Despite this, there is still a problem with heavy system dependencies such as OpenSSL, QEMU, and even CUDA. To solve this problem, we can use Nix, which is an operating system-neutral unix-like package manager that can install pretty much anything in a truly reproducible way while also being versioned and locked via Flake similarly to package-lock.json.

# flake.nix

{
  # Explicit your dependencies
  inputs = {

   # We are looking for nixpkgs which is the central repository
   # that hold more than 70000 packages
   nixpkgs.url = "github:nixos/nixpkgs";

   # Devenv is a project that configure your development environment
   # such as installing and configuring your dependencies including
   # pre-commit in a single line for many well known formatters
   devenv.url = "github:cachix/devenv";
  };

  outputs = { nixpkgs, devenv, ... }@inputs: {

   # Now we are declaring our development environment, please take
   # look at the devenv documentation for futher explaination
   devShells = nixpkgs.lib.genAttrs nixpkgs.lib.platforms.unix (system:
      let pkgs = import nixpkgs { inherit system; }; in {
        default = devenv.lib.mkShell {
          inherit inputs pkgs;
          modules = [
            {
              pre-commit.hooks = {
                eslint.enable = true;
                prettier.enable = true;
                black.enable = true;
                isort.enable = true;
              };
              packages = [
                pkgs.python
                pkgs.nodejs
              ];
            }
          ];
        };
      }
    );
  };
}

By combining with direnv and the associated VSCode extension, every change made in the repository can be instantaneously reflected without having to rebuild the entire container.

# .envrc

# Create the development environment
use flake

# Predefine a Google Cloud project if not provided for convenience
export GOOGLE_PROJECT_ID=${GOOGLE_PROJECT_ID:-foobar}

# Fetch Google Application Credential from Vault
if [ ! -f key.json ]; then
  vault kv get -field=keyfile $GOOGLE_PROJECT_ID > key.json
fi

In a short conclusion, container development environments are a powerful tool for mitigating the risk of open-source third-party dependency RCE vector attacks. By providing an isolated environment for the application to run in, containerization can help prevent the spread of malicious code and significantly increase the security of our development environment. As developers, it is our responsibility to ensure that our systems are as secure as possible, and containerization is an essential step towards achieving this goal.

For further exploration, you can check out the following resources: