In my previous post about pass, I covered how to use the Unix password manager for personal password management. But one of pass's most powerful features is its ability to manage shared secrets across a development team.
Most teams struggle with secret management. Secrets end up in Slack messages, wikis, or worse — committed to repositories. Commercial secret management solutions exist, but they often come with costs, complexity, and vendor lock-in. What if you could use the same simple, encrypted, version-controlled approach for team secrets?
Why Team Secret Management is Hard
Development teams deal with numerous secrets:
- Third-party API keys (Stripe, SendGrid, AWS)
- Database credentials for different environments
- CI/CD deployment tokens
- SSH keys and certificates
- Service account passwords
- Internal API credentials
The challenges are real:
- Distribution: How do you securely share secrets with new team members?
- Rotation: When someone leaves, how do you rotate secrets?
- Access control: Not everyone needs access to production credentials
- Audit trail: Who changed what and when?
- Availability: Secrets need to work offline and survive service outages
Pass with GPG addresses all of these through encryption, version control, and granular access control.
Project Structure
The typical setup uses a dedicated private repository for team secrets:
team-secrets/
├── .gitignore
├── setup.sh
└── store/
├── .gpg-id
├── AWS/
│ ├── dev-access-key.gpg
│ ├── staging-access-key.gpg
│ └── prod-access-key.gpg
├── Database/
│ ├── dev-postgres.gpg
│ ├── staging-postgres.gpg
│ └── prod-postgres.gpg
└── API-Keys/
├── stripe-test.gpg
├── stripe-live.gpg
└── sendgrid.gpg
The setup.sh script configures your shell to use this password store:
#!/bin/bash
# setup.sh - Source this file to use the team password store
export PASSWORD_STORE_DIR="$PWD/store"
echo "Password store set to: $PASSWORD_STORE_DIR"
echo "You can now use 'pass' commands for team secrets"
Usage is simple:
# Clone the repository
git clone git@github.com:yourteam/team-secrets.git
cd team-secrets
# Source the setup script
source setup.sh
# Now pass commands work with the team store
pass AWS/dev-access-key
This approach keeps team secrets isolated from personal passwords and makes it clear which password store you're working with.
Team GPG Key Exchange
Before initializing the team password store, everyone needs to share their GPG public keys.
Sharing Public Keys
Each team member exports their public key:
# Export your public key
gpg --export --armor your.email@company.com > your-name.asc
# Share this file with the team (email, Slack, etc.)
Importing Team Keys
When you receive a teammate's public key:
# Import the key
gpg --import teammate-name.asc
# Trust the key
gpg --edit-key teammate@company.com
gpg> trust
# Your decision? 4 (full trust) or 5 (ultimate trust)
gpg> quit
Initializing the Team Password Store
Here's where pass gets interesting. Instead of using cryptic key IDs, you can use email addresses to specify who can decrypt secrets:
# Initialize the password store with multiple recipients
pass init \
alice@company.com \
bob@company.com \
charlie@company.com
# This creates a .gpg-id file listing all recipients
Now when you add a secret, it's encrypted for all three team members:
pass insert API-Keys/stripe-test
# This secret can be decrypted by alice, bob, or charlie
Per-Folder Access Control
One of pass's most powerful features is per-directory recipients. You can restrict certain secrets to specific team members:
# Create a production-only folder
mkdir -p store/production
# Initialize it with only senior team members
pass init -p production \
alice@company.com \
bob@company.com
# Now only alice and bob can decrypt production secrets
pass insert production/database-password
# But everyone can still access non-production secrets
pass insert staging/database-password
This creates a .gpg-id file in the production directory with its own
recipient list. Pass automatically uses the most specific .gpg-id
file for each secret.
Organization Best Practices
How you organize secrets depends on your team, but here's a structure that has worked well:
store/
├── .gpg-id # Default: all team members
├── Third-Party/
│ ├── AWS/
│ ├── Stripe/
│ ├── SendGrid/
│ └── GitHub/
├── Environments/
│ ├── dev/
│ ├── staging/
│ └── production/ # .gpg-id with limited access
│ ├── .gpg-id
│ ├── database.gpg
│ ├── api-keys.gpg
│ └── ssh-keys.gpg
├── CI-CD/
│ ├── github-actions.gpg
│ └── deploy-key.gpg
└── Internal/
├── vpn-credentials.gpg
└── admin-accounts.gpg
Key principles:
- Separate by environment: dev, staging, production
- Restrict production access: Only necessary team members
- Group by service: Keep related secrets together
- Include metadata: Use multiline entries with notes
Example of a well-structured secret:
pass insert -m Environments/production/database
# Enter:
# postgresql://user:password@host:5432/database
# Host: prod-db-01.company.internal
# Last rotated: 2022-10-01
# Owner: SRE team
CI/CD Integration
Using secrets in automated environments like CI/CD pipelines requires some
configuration, but the good news is you don't need pass at all — just GPG
to decrypt the files directly.
Setting Up a CI/CD GPG Key
Create a dedicated GPG key for your CI/CD system with no passphrase:
# Generate a key with NO passphrase (for automation)
gpg --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: CI/CD Bot
Name-Email: cicd@company.com
Expire-Date: 1y
EOF
# Export the private key
gpg --export-secret-keys --armor cicd@company.com > cicd-private-key.asc
# Add the CI/CD key as a recipient to your secrets
# Re-initialize with all team members plus the CI/CD key
pass init \
alice@company.com \
bob@company.com \
charlie@company.com \
cicd@company.com
Using in CI/CD
The CI/CD machine just needs GPG configured and can decrypt files directly. Here's a GitHub Actions example:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup GPG
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
# Import the CI/CD GPG key
echo "$GPG_PRIVATE_KEY" | gpg --import --batch
# Configure GPG for non-interactive use
echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf
echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
gpg-connect-agent reloadagent /bye
- name: Clone secrets repo
run: git clone git@github.com:yourteam/team-secrets.git
- name: Decrypt and use secrets
run: |
# Decrypt secrets directly with gpg (no pass needed!)
export DATABASE_URL=$(gpg --decrypt team-secrets/store/Database/prod-postgres.gpg | head -n1)
export API_KEY=$(gpg --decrypt team-secrets/store/API-Keys/stripe-live.gpg)
# Or decrypt to a file
gpg --decrypt team-secrets/store/CI-CD/deploy-key.gpg > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
# Run your deployment
./deploy.sh
The key configuration for non-interactive GPG:
# Tell GPG to not require interactive passphrase entry
echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf
echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
gpg-connect-agent reloadagent /bye
This works because the CI/CD key has no passphrase, making it safe for automated decryption.
Security considerations:
- Store the CI/CD private key in your CI platform's secret management (GitHub Secrets, GitLab CI Variables, etc.)
- Never commit the CI/CD private key to any repository
- Set an expiration date on CI/CD keys and rotate them regularly
- Limit CI/CD key access to only necessary secrets using per-folder encryption
Team Changes: Adding and Removing Members
Teams evolve. Here's how to handle membership changes:
Adding a New Member
# Get their public key and import it
gpg --import new-member.asc
# Re-initialize the password store with the new member
pass init \
alice@company.com \
bob@company.com \
charlie@company.com \
diana@company.com # New member
# This re-encrypts ALL secrets for the new recipient list
Pass automatically re-encrypts every secret for the new set of recipients. This can take a moment for large password stores, but it's a one-time operation.
Removing a Member (Critical!)
When someone leaves the team, you must re-encrypt secrets without their key:
# Remove them from GPG (optional, prevents re-adding accidentally)
gpg --delete-key departed@company.com
# Re-initialize without them
pass init \
alice@company.com \
bob@company.com \
charlie@company.com
# departed@company.com removed
# Commit the changes
pass git push
Important: This re-encrypts secrets, but the departing member already has decrypted copies. For true security, you should rotate sensitive secrets after someone leaves, especially production credentials.
A prudent approach:
# 1. Remove them from pass
pass init alice@company.com bob@company.com charlie@company.com
# 2. Rotate critical secrets
pass generate -f Environments/production/database 32
pass generate -f API-Keys/stripe-live 32
# 3. Update actual services with new credentials
# 4. Commit and push
pass git push
Real-World Experience
I've used this approach with development teams ranging from 3 to 12 people, and it has several advantages over commercial solutions:
What works well:
- Offline access: No dependency on external services during deployments
- Git workflow: Familiar branching, merging, pull requests for secret changes
- Audit trail: Complete history of who changed what and when
- Cost: Zero ongoing costs, just Git hosting
- Flexibility: Per-folder access control handles complex permission needs
- CI/CD integration: Works in any automation environment
Challenges:
- Onboarding overhead: New members need GPG knowledge
- Key management: Team members must back up their GPG keys
- Rotation discipline: Requires team discipline to rotate on departures
- Scale: Works best with teams under 15 people
Security Considerations
Some important security practices:
Key hygiene: Team members must protect their GPG private keys. A compromised key compromises all secrets they can access.
Rotation policy: Establish a rotation schedule for critical secrets, especially:
- When team members leave
- Annually for production credentials
- After any suspected compromise
Audit regularly: Review .gpg-id files to ensure access lists are current:
# See who can access production secrets
cat store/production/.gpg-id
Separate by sensitivity: Use directory-level encryption for different access levels. Not everyone needs production access.
Backup strategy: The Git repository is your backup, but ensure multiple team members can access it. Avoid single points of failure.
Conclusion
Team secret management doesn't have to be complicated or expensive. Pass with GPG provides a simple, auditable, version-controlled approach that works for small to medium-sized teams.
Is it perfect? No. Very large organizations might need more sophisticated solutions with centralized key management and detailed audit logging. But for most development teams, this approach offers the right balance of security, simplicity, and cost.
The key advantages are ownership and transparency. You know exactly where your secrets are, who can access them, and what changed over time. When combined with the personal password management setup from my previous post, you get a complete, unified approach to secret management.
Your team's secrets are too important to scatter across wikis, chat messages, and developer laptops. With pass and GPG, you can manage them properly — encrypted, version-controlled, and access-controlled — using tools that will still work a decade from now.