Engineering
Updated
4 min read

Secure local Node.js dev servers with OrbStack

Configuring OrbStack for local SSL certificates without needing to manage custom self-signed certs.

Secure local Node.js dev servers with OrbStack

At Arcjet, we use OrbStack to manage our local development environment. We run various containers in production, so we use Docker Compose to mirror those services locally. This includes our low-latency security decision API used by our security as code SDKs, the dashboard webapp, website, docs, and backend processing components.

OrbStack is a feature-rich alternative to (but compatible with) Docker Desktop. It has a significantly better UI, is much more performant, and comes with powerful features like Custom Domains and Automatic HTTPS. These allow us to mirror our production environment very closely, particularly ensuring we have full SSL configured locally so we can properly test things like security headers.

Recently, OrbStack 1.9 made improvements so SSL certificates are now trusted between containers. I decided to spend some time improving our development experience by replacing the self-signed certificates we previously used between containers with this new OrbStack functionality.

Working… Sometimes

The first change I made was removing a custom HTTP transport workaround to allow the local certificates between our services written in Go. This was very straightforward and no other changes were needed—the Go services trusted the certificate and seamlessly communicated via HTTPS. This allowed me to remove the self-signed certificates generated by mkcert and remove a setup step for our development environment.

However, whenever I tried to make the same changes for our Node.js services, they would fail with an error: self-signed certificate in certificate chain. This didn’t make sense to me because OrbStack claimed that these certificates were trusted between containers and the error went away in Go code with the 1.9 release.

Bundled certificates

In scouring the Node.js documentation, I discovered the --use-openssl-ca command-line flag. The documentation explicitly calls out:

The bundled CA store, as supplied by Node.js, is a snapshot of Mozilla CA store that is fixed at release time. It is identical on all supported platforms.

This means that Node.js bundles a static snapshot of Mozilla’s Certificate Authority which it uses by default to validate certificates. Once I understood this, it makes sense that each Node.js service still flagged the OrbStack certificates would be untrusted—they aren’t included in Mozilla’s CA!

Luckily, Node.js accepts the --use-openssl-ca flag to opt-out of the bundled CA and instead rely on OpenSSL for CA. The documentation even explains that changes to the CA require this flag:

Using OpenSSL store allows for external modifications of the store. For most Linux and BSD distributions, this store is maintained by the distribution maintainers and system administrators. OpenSSL CA store location is dependent on configuration of the OpenSSL library but this can be altered at runtime using environment variables.

Custom OpenSSL CA store location in a container

We want this to apply to any instance of the node command run inside our containers without needing to specify the flag each time. We can apply this globally by setting the NODE_OPTIONS environment variable in our container. I set this in our docker-compose.yml file:

node-service:
  build:
    context: .
    dockerfile: services/node-service/Dockerfile
  environment:
    - NODE_OPTIONS="--use-openssl-ca"

Root certificate

Even with this option set, communication from our Node.js services was still failing. It turns out that OrbStack mounts the root certificate at /usr/local/share/ca-certificates/orbstack-root.crt but the OpenSSL CA doesn’t know about it.

To make Node.js aware of the root certificate, we need to set the SSL_CERT_FILE environment variable. I updated our docker-compose.yml file with this variable:

node-service:
  build:
    context: .
    dockerfile: services/node-service/Dockerfile
  environment:
    - NODE_OPTIONS="--use-openssl-ca"
    - SSL_CERT_FILE=/usr/local/share/ca-certificates/orbstack-root.crt

Perhaps we could apply some other commands to avoid setting the SSL_CERT_FILE variable, but I wasn’t sure which magic incantation to apply.

OpenSSL headers are not in slim containers

Be aware of your base containers when applying this technique. Using a *-slim container will fail with ERR_SSL_WRONG_VERSION_NUMBER because the OpenSSL headers are removed to slim down the container. Instead, use the full container image or reinstall the OpenSSL headers that were removed.

HTTPS dev servers

With all of the above applied, we have seamless HTTPS communication in our entire development stack—our browser communicates over HTTPS with our applications which communicate over HTTPS to various other services.

Development certificates have always frustrated me, so I am delighted that none of this required manually generating a certificate or committing a development certificate into the repository. Finally, we have a streamlined onboarding process for our development environment with full HTTPS support!

Bonus: Vite

Recently, Vite fixed a CVE that would allow a malicious page to interact with the dev server. Their solution (more or less) was to disallow anything other than the localhost domain from communicating with the dev server.

This is a problem with the OrbStack Custom Domains feature because you are accessing the Vite dev server via a domain such as https://modest_bhaskara.orb.local instead of localhost. You can solve this by setting your custom domain in server.allowedHosts in your vite.config.js file:

export default {
  server: {
    allowedHosts: ["modest_bhaskara.orb.local"]
  }
}

Unfortunately, OrbStack doesn’t provide this value inside the container. If it were available as an environment variable, we could specify it as a generalized process.env property access. However, the custom domain names are based on the container name or customizable with labels, so we know what the domain will be for each container.

It’s great to see these regular improvements to OrbStack, which is a core part of our development setup. It means we can remove dev-only workarounds so our development environment is as close as possible to mirroring production.

Related articles

Subscribe by email

Get the full posts by email every week.