How To Prevent Secrets From Ending Up On Developer's Machines

Goodbye hard-coded .env files. Hello, runtime secrets injection.
Tutorials
Jun 9, 2022
/
5
 MIN READ

Even with environment variable storage offered by modern hosting platforms and secrets managers provided by every cloud, developer's machines are still littered with secrets in unencrypted text files because local development was left out of the picture.

But we can remedy this situation using dynamic secrets injection and ephemeral secrets files, and in this post, we'll use the Doppler CLI to demonstrate how this is possible.

But before diving in, we need to solve the problem of secure and encrypted storage for secrets used in local development.

SecretOps and Local Development

Development-scoped secrets simply don't exist in traditional solutions because secrets are siloed within the confines of their respective cloud or platform.

While multi-cloud capable secret managers such as HashiCorp Vault showed great promise, the prohibitively steep learning curve and unavoidable complexity involved with fetching secrets create a significant barrier to adoption as teams are left with no incentive to switch.

A SecretOps Platform builds upon the idea of centralized secrets storage but differs from existing solutions by providing a CLI and integrations for syncing secrets to any environment, machine, platform, or cloud secrets manager. It's the best of both worlds, providing a single source of truth for management while development teams choose the best secrets access method on a per-application basis.

How does this help local development? In a SecretOps world, each application has a Development environment specifically for use on developer's machines, solving the storage problem.

Using the Doppler CLI to demonstrate, let's dive in to explore the mechanics of dynamic secrets injection and ephemeral secrets files.

Dynamic Secrets Injection

The Doppler CLI uses the same secrets injection model as platforms such as Heroku and Cloudflare Workers by injecting the secrets as environment variables into the application process.

You can use a command:

doppler run -- npm start

A script:

doppler run -- ./launch-app.sh

Create a long-running background process in a virtual machine:

nohup doppler run -- npm start >/dev/null 2>&1 &

Use the Doppler CLI inside a Docker container:

FROM ubuntu

# Install Doppler CLI
RUN curl -Ls --tlsv1.2 --proto "=https" --retry 3 https://cli.doppler.com/install.sh | sh

CMD ["doppler", "run", "--", "npm", "start"]

And inject environment variables consumed by Docker Compose:

doppler run -- docker-compose up

It's also possible to pipe secrets in .env file format to the Docker CLI, where it reads the output as a file using process substitution:

docker run \
  --env-file <(doppler secrets download --no-file --format docker) \
  my-awesome-app

The same technique applies to creating a Kubernetes secret:

kubectl create secret generic my-app-secret \
  --from-env-file <(doppler secrets download --no-file --format docker)

But if a secrets file is what your application needs, we've got you covered.

Ephemeral Secrets Files

The Doppler CLI enables the mounting of ephemeral secrets files in .env, JSON, or a custom file format using a secrets template that is automatically cleaned up when the application process exits. Imagine how happy your Security team will be when they learn that secrets will never live on any developer's machines again!

To mount a .env file for Node.js applications:

doppler run --mount .env -- npm start

To mount a .env file for PHP applications:

doppler run --mount .env -- php artisan serve

You can specify the format using --mount-format if the file extension doesn't map to a known format:

doppler run --mount app.config --mount-format json -- npm start

Or you can use a custom template, e.g. configure Firebase functions emulator using a .runtimeconfig.json file:

# 1. Create the template
echo '{ "doppler": {{tojson .}} }' > .runtimeconfig.json.tmpl

# 2. Mount the .runtimeconfig.json and run the emulator
doppler run \
  --mount .runtimeconfig.json \
  --mount-template .runtimeconfig.json.tmpl -- firebase emulators:start --only functions

You can even make things more secure by restricting the number of read requests using the --mount-max-reads option, e.g. caching PHP configuration, which only requires the .env file to be read once:

doppler run \
  --mount .env \
  --mount-max-reads 1 \
  --command="php artisan config:cache && php artisan serve"

If you're wondering what happens to the mounted file if the Doppler process is force killed, its file contents will appear to vanish instantly. The mounted file isn't a regular file at all, but a Unix named-pipe. If you've heard the phrase "everything is a file in Unix", you now have a better understanding of what that means.

Named pipes are designed for inter-process communication while still using the file system as the access point. Think of it as a client-server model where your application (the client) is sending a read request to the Doppler CLI (server). If the Doppler CLI is force killed, the .env file (named pipe) will still exist, but requests to read the file will hang because no process is attached. In this event, delete the file from your terminal, and you're good to go.

Summary

Thanks to SecretOps and Doppler, we now have the workflows required to prevent secrets from ever living on a developer's machine again. All you need is dynamic secrets injection, ephemeral secrets files, and a single source of truth to pull it all together.