GnuPG, pass and environment variables

nix linux wsl

Vault is a popular secret store in the production thanks to its helm integration: the secrets are injected to the pods via helm side car. No secrets are exposed in the plaintext in the deployment pipeline. πŸŽ‰

I would like to adopt the similar approach to manage secrets in the local development such as the database credentials, API keys, etc. They are currently all over the place:

Pass and GnuPG

First, we need a secure secret store, such as pass. It encrypts the secrets with GnuPG public key, and decrypt the secrets with private key. Let’s create a new GnuPG key pair:

gpg --gen-key
... ...
pub   ed25519 2025-06-15 [SC] [expires: 2028-06-14]
      6C84280B702324AC558BF08E29D906D3B8CA5CA2
... ...

Don’t forget to secure the .gnupg with correct file permissions:

chmod 700 ~/.gnupg

The key is identified as 6C84280B702324AC558BF08E29D906D3B8CA5CA2, which will be used to intialize the pass store, β€” alternatively you can use the email. More details can be found in this guide.

pass init 6C84280B702324AC558BF08E29D906D3B8CA5CA2
pass git init

We can organze the secrets with arbitary nested folders, one benefit is that you can delete them recursively 😊.

❯ pass insert epicgames/CODEFRESH_API_KEY
mkdir: created directory '/Users/kunxi/.password-store/epicgames'
Enter password for epicgames/CODEFRESH_API_KEY:

We can list the secrets as:

❯ pass
Password Store
└── epicgames
    └── CODEFRESH_API_KEY

Under the hood, the secrets are stored as gpg encrypted data:

❯ tree ~/.password-store
/Users/kunxi/.password-store
└── epicgames
    └── CODEFRESH_API_KEY.gpg

Then I can replace the secret in plaintext with the following snippet in the .envrc:

export CODEFRESH_API_KEY=$(pass epicgames/CODEFRESH_API_KEY)

You will be challenged by the pinentry for the pass phase of the private key. In macOS, it is pinentry-mac as the GnuPG key pairs are stored in the system keychain. In Linux, it is curses-based pinentry app coupled with gpg-agent.

Caveats under Windows

After I published this post, I suddenly realized the above solution does not solve my problem, at all β€” how could I populate the environment variables for IntelliJ in build and run time without exposing the secrets in plaintext?

There are several options, but none of them just works β„’.

JetBrains Gateway

The JetBrains Gateway is still in beta, but it is probably the most promising approach. It worked like VsCode remote development: the server runs in the remote machine, WSL more concretely, then the UI client connects to the server for rendering. Unfortunately, the server is a puppet of the client. It does not support direnv, and I have no idea how to inject the environment variables from the shell.

JetBrains Gateway in Action

I really have high hope on this project, as it seems to lead to the right direction to do remote development right. /sigh.

Remote Debugging

We can run maven or gradle in WSL to build, test the project in the shell configured by the direnv, and just use IntelliJ for editing and debugging. This might be a reasonable compromise, and it should work. But I just miss the one-click convenience to run gradle tasks.

All-in Windows

We can try to replicate the above setup in Windows:

This might work, but I loathe to setup another shell environment with inconsistent behaviors while I already have one. I might just stick to cygwin if I could settle for a suboptimal experience, β€” personal opinion, no offense.

All-in Linux

This is the exactly opposite of the above direction, we can run IntelliJ as a WSL GUI application, recommended in this guide.

I still consider the WSL is an augment for Windows developer to access Unix environment. If I decide to go this far, maybe I should just pull the trigger to ditch Windows and install NixOS on my workstation.

Closing Thoughts

Every time I am frustrated with the tug of war of Windows application and shell, I just eyed my MacBook Pro: there is a reason developers pay premium for a coherent development environment.

Footnotes

  1. If you use zsh, you can set HIST_IGNORE_SPACE option to avoid persisting sensitive data in the history. ↩