Git-Crypt: Encrypting Sensitive Files in Git Without a Separate Secrets Store
The problem: some files are inherently tied to the codebase (.env, credentials.json, TLS keys) but can’t be committed in plaintext. The usual solution is a separate secrets manager (Vault, AWS SSM, GCP Secret Manager) — but that adds operational overhead. Git-crypt offers a simpler path: encrypt specific files in the repo itself, so they’re protected at rest but still versioned alongside the code.
How Git-Crypt Works
%%{ init: { 'look': 'handDrawn' } }%%
graph TD
A[git commit] --> B{Gitattributes<br/>match?}
B -->|yes| C[git-crypt encrypts<br/>before storing]
B -->|no| D[Stored in plaintext]
C --> E[Git object store<br/>encrypted content]
D --> F[Git object store<br/>plaintext content]
G[git checkout] --> H{User has<br/>git-crypt key?}
H -->|yes| I[git-crypt decrypts<br/>transparently]
H -->|no| J[File remains<br/>encrypted binary]
E --> I
E --> J
F --> I
F --> J
Git-crypt hooks into Git’s filter driver. When you commit a matching file, it’s encrypted before being stored. When you checkout, it’s decrypted if you have the key. To anyone without the key, the file looks like binary noise.
Setup and Initialization
# Install git-crypt
brew install git-crypt # macOS
apt install git-crypt # Ubuntu/Debian
# Initialize in a repo
git crypt init
# Generate a GPG key (if you don't have one)
gpg --full-generate-key # Choose RSA, 4096, no expiry
Configuring Files to Encrypt
# .gitattributes
# Encrypt these files
.env filter=git-crypt diff=git-crypt
*.key filter=git-crypt diff=git-crypt
credentials.json filter=git-crypt diff=git-crypt
secrets/** filter=git-crypt diff=git-crypt
terraform/*.tfvars filter=git-crypt diff=git-crypt
# Don't encrypt these even if they match above
!important.env !filter
The !filter entries are exclusions. The diff=git-crypt ensures git diff on encrypted files shows the plaintext when you have the key.
Adding Team Members
# Add a team member by GPG key ID
git crypt add-gpg-user --trusted GPG_KEY_ID
# The --trusted flag means their key is marked as ultimately trusted
# (needed for GPG key verification to work cleanly)
This encrypts the repository’s symmetric key with each team member’s GPG public key. Anyone with a registered GPG private key can unlock the repo.
The Encryption Flow
%%{ init: { 'look': 'handDrawn' } }%%
graph LR
A[Repo admin] -->|1. Generate repo<br/>symmetric key| B[git-crypt<br/>symmetric key]
B -->|2. Encrypt key with<br/>each user's GPG| C[GPG encrypted<br/>key copies]
D[Committer with<br/>GPG private key] -->|3. Unlock repo| E[git-crypt<br/>unlock]
E -->|uses GPG<br/>private key| F[Decrypt symmetric key]
F -->|4. Access<br/>encrypted files| G[Plaintext files<br/>available]
H[No GPG key] -->|git crypt unlock<br/>fails| I[Encrypted<br/>binary only]
The symmetric key is stored encrypted with multiple GPG keys — one per team member. Decrypting requires having one of those private keys available (gpg-agent handles this).
Unlocking and Locking
# Unlock the repo (prompts for GPG passphrase)
git crypt unlock
# Lock the repo (clears the symmetric key from memory)
git crypt lock
# Verify encryption status
git crypt status
Workflow Example: Encrypted Terraform Variables
# .gitattributes
terraform/prod.tfvars filter=git-crypt diff=git-crypt
# terraform/prod.tfvars (plaintext locally, encrypted at rest)
resource_group_name = "prod-rg"
db_password = "actual-production-password"
storage_account_key = "xxxxxxxxxxxxx"
# Commit — git-crypt encrypts automatically
git add terraform/prod.tfvars
git commit -m "Add prod variables"
# Push — encrypted blob goes to GitHub/Gitea
git push
Anyone cloning the repo without the key sees a binary blob in terraform/prod.tfvars. Anyone with the key sees the actual values.
When Git-Crypt Makes Sense
| Use Case | Git-Crypt | External Secrets Manager |
|---|---|---|
| Small team, simple workflow | ✅ | Overkill |
| IaC with tfvars per environment | ✅ | Works but adds complexity |
| Credentials tied to specific files | ✅ | Extra layer needed |
| Multi-cloud, multi-env secrets | ⚠️ | ✅ Better |
| Compliance requires audit trail | ⚠️ | ✅ Better |
| Secrets shared with non-GPG users | ❌ | ✅ Works |
Limitations
- GPG key management is the operational burden — lost keys mean lost access to encrypted files
- No per-file revoke — you can’t revoke one person’s access without regenerating the symmetric key
- Not audited by security teams — enterprise compliance often requires a dedicated secrets manager
- Doesn’t work with GitHub’s web interface — encrypted files can’t be viewed in the browser
For small teams and personal projects, git-crypt is the right balance. For enterprise use, a dedicated secrets manager (Vault, AWS SSM) integrated with your IaC tool is the better path.
There’s a middle option worth knowing about: SOPS encrypts individual values rather than whole files, so encrypted YAML still diffs meaningfully in review. That’s the approach I use for Terragrunt repositories, where seeing which variable changed matters more than hiding the variable names.