GPG Has Been Out Here Making Grown Engineers Cry
You’ve been there. You need to encrypt a file. You type gpg --help and get back four scrolling screens of flags, subcommands, and options that were clearly designed by someone who genuinely enjoys pain. You spend 45 minutes trying to figure out why your keyring isn’t found, why the agent isn’t running, why the web of trust is rejecting your own key, and why the passphrase prompt appeared in a terminal you can’t see.
Then you discover that your colleague can’t decrypt your file because you used their email address instead of their key fingerprint, or because their key expired three years ago and they never updated it, or because the keyserver returned 500 for the fourth time today.
GPG has been the de facto standard for file encryption since the late 90s. It’s also been a baroque nightmare to operate for roughly the same amount of time. It works. It’s everywhere. It’ll outlive us all. And if you need OpenPGP compatibility — email encryption, git commit signing, talking to other GPG users — you still need it.
But if you just need to encrypt a file, there’s a better way now. It’s called age, it was built by Filippo Valsorda (ex-Go cryptography team lead, current maintainer of the Go standard library’s crypto/ packages), and it will make you feel like a person again.
What age Actually Is
age (pronounced like the English word, not an acronym) is a simple, modern file encryption tool. That’s it. No key servers. No web of trust. No subkeys. No --armor vs binary mode confusion. No agent daemon you need to coax awake.
Under the hood it uses X25519 for key exchange and ChaCha20-Poly1305 for encryption. For passphrase-based encryption it uses scrypt for key derivation. These are modern, well-audited primitives. Compare that to GPG’s default of RSA-2048 with SHA-1 HMAC in some configurations, depending on which decade your key was generated.
The CLI has two main operations: encrypt and decrypt. That’s intentional.
There’s also rage, a Rust port of age that’s fully compatible — same file format, same key format, same behavior. You can use either interchangeably. If you’re already deep in the Rust ecosystem or just want a single statically-linked binary, rage is your friend.
Installing Both
# age — Go binary, available in most distros# Arch/CachyOSsudo pacman -S age
# Debian/Ubuntusudo apt install age
# macOSbrew install age
# Direct from releases (any platform)# https://github.com/FiloSottile/age/releases# rage — Rust port, same wire formatcargo install rage
# Or from releases:# https://github.com/str4d/rage/releasesVerify:
age --version# age v1.2.0
rage --version# rage 0.10.0Both ship age-keygen (or rage-keygen) for generating identity files. The key format is identical.
Generating an Identity
In GPG you generate a keypair and it goes into a keyring managed by gpg-agent. In age, your identity is just a text file containing a private key. You own it. You back it up. You treat it like a secret. No daemon, no keyring, no GNUPGHOME nonsense.
age-keygen -o ~/.config/age/identity.txt# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8pThe public key is printed to stdout. The private key goes in the file. The public key also lives in the file as a comment line, so you don’t need to track them separately.
cat ~/.config/age/identity.txt# created: 2026-05-26T08:00:00Z# public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8pAGE-SECRET-KEY-1...Keep this file private. Back it up to somewhere offline. Done. That’s the entire key management story.
Encrypting and Decrypting Files
Encrypt to a recipient’s public key:
# Encrypt a file for a specific recipientage -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p secrets.env > secrets.env.age
# Encrypt to multiple recipients (any one of them can decrypt)age \ -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \ -r age1lggyhqjf7pfj8xz6pq3e8jynm5qmyz2pvadl2u7yrqvmvs86yf8sctg7pw \ secrets.env > secrets.env.ageDecrypt:
age -d -i ~/.config/age/identity.txt secrets.env.age > secrets.envPassphrase-based (no identity file needed):
# Encrypt with passphraseage -p secrets.env > secrets.env.age# Enter passphrase: [scrypt-derived, no weak KDF here]
# Decrypt passphrase-encrypted fileage -d secrets.env.age > secrets.env# Enter passphrase:Pipe-friendly from the start:
# Encrypt stdinecho "super secret token" | age -r age1ql3z7... > token.age
# Decrypt to stdoutage -d -i ~/.config/age/identity.txt token.age# super secret tokenThat last one. Just… works. No --batch flag, no --no-tty dance, no explaining to your CI pipeline why it can’t find a TTY. It just pipes.
The SSH Key Reuse Trick
This is genuinely clever. age can encrypt directly to an SSH ed25519 or RSA public key. If you already distribute SSH keys via GitHub or have them in ~/.ssh/authorized_keys, you can use those as age recipients without generating any new keys.
# Encrypt to a GitHub user's SSH keysage -R <(curl -s https://github.com/username.keys) secrets.env > secrets.env.age
# Encrypt to a local SSH public keyage -r "$(cat ~/.ssh/id_ed25519.pub)" secrets.env > secrets.env.ageDecrypt with the corresponding SSH private key:
age -d -i ~/.ssh/id_ed25519 secrets.env.age > secrets.envYour SSH key is already backed up, already trusted, already distributed to the places you care about. For team scenarios where everyone has their SSH pubkeys on GitHub anyway, this is a zero-overhead key distribution model.
sops + age: Secret Management for Real Workflows
Encrypting individual files is useful, but the real power move for dotfiles, Ansible vaults, or Kubernetes secrets is sops (Secrets OPerationS) with age as the backend.
sops encrypts only the values in a YAML/JSON/ENV file, leaving the keys readable. This means your secrets files are still diffable in git, reviewable in PRs, and understandable to anyone who needs to know the structure without needing the actual secrets.
Install sops:
# Arch/CachyOSsudo pacman -S sops
# Debian/Ubuntu / direct download# https://github.com/getsops/sops/releasessudo apt install sops # or download binary
# macOSbrew install sopsConfigure sops to use your age identity. Create .sops.yaml at the root of your repo:
creation_rules: - path_regex: secrets/.*\.yaml$ age: >- age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p, age1lggyhqjf7pfj8xz6pq3e8jynm5qmyz2pvadl2u7yrqvmvs86yf8sctg7pwNow encrypt a secrets file:
# Set identity for decryptionexport SOPS_AGE_KEY_FILE=~/.config/age/identity.txt
# Create and encrypt a new filesops secrets/app.yaml# Opens $EDITOR — fill in your values, save, sops encrypts on write
# Or encrypt an existing plaintext filesops -e -i secrets/app.yamlThe encrypted file looks like this (structure visible, values encrypted):
database_url: ENC[AES256_GCM,data:abc123...,type:str]api_key: ENC[AES256_GCM,data:xyz456...,type:str]sops: age: - recipient: age1ql3z7hjy... enc: | -----BEGIN AGE ENCRYPTED FILE----- ... lastmodified: "2026-05-26T08:00:00Z" version: 3.9.0Decrypt for use:
# Decrypt to stdoutsops -d secrets/app.yaml
# Edit in place (encrypts on save)sops secrets/app.yaml
# Use in a scriptexport DATABASE_URL=$(sops -d secrets/app.yaml | yq '.database_url')In Ansible, sops integrates via the community.sops collection. You point Ansible at your identity file and it handles decryption transparently during playbook runs. No more Ansible Vault passwords floating around in CI environment variables.
GPG Is Still Winning in Some Places
Honest take time. age does not replace GPG for everything.
GPG wins when:
- Email encryption (OpenPGP/S-MIME): Thunderbird, ProtonMail, email clients generally. The standard is OpenPGP.
agehas nothing to do with email. - Git commit signing:
git config user.signingKeyexpects a GPG key. Yes, there’s SSH signing now (git config gpg.format ssh), but if you’re talking to a team that verifies GPG signatures, you need GPG. - Package signing: RPM, Debian packages, APT repo signatures — all OpenPGP. The distro infrastructure is built on GPG.
- Talking to other GPG users: If someone sends you a GPG-encrypted message, you need GPG to read it.
ageis not compatible with OpenPGP. - Long-term archive signing: Notarizing documents with a timestamped signature that needs to be verifiable decades from now. OpenPGP has an established audit trail and tooling ecosystem for this.
age wins when:
- Encrypting files, secrets, configs, tokens
- Sharing secrets with teammates (send them your age pubkey once, done)
- CI/CD secret management with sops
- Dotfile encryption
- Any situation where you control both ends of the encryption/decryption
- Anywhere you’d have reached for
gpg --symmetricand then immediately regretted it
The framing isn’t “replace GPG everywhere” — it’s “stop using GPG for the 80% of your encryption use cases that don’t need OpenPGP compatibility.”
A Note on Key Management (Or The Lack Of It)
The part that makes age feel almost suspicious is how little ceremony there is. No expiry dates. No subkeys. No signing keys vs encryption keys. No key servers. No web of trust. No revocation certificates you need to publish and hope someone fetches.
Your identity is a file. You protect it the same way you protect your SSH private key. If you lose it, your encrypted data is gone — just like with any encryption system, honestly, though GPG does a good job of making this feel extra scary.
For team key distribution, the pattern is: share age public keys the same way you share SSH public keys. Put them in your dotfiles repo. Share them in Slack. Post them on your GitHub profile. They’re public keys. You don’t need a keyserver because they’re just 64-character strings.
If you want revocation, stop using the old key and rotate to a new one. Re-encrypt your secrets to the new key. That’s the whole story.
Quick Reference
# Generate identityage-keygen -o ~/.config/age/identity.txt
# Encrypt to recipientage -r <pubkey> file.txt > file.txt.age
# Encrypt to SSH pubkeyage -r "$(cat ~/.ssh/id_ed25519.pub)" file.txt > file.txt.age
# Encrypt to multiple recipientsage -r <pubkey1> -r <pubkey2> file.txt > file.txt.age
# Encrypt with passphraseage -p file.txt > file.txt.age
# Decryptage -d -i ~/.config/age/identity.txt file.txt.age > file.txt
# Encrypt from GitHub SSH keysage -R <(curl -s https://github.com/username.keys) file.txt > file.txt.age
# sops: encrypt file in placeSOPS_AGE_KEY_FILE=~/.config/age/identity.txt sops -e -i secrets.yaml
# sops: decrypt to stdoutSOPS_AGE_KEY_FILE=~/.config/age/identity.txt sops -d secrets.yamlThe Decision Rule
If someone says “OpenPGP” or “PGP” or “email encryption” or “git signing” — use GPG.
If someone says “encrypt this file” or “store this secret” or “share this token with my team” — use age.
Your 2 AM self dealing with an expired GPG key in a deployment pipeline will appreciate having made this distinction earlier.
Install age. Generate an identity. Add it to your dotfiles. Set up .sops.yaml in your secrets repo. Then only open gpg --help when you genuinely need OpenPGP, which, honestly, is way less often than you think.