SecurityPaolo Castro

Managing Team Secrets with Pass and GPG

Extend pass beyond personal use to manage team secrets, API keys, and environment variables. Learn how to share encrypted secrets across a development team with version control and granular access control.

#security#gpg#passwords#devops#git#team

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:

hljs text
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:

hljs bash
#!/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:

hljs bash
# 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:

hljs bash
# 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:

hljs bash
# 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:

hljs bash
# 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:

hljs bash
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:

hljs bash
# 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:

hljs text
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:

hljs bash
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:

hljs bash
# 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:

hljs yaml
# .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:

hljs bash
# 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

hljs bash
# 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:

hljs bash
# 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:

hljs bash
# 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:

hljs bash
# 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.

Want to read more?

View All Stories