Skip to content
Go back

age vs GPG: Modern File Encryption That Doesn't Make You Cry

By SumGuy 9 min read
age vs GPG: Modern File Encryption That Doesn't Make You Cry

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

Terminal window
# age — Go binary, available in most distros
# Arch/CachyOS
sudo pacman -S age
# Debian/Ubuntu
sudo apt install age
# macOS
brew install age
# Direct from releases (any platform)
# https://github.com/FiloSottile/age/releases
Terminal window
# rage — Rust port, same wire format
cargo install rage
# Or from releases:
# https://github.com/str4d/rage/releases

Verify:

Terminal window
age --version
# age v1.2.0
rage --version
# rage 0.10.0

Both 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.

Terminal window
age-keygen -o ~/.config/age/identity.txt
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

The 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.

Terminal window
cat ~/.config/age/identity.txt
# created: 2026-05-26T08:00:00Z
# public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
AGE-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:

Terminal window
# Encrypt a file for a specific recipient
age -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.age

Decrypt:

Terminal window
age -d -i ~/.config/age/identity.txt secrets.env.age > secrets.env

Passphrase-based (no identity file needed):

Terminal window
# Encrypt with passphrase
age -p secrets.env > secrets.env.age
# Enter passphrase: [scrypt-derived, no weak KDF here]
# Decrypt passphrase-encrypted file
age -d secrets.env.age > secrets.env
# Enter passphrase:

Pipe-friendly from the start:

Terminal window
# Encrypt stdin
echo "super secret token" | age -r age1ql3z7... > token.age
# Decrypt to stdout
age -d -i ~/.config/age/identity.txt token.age
# super secret token

That 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.

Terminal window
# Encrypt to a GitHub user's SSH keys
age -R <(curl -s https://github.com/username.keys) secrets.env > secrets.env.age
# Encrypt to a local SSH public key
age -r "$(cat ~/.ssh/id_ed25519.pub)" secrets.env > secrets.env.age

Decrypt with the corresponding SSH private key:

Terminal window
age -d -i ~/.ssh/id_ed25519 secrets.env.age > secrets.env

Your 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:

Terminal window
# Arch/CachyOS
sudo pacman -S sops
# Debian/Ubuntu / direct download
# https://github.com/getsops/sops/releases
sudo apt install sops # or download binary
# macOS
brew install sops

Configure sops to use your age identity. Create .sops.yaml at the root of your repo:

.sops.yaml
creation_rules:
- path_regex: secrets/.*\.yaml$
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
age1lggyhqjf7pfj8xz6pq3e8jynm5qmyz2pvadl2u7yrqvmvs86yf8sctg7pw

Now encrypt a secrets file:

Terminal window
# Set identity for decryption
export SOPS_AGE_KEY_FILE=~/.config/age/identity.txt
# Create and encrypt a new file
sops secrets/app.yaml
# Opens $EDITOR — fill in your values, save, sops encrypts on write
# Or encrypt an existing plaintext file
sops -e -i secrets/app.yaml

The encrypted file looks like this (structure visible, values encrypted):

secrets/app.yaml (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.0

Decrypt for use:

Terminal window
# Decrypt to stdout
sops -d secrets/app.yaml
# Edit in place (encrypts on save)
sops secrets/app.yaml
# Use in a script
export 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:

age wins when:

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

Terminal window
# Generate identity
age-keygen -o ~/.config/age/identity.txt
# Encrypt to recipient
age -r <pubkey> file.txt > file.txt.age
# Encrypt to SSH pubkey
age -r "$(cat ~/.ssh/id_ed25519.pub)" file.txt > file.txt.age
# Encrypt to multiple recipients
age -r <pubkey1> -r <pubkey2> file.txt > file.txt.age
# Encrypt with passphrase
age -p file.txt > file.txt.age
# Decrypt
age -d -i ~/.config/age/identity.txt file.txt.age > file.txt
# Encrypt from GitHub SSH keys
age -R <(curl -s https://github.com/username.keys) file.txt > file.txt.age
# sops: encrypt file in place
SOPS_AGE_KEY_FILE=~/.config/age/identity.txt sops -e -i secrets.yaml
# sops: decrypt to stdout
SOPS_AGE_KEY_FILE=~/.config/age/identity.txt sops -d secrets.yaml

The 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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Nerdctl vs Docker CLI
Next Post
OpenTelemetry Collector: One Pipeline to Rule Them All

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts