Skip to main content
· Security · 4 min read

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 CaseGit-CryptExternal Secrets Manager
Small team, simple workflowOverkill
IaC with tfvars per environmentWorks but adds complexity
Credentials tied to specific filesExtra 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.