Secret management for the layman
For a long time, I've been debating myself how to properly handle secrets in a convenient, cheap and reproducible fashion.
I run a small website: reciperium.com (check it out!) and I don't want to depend too much on the cloud.
Most of the available tools have the following problems:
- Hard to setup
- Hard to integrate with your infra
- Hard to communicate / share with teammates (you can give access but they still have to discover the secrets, and build the files)
I've developed a straightforward solution using age
, and git
.
- Secrets are stored as
.age
files in thegit
repo - Supports multiple users
I call this workflow "tracked secrets".
Setup
First thing, prevent your private key from being leaked into git.
For that we add to our .gitignore
the private keys:
*.key
If you use nix, you can set up a development environment with all the dependencies needed. In my case, I use just
and age
for this.
{
description = "My Application";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};å
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
perSystem =
{
config,
self',
inputs',
pkgs,
...
}:
{
devShells.default = pkgs.mkShell {
name = "reciperium";
buildInputs = [
pkgs.age
pkgs.just
];
shellHook = ''
just --list
'';
};
};
};
}
You can also configure direnv, to load the nix dependencies, as soon as you cd
into the directory.
This has been a powerful new method in my development toolkit.
Interface
The generic commands we are gonna define are the following
# Create new key and add it to the recipients
new-key
# Decrypt ALL secrets
decrypt
# Encrypt ALL secrets
encrypt
These commands can be implemented in a Makefile
, justfile
, bash scripts, or any scripting mechanism.
Workflows
With those 3 commands we can support several different workflows:
Initializing my host
The commands to be executed for this workflow should be:
new-key
This should create a me.key
and populate recipients.txt
with the public key.
If working on an existing repository, then I should create a commit, push the changes to git and ask to a colleague to encrypt the secrets again (after doing a pull)
Encrypting all the secrets
The command to be executed:
encrypt
This command can either encrypt everything necessary or it can be split into multiple commands internally.
It should encrypt with all the recipients present in recipients.txt
.
Decrypting all secrets
decrypt
It should decrypt everything in the right place. And the user is ready to work!
Creating a secret for a CI
One of the cool benefits of this approach, is that we can integrate it with our CI.
new-key github
encrypt
That should generate a github.key
, and it should populate the recipients.txt
. You can upload the github.key
as a secret and your pipelines are ready to use it!
The second command encrypts everything again.
Implementation example
Let's say we have a repository with our infrastructure. It has 2 folders, one with terraform code. And another with a docker-compose, which gets deployed to the server using docker-swarm.
app
├── infra-tf
│ ├── main.tf
│ └── terraform.tfvars
└── stack
├── .env
└── docker-compose.yaml
The secrets of our application are terraform.tfvars
and .env
.
As I said, I've been using just
for managing the commands, but you can use shell scripts or make
.
Let's take a look at the justfile
:
# Create a new private key and store public in recipients.txt
sec__new_key name="me":
test -f "{{name}}.key" || age-keygen -o "{{name}}.key" && age-keygen -y "{{name}}.key" >> recipients.txt
# Decrypt ALL secrets
sec__decrypt:
age -d -i me.key stack/stack.env.tar.gz.age | tar xvz
age -d -i me.key infra-tf/infra-tf.tar.gz.age | tar xvz
# Encrypt ALL secrets
sec__encrypt: _sec__encrypt__infra _sec__encrypt__stack
# Encrypt terraform.tfvars
_sec__encrypt__infra:
tar cvz \
infra-tf/terraform.tfvars | \
age -R recipients.txt > infra-tf/infra-tf.tar.gz.age
# Encrypt the stack environment files
_sec__encrypt__stack:
tar cvz \
stack/.env \
age -R recipients.txt > stack/stack.env.tar.gz.age
Okay, let's recap. For creating a private key for your current host:
just sec__new_key
The "new key" command is self-service, a user pulls the repo, runs the just sec__new_key
,
and then they push. Another user, with access to the secrets, runs the encrypt again. And finally, the original
user pulls the latest commits, so they can use decrypt locally.
For encryption, we run:
just sec__encrypt
And to decrypt:
just sec__decrypt
Finally, we want to create a secret for our CI
just sec__encrypt github
We can also on-board other users, but it involves sending the private key to them, which can be cumbersome, the recommendation is to let people onboard themselves, and they are responsible for their private key.
just sec__encrypt lara
just sec__encrypt jon
What to do with the private key?
After running just sec__new_key
, we have a me.key
. You can store a copy in your secret manager.
I personally use gopass
, but there are more options, e.g: 1password
, lastpass
or bitwarden
.
Potential improvements
How can we handle different environments?
One option is to have a secrets
folder, with the different environments there.
secrets/
├── dev/
├── preview/
└── prod/
And we could make just accept different parameters when calling the commands:
just sec__encrypt prod
Final comments
Even though, I've seen a lot of people share their nixos configuration publicly, including their encrypted secrets, I still wonder if it's safe, and if so, why is it not used widely? I want to believe it's safe, and I know that if the secrets get leaked, I'll have to update them all. The plus side, is that you know where they are, and you can probably write comments on how to retrieve or generate them again, because it's just files.
What do you think of this approach?
Thanks for reading