wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

This article is part of a mini series. Read them all:

Coming of age - encrypt your environment

Harden your developer setup with age and direnv

Hero image for Harden your developer setup with age and direnv

Credential stealing is rampant, so some diligence in guarding API keys, passphrases and secrets is warranted.

Stop wondering if it could happen to you, but prepare for when it might happen. Here is one arrow in your defense portfolio.

Limit the blast radius

When an attacker sneaks by your 2FA using e.g a supply chain attack your next line of defense is "How much can they see?"

  • For starters no key (other than public ones), token or credential shall live on disk unencrypted.
  • Loaded evn variables should be limited to their projects, when in ~/Code/someproject there must not be any env variable from ~/Code/otherproject be loaded.
  • The day-2-day use must be easy and convenient

Three components

To make this work we need three components:

  • macOS keychain: holds the AGE key(s), (Other operating systems is another story for another time).
  • age "Actually good encryption": to encrypt your secrets and .env variables.
  • direnv: Loads and unloads environment variables depending on the current directory.

Step by step

I'm presuming you have Homebrew installed. YOu might want to check my macOS setup for this.

Step 0 - installation and layout

brew install age direnv

direnv needs to be hooked into zsh, so add one line to ~/.zshrc

eval "$(direnv hook zsh)"

Your directory layout should look like this. Note that a strongly suggest Code as root for your projects, keep them out of Documents unless you want various cloud syncs and git fight over them

Directory Purpose
~/.config/age/ key material (transient)
~/.config/direnv/direnvrc load_age helper
~/Code/.envrc global loader for all projects
~/Code/.env.age global secrets, for all projects
~/Code/petproject/.envrc personal project
~/Code/petproject/.env.age optional overrides
~/Code/pro_team/.envrc team project
~/Code/pro_team/.env.age team secrets (multi-recipient)
~/Code/pro_team/recipients.txt team public keys, committed
Corporate

the recipients.txt is needed for projects with more than one developer.

Step 1 - Personal identity (once)

You will generate your personal age key(s) and add them to the macOS keychain. I create two keys, one for personal and one for corporate use.

# Make keys
mkdir -p ~/.config/age
age-keygen -o ~/.config/age/personal_key.txt
age-keygen -o ~/.config/age/corporate_key.txt

# Add to keychain
security add-generic-password -s age-personal -a "$USER" -w "$(grep '^AGE-SECRET-KEY-' ~/.config/age/personal_key.txt)"
security add-generic-password -s age-corporate -a "$USER" -w "$(grep '^AGE-SECRET-KEY-' ~/.config/age/corporate_key.txt)"

# remove the keys
rm ~/.config/age/personal_key.txt
rm ~/.config/age/corporate_key.txt

You need to capture your public keys, I add them to my ~/.zshrc

export AGE_PERSONAL_PUBLIC=age1qr8xa48c38xwlv8jhtcallv2vy4nz5kg5928svtjl7nhxu6s5dusaq8ypz
export AGE_CORPORATE_PUBLIC=age1eh3arwjahag7hpg5salc7rzwwq5eagrqmvl3ck9jf6r3pn59vvvslvdlcr

!!! Warning Local only

The keychain entry does not sync with iCloud

Step 2 - helper (once)

Create the file ~/.config/direnv/direnvrc

load_age_personal() {
  watch_file "$1"
  eval "$(age -d -i <(security find-generic-password -s age-personal -w) "$1")"
}

load_age_corporate() {
  watch_file "$1"
  eval "$(age -d -i <(security find-generic-password -s age-corporate -w) "$1")"
}

Create the file ~/bin/editenv.sh (or somewhere on your $PATH). It allows you comfortably updating encrypted env files

#!/bin/bash
# editenv - decrypt .env.age, edit with nano, re-encrypt.
set -euo pipefail

ENC=".env.age"
PLAIN=".env"
RECIPIENTS="recipients.txt"

die() { echo "envedit: $*" >&2; exit 1; }

keychain_identity() {
  local item="$1"
  security find-generic-password -s "$item" -w 2>/dev/null \
    || die "Keychain item '$item' not found"
}

command -v age >/dev/null || die "age is not installed"

# Decrypt or create
if [[ -f "$ENC" ]]; then
  if [[ -f "$RECIPIENTS" ]]; then
    IDENTITY_ITEM="age-corporate"
  else
    IDENTITY_ITEM="age-personal"
  fi
  echo "Decrypting $ENC using Keychain item '$IDENTITY_ITEM'..."
  age -d -i <(keychain_identity "$IDENTITY_ITEM") -o "$PLAIN" "$ENC" \
    || die "decryption failed"
else
  echo "No $ENC found — starting with an empty $PLAIN"
  touch "$PLAIN"
fi

chmod 600 "$PLAIN"

# Edit
nano "$PLAIN"

# Re-encrypt
if [[ -f "$RECIPIENTS" ]]; then
  echo "Encrypting to recipients listed in $RECIPIENTS..."
  age -e -R "$RECIPIENTS" -o "$ENC" "$PLAIN" || die "encryption failed"
else
  PUBKEY="$(age-keygen -y <(keychain_identity "age-personal"))" \
    || die "could not derive personal public key"
  echo "Encrypting to personal key $PUBKEY..."
  age -e -r "$PUBKEY" -o "$ENC" "$PLAIN" || die "encryption failed"
fi

rm -f $PLAIN
echo "Done: $ENC updated."

If something goes wrong the .env file might be left behind, if you are confident you can add an exit trap right after keychain_identity()

# Never leave plaintext behind, whatever happens
cleanup() { rm -f "$PLAIN"; }
trap cleanup EXIT

Step 3 - Global secrets

Execute whenever they change:

cat <<'EOF' | age -e -r ${AGE_PERSONAL_PUBLIC} -o ~/Code/.env.age
export SOME_KEY="dop_v1_..."
export SOME_TOKEN="cf_..."
EOF
printf 'load_age_personal ~/Code/.env.age\n' > ~/Code/.envrc
cd ~/Code && direnv allow

Step 4a - personal projects

Create .envrc in your project root:

source_up
[[ -f .env.age ]] && load_age_personal .env.age

Step 4b - corporate projects

Create .envrc in your project root:

source_up
[[ -f .env.age ]] && load_age_corporate .env.age

Add recipients.txt file with all participants' public keys line by line. Then encrypt to all:

cat <<'EOF' | age -e -R recipients.txt -o .env.age
export DO_KEY="team-specific..."
EOF

Use the helper script editenv to comfortably edit the encrypted environment.

Next up

integration into Github actions, Yubikey and SSH

As usual YMMV


Posted by on 21 August 2026 | Comments (0) | categories: Development GitHub macOS

Comments

  1. No comments yet, be the first to comment