Refactor Secrets Management #3
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Per #2
For each stack in
homelab/fighter/config/*
, we want to use compose secrets instead of opaquely injecting the contents of*_secrets.env
into each container's environment.Procedure
For each stack,
secrets.env
file:services > myapp > secrets
configuration block:services > myapp > environment
block for secrets:services > myapp > environment
block for non-secrets:secrets:
block:Put it all together to a refactored
docker-compose.yml
Populate our
./secrets/
directory:Ah, I recall why this solution kinda sucks.
Compose doesn't source the secret files. It only mounts files.
So when we say
POSTGRES_USER: /run/secrets/postgres_POSTGRES_USER
it sets the variablePOSTGRES_USER
to the literal string/run/secrets/postgres_POSTGRES_USER
. Not very helpful.It seems that the standard approach to exporting secrets files to the container environment at startup is to inject a chained entrypoint script to read the
/run/secrets/
directory and source each secret.https://stackoverflow.com/questions/48094850/docker-stack-setting-environment-variable-from-secrets
We'll implement it for Keycloak to get things working and see how ugly it is.
Alright, I think we can use SOPS+age to store our secrets in the codebase, and use some scripting/automation to ease the burden of the encryption step.
This little command is pretty helpful for validating the compose file:
(export $(sops --decrypt --age ${SOPS_AGE_RECIPIENTS} secrets.enc.env | xargs); docker compose config)
And this is what we ran to bring up the stack:
export $(sops --decrypt --age ${SOPS_AGE_RECIPIENTS} secrets.enc.env | xargs); docker compose up -d
For these to work, we need the following environment variables set for SOPS to use:
SOPS_AGE_RECIPIENTS
should be a comma-delimited list of pubkeys.SOPS_AGE_KEY_FILE
should be a path to the key file (e.g.~/.age/key
)Definitely have more work to do on the workflow.
Workflow still under development, but I think we're getting close to a simple, secure, system.
.age-author-pubkeys
file with a comma-separated list of pubkeys by whom all secrets should be decryptable. For now, that's one author (me)..age-pubkey
file at the root of that host's subdirectory (e.g.homelab/fighter/.age-pubkey
).SOPS_AGE_RECIPIENTS
variable from the authors key list, and the pubkey of the host to which the secret belongs.We still need to implement automation for this workflow with git filters, and script the setup process.
New procedure:
export secrets.env; docker compose config
to validate the stack.export secrets.env; docker compose up -d
to bring up the stack.We've run into a snag with our SOPS+age Git filter solution: the encrypted files are non-deterministic.
Everything works great when we write a secret in plaintext on our dev machine, git stage (and automatically encrypt), commit, and push it to the repo. It's stored in an encrypted state in our repo. Awesome.
And decrypting again on the server works great. We see only the decrypted file stored in plain text on the disk. No manual intervention required to decrypt the secrets.
However, our file is permanently "modified" on the server because it compares the two encrypted versions of the file (source encrypted by my private key, local encrypted by the server's private key) and will always see differences between them, as they do not result in the same encrypted file content.
It may make sense to switch to another tool, such as git-crypt
git-crypt seems to only support GPG-based keys. A unique pain for my environment where the GPG agent seems to have gone senile, forcing me to delete a stale lockfile to perform any action.
Would much prefer a symmetric-key based approach.
Edit: oh, git-crypt does support symmetric-key encryption/decryption. We'll review that workflow.
Yeah this is a thousand times simpler. (Albeit with the limitations that entails).
scp .git-crypt.key <user>@<host>:/path/to/repo/.git-crypt.key
git-crypt unlock .git-crypt.key
That's the whole thing.
The process is so seamless than it can be difficult to verify that the committed files are, in fact, encrypted.
To verify that our staged files are encrypted, we can temporarily disable the git-crypt diff filter.
git -c diff.git-crypt.textconv=false diff --cached
Where:
git -c
tells git to run with the following configure key-value overridding the default (as defined in the repo's.git/config
diff.git-crypt.textconv=false
Disables our diff text conversion filter from git-cryptdiff --cached
compares our staged files to the previous commit.I'm pretty happy with where we're at. I suspect that the setup process has some friction, but we'll cross that bridge when we get to it.
As is, our workflow for secrets:
.gitattributes
patterns is checked in, it gets encrypted against the list of age recipients defined in.sops.yaml
, requiring keys from any two of the four groups to decrypt.sops
Git filter runs the.sops/encrypt-filter.sh
script against each matched file before a secret is pushed to the remote.sops
Git filter runs the.sops/decrypt-filter.sh
script against each matched file after a secret is checked out locally.