Enhancing security: Protecting your environment variables with Nix flakes, Direnv, and Infisical

Ivan Molina Rebolledo
- 3 min read

Abstract

Discover the security risks of storing variables in an .env file and how Infisical can help. With an easy-to-use interface and CLI, Infisical manages secrets securely. Plus, learn about the magic of Nix as we use a Scala project example.


Storing variables in an .env file can be dangerous since these files are typically not encrypted or protected, and they can be easily read by any process. This poses a significant security risk in which application secrets are exposed freely in the filesystem, giving easy insecure access to sensitive information.

Infisical is an open source tool designed to deal with this kinds of issues. This tool provides an interface for managing secrets, as well as a command-line interface (CLI) for accessing them via the terminal.

Nix is just magic. We won't go into details on how to install Nix, but that can be easily done with the new installer provided by Determinate Systems https://github.com/DeterminateSystems/nix-installer

We're going to use a Scala project as an example project, just because Scala is nice.

Infisical

While Infisical can be self-hosted [https://infisical.com/docs/self-hosting/overview], we are going to dependent in the free hosted services just for brevity. This is just an example after all. But the documentation for self-hosting is great, so it shouldn't matter that much for the general idea of what I'm trying to show.

You can begin making an account at https://app.infisical.com/signup. Then, after having created an account, you should generate a new env variable. Infisical generates an example project for you, we are going to use it. Go to secrets, change in your set of secrets to Test and add the following secret SECRET_API = verymuchapikey that we are going to use later on.

Example project

Let's write the basics. First the project build file ./build.sc:

import mill._, scalalib._

object example extends ScalaModule {
  def scalaVersion = "3.2.2"
  
  def ivyDeps = Agg(
    ivy"org.typelevel::cats-effect:3.4.8"
  )
}

then the main file ./example/src/example/Main.scala:

import cats.effect.{IO, IOApp}
import cats.effect.std.{Console, Env}

object HelloWorld extends IOApp.Simple:
  val run = Env[IO].get("SECRET_API").flatMap {
    case Some(secret) if secret == "verymuchapikey"  =>
      /* Do something with the secret */
      IO.pure(secret) *> Console[IO].println("[LOG]: OK")
    case other => Console[IO].println(s"[ERROR]: Connection... $other")
  }

But we also need our nix file flake.nix

{
  inputs = {
    nixpkgs.url = "github:ivanmoreau/nixpkgs";
    flake-parts.url = "github:hercules-ci/flake-parts";
  };

  outputs = inputs@{ self, nixpkgs, flake-parts, ... }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      systems = [
        "aarch64-darwin"
        "aarch64-linux"
        "x86_64-darwin"
        "x86_64-linux"
      ];
      perSystem = { config, self', inputs', pkgs, system, ... }: {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            mill infisical
          ];
        };
      };
    };
}

Now that's done!

(Don't forget to create an .gitignore and to init your git repo!)

direnv

Usually we need to use nix develop every time that we want to have our development tools as defined in the flake. direnv can simplify this process, since it will allows to automatically modify our environment so it matches the configuration for our project. There are several ways of getting direnv, but we'll use the nicest one:

nix profile install nixpkgs#direnv

then we need to install the hook for our respective shell as described here: https://direnv.net/docs/hook.html

In our project we add an .envrc file that direnv uses to load things:

use flake

That's it for now.

Now the next time that your enter your project folder it will ask for execution permission:

direnv allow

Then it'll begin initialising all the dependencies in the flake.

Using Infisical in the terminal

The first thing here is to auth Infisical with infisical login which will ask us for our access details:

Email: [email protected]
Password: ********
>>>> Welcome to Infisical! You are now logged in as [email protected] <<<< 

Quick links
- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage
- Stuck? Join our slack for quick support https://infisical.com/slack

Then we connect our Scala project with the example project in Infisical:

$ infisical init

✔ Example Project

After this point we have access to secrets.

Manually using secrets

We can run Infisical so it runs the secrets (remember the test configuration)

infisical run --env=test -- mill example.run

That should print [LOG]: OK.

Automatically with the flake and direnv

We can just add a hook to the mkShell:

...
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            mill infisical
          ];
          shellHook = ''
            eval $(infisical export --env=test --format=dotenv-export)
          '';
        };
...

the we can just run mill example.run. It will work without having to use Infisical at the start. And those secrets will only be available when you are in the project folder.

Conclusion and further exploration

Has Infisical allows several configurations per project, it's easy to have a default setup (that we can execute with the shellHook) and a specific setup that need to be called for the most important secrets. It's up to you. Infisical has good documentation of what you can do with it in the CLI. https://infisical.com/docs/cli/usage

There is also self-hosting if needed; which is probably going to be required in some contexts for security and compliance.

The management interface has things like logging audit, role-member management and integration with aws and/or GitLab.

Although you probably want to deploy it self-hosted behind a VPN in order to reduce risk to the minimum possible. After all, security it's highly important.