An alternative way of dealing with secrets
I’m experimenting with ways to deploy web services and projects. One part of that has been trying to find a nice way to manage secrets. I’ve disqualified secret managers and vaults since a goal is to have as few external dependencies as possible. Dealing with plaintext files hidden in different paths quickly becomes annoying and it’d be pretty nice if they were encrypted at rest.
The age of age
I started looking at age and agebox - promising a neat way to encrypt/decrypt files
so you can store the secrets in the repo next to the code! Neat.
You can also use Ed25519 keys with agebox
, so devs can get access with their .ssh
keys.
A key feature here is assymetric key encryption: public and private keys.
This lets us encrypt secrets for a specific secret key to decrypt - called “recipients” in the age
docs. agebox
lets you put these in a keys
file for ease of use.
Here’s my grand plan:
- Keep secrets in a human and/or computer readable format in the repo.
- Create keys for specific servers and get developers’ ssh-pubkeys, add to the
keys
-file. - Use
agebox
to track and encrypt our secret files - these can now be committed in the codebase.- I found
agebox
easier to use thanage
when changing/updating the secrets.
- I found
- Embed the encrypted secret files in the binary itself
- Implement
age
decryption of the embedded files on startup using private keys found on target - The secrets are now in the application, and we dont have to fiddle with files/secrets on the target.
The only thing we need on the server is a private key with its pubkey listed in the keys-file!
Building/deploying from CI? No worries! Need devs to access some secrets? No problem!(password or hardware keys for the ssh keys please) Need to move to another server? Just add a new key! Some fancy automated dev/prod secrets? Go crazy - embed both in the same binary, and control who sees what with the keys.
The main issue I can think of with this would be exfiltration of the binary combined with leaking of a server/developers private key. But in a world where the alternative is having developers copy-paste secrets to debug an api or fiddle with copying files between servers, I’d say we’re even steven.
Below is some example code just to demonstrate how my first implementation looked.
An example in go
Setting this up in go could look like this:
type Config struct {
Megasecret string
ApiKey string
}
//go:embed prod.toml.agebox
var prodtomlEncrypted string
And then we use age
to decrypt and then parse the, in this case, .toml
.
prodtoml, err := secrets.DecryptSecret(prodtomlEncrypted)
if err != nil {
log.Errorf("could not decrypt secret", err)
os.Exit(1)
}
_, err = toml.Decode(secret, &config)
if err != nil {
log.Errorf("could not parse config", err)
os.Exit(1)
}
//Do the things we need to do
Configs encrypted until the very moment we need them.
The secret could of course be a .json
, .xml
or whatever you like, as long as you like to parse it, go crazy.
I’ve implemented decryption here, and it seems to be working fine