Managing dev secrets with pass
I had credentials hardcoded in my Go project’s config, with defaults baked right into the binary. The LDAP admin password was sitting in config.go for anyone to read. Not great, even for a personal project. Here’s how I replaced that with pass, the standard Unix password manager.
The problem
func Load() *Config {
return &Config{
LdapPassword: getEnv("LDAP_PASSWORD", "<some secret value>"),
}
}
That default value ends up in git history, container images, and the compiled binary. The env var override exists but nobody uses it because the default just works.
Setting up pass
pass stores passwords as GPG-encrypted files under ~/.password-store/. It needs a GPG key to encrypt with:
gpg --gen-key
pass init <email>
Then store the secret:
pass generate --no-symbols myproject/ldap-password 32
The --no-symbols flag avoids shell escaping headaches with characters like ', {, and & in env vars.
Wiring it into a project
The config now requires the env var to be set, no fallback:
func requireEnv(env string) string {
v := os.Getenv(env)
if v == "" {
log.Fatalf("Required environment variable %s is not set", env)
}
return v
}
A .env file (gitignored) holds the actual values, and Docker Compose picks it up automatically. The justfile generates it from pass:
env:
@pass show myproject/ldap-password > /dev/null 2>&1 || pass generate --no-symbols myproject/ldap-password 32
@echo "LDAP_PASSWORD=$(pass myproject/ldap-password)" > .env
@echo ".env written"
This creates the password if it doesn’t exist yet, so a fresh clone just needs just env to get going.
In docker-compose.yml, both the app and the LDAP server reference the same variable:
services:
app:
environment:
- LDAP_PASSWORD=${LDAP_PASSWORD}
lldap:
environment:
- LLDAP_LDAP_USER_PASS=${LDAP_PASSWORD}
Syncing across machines
pass has built-in git support:
pass git init
pass git remote add origin ssh://git@git.example.com/user/password-store.git
pass git push -u origin main
Every pass insert or pass generate auto-commits, but doesn’t push. To sync automatically, add a post-commit hook:
cat > ~/.password-store/.git/hooks/post-commit << 'EOF'
#!/bin/sh
git pull --rebase && git push
EOF
chmod +x ~/.password-store/.git/hooks/post-commit
Now every pass operation will pull any remote changes and push the new commit.
On a new machine, clone the repo into ~/.password-store/ and import the GPG key:
gpg --import private-key.asc
gpg --edit-key <email> # trust -> 5 (ultimate)
I keep the GPG private key backed up in my password manager, and the password store in a private git repo. The encrypted files are safe to push since they’re useless without the GPG key.
The result
No more secrets in source code. Run just env to pull secrets from pass into .env, then docker compose up picks them up and injects them into containers. The app refuses to start without them. New machines just need pass initialized with the GPG key and a git clone.