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:

  1. Keep secrets in a human and/or computer readable format in the repo.
  2. Create keys for specific servers and get developers’ ssh-pubkeys, add to the keys-file.
  3. Use agebox to track and encrypt our secret files - these can now be committed in the codebase.
    • I found agebox easier to use than age when changing/updating the secrets.
  4. Embed the encrypted secret files in the binary itself
  5. Implement age decryption of the embedded files on startup using private keys found on target
  6. 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