▘ soppy wet nix (16MAY2025) ^a
what if that one read
/secrets/dont_leak but it said dont_leak: ENC[AES256_GCM,data:psyelHNBMy+xglw=,iv:UhxfqAqVbCgMRMqRMA1MmvgIO18zTrVtQdFywupZyYA=,tag:Legj+njC3z8jX16n1pZszg==,type:str]
that's cool but what?
let's face it; sometimes stuff is best left to one's own eyes. like auth tokens, passwords, private keys...
most of the time, handling these manually once somewhere, dropping it in a file somewhere on a remote machine or UI and that's the end of it.
but what if that one's like. the type of lazy that doing a lot of work now means not doing the work a second time? that's like this doll.
lets talk about our bestie sops-nix
but first, sops.
sops is a way to automate encryption and decryption of secret data doll might want tightly coupled to a use case.
irl, one might pair this to AWS KMS or another type of distributed keystore; and that's cool. but. this isn't irl. this is a video game, dolly.
instead we'll focus on sops's age encryption method, which is based on ed25519 SSH keys doll already use (right?)
it can hear those thoughts,> aki wtf, this is 3 whole things, this one is getting overwhelmed
yeah real
we're going to store all of our secrets in this sort of tree,
project ├── secrets │ ├── global.yaml │ ├── machine-1 │ │ └── secrets.yaml │ ├── machine-2 │ │ └── secrets.yaml └── .sops.yaml
each machine gets a folder under secrets, and there's a global one for every machine too.
there's also a
.sops.yaml file to mention, to support our needs, we'll do...creation_rules: - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ key_groups: - age: - ??? # for doll - ??? # for machine-1 - ??? # for machine-2 - path_regex: secrets/machine-1/[^/]+\.(yaml|json|env|ini)$ key_groups: - age: - ??? # for doll - ??? # for machine-1 - path_regex: secrets/machine-2/[^/]+\.(yaml|json|env|ini)$ key_groups: - age: - ??? # for doll - ??? # for machine-2
wait. age. that ag- - shh.
hold on ,...
dolly,,, listen. yaml is. very fun. and so lets make it fun.
keys: &all - &op_doll ??? - &m_machine-1 ??? - &m_machine-2 ??? creation_rules: - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ key_groups: - age: *all - path_regex: secrets/machine-1/[^/]+\.(yaml|json|env|ini)$ key_groups: - age: *op_doll - age: *m_machine-1 - path_regex: secrets/machine-2/[^/]+\.(yaml|json|env|ini)$ key_groups: - age: *op_doll - age: *m_machine-2
now this isn't a sops thing, this is a yaml thing. yaml has anchors......
sops doesn't know what that
keys key is, it gracefully ignores stuff it doesn't care about, so we can exploit this!!!this solves three huge problems with sops + age
- which key is machine
age1lq5q5g5qjsdcc3kanyway? - we solve this by naming every key - the "global" group should always be every key... so we have that too
- a secret third problem
ok. back to whatever we had before
how old is one's encryption
age answers that question by letting doll use ed25519 keys to encrypt and decrypt doll's data.
ed25519 would be a good name for a doll!!
since we're deploying to machines with presumably an SSH server running, and presumably the doll also has an SSH client, all of these ends have SSH keys we can use
lets start with the doll end,
$> ssh-to-age -i ~/.ssh/id_ed25519.pub #|> age13c5wv623jxjja5mjz7fajg9qqwvypzgsfqrs4tmk7rpgyzu7aufs4ul9f9
wow that's a string! yay!
we'll plop that in our keys section.
keys: &all - &op_doll age13c5wv623jxjja5mjz7fajg9qqwvypzgsfqrs4tmk7rpgyzu7aufs4ul9f9 - &m_machine-1 ??? - &m_machine-2 ???
sops will automatically use the
~/.ssh/id_ed25519 file, we're good to go!!!the machines,,
so SSH servers also have host keys, so when sops is ran on a specific host, it can also decrypt like the doll can.
we'll fetch machine-1's ssh public keys and slap those in too.
$> ssh-keyscan -qt ed25519 machine-1.local | ssh-to-age #|> machine-1 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOGae8gFqSsfezM/ul7LnCmv9GAk6AIah18+tc212LQW #|> age1jc6ghxfgxe3gx53xa55azxan447cfxaqfqeh5y5yzqapj7mw7ajql8kv02
and yeet into the keys block, and machine-2's too.
keys: &all - &op_doll age13c5wv623jxjja5mjz7fajg9qqwvypzgsfqrs4tmk7rpgyzu7aufs4ul9f9 - &m_machine-1 age1jc6ghxfgxe3gx53xa55azxan447cfxaqfqeh5y5yzqapj7mw7ajql8kv02 - &m_machine-2 age1438lvn7gh4he0rnj0xnvnx56l97mpz0vsv3wktj8utk65kqs8ycqftcxze
so.. lets sops!!!!!!!!!
we did all the hard parts. lets do the secret parts.
$> sops secrets/global.yaml # secret, set EDITOR to something.. # like `EDITOR="code -w"` will open a vscode tab.... (close tab to save)
this usually shows a nice default yaml file, move all that into the shadow realm.,
we wanna store something useful like... an authentication token to a certain doll's multicelluar digital cortex.
doll_token: awawawa
lets save that file, and go inspect it.
doll_token: ENC[AES256_GCM,data:DryFfMa/dQ==,iv:IdqvGEKKTSOaATlqMqyHQ3PEAwC6mJqjHbO3KwfTlHc=,tag:5ByySoYZu6qZexHBmnQBKA==,type:str] sops: age: - recipient: age13c5wv623jxjja5mjz7fajg9qqwvypzgsfqrs4tmk7rpgyzu7aufs4ul9f9 enc: | -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvdTBVb0FvM2xocWFVclJt L05aRmpKOHh6d3hXcE9Vem9SL1hrSjFMRng4CmNrTmROTFVMRlc4QjllWVo1S3lv MXo5UVFuazVoeWQ5TEE1NXJIUzl0K1EKLS0tICtrSTdoWHIrSlJLRTUweWpoVWlY ZTNXNUM1ZEpsQlMzRlNoZHpkU2lhVzAKpYJAfihRyG/ok0tgJg4FjnN8vj6Bz/+z 0Y/P21JJp/SnXq4LjivBCT4XJ0s6XoffUEqw/uxLzsY1wwox003pOA== -----END AGE ENCRYPTED FILE----- - recipient: age1jc6ghxfgxe3gx53xa55azxan447cfxaqfqeh5y5yzqapj7mw7ajql8kv02 enc: | -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGWVUrWmIvOXUzYmJyWm9G dmhNQ0lUNFh5ZTdXMmMxNWJENXlMU1daN3lzCnh5cmQyMFlSWDBQV1VSOCtoTUcv anpmWVFVUmpmZ2svUFpFMUI0YUlwclkKLS0tIFc5NDRQWTdseXdyUE5BbXUwSFk4 VXptdjk3cHFEV0twaXhSYzY2R0JrUzQK5RNS2XdR1m7/SGfpNFh5Z9Q2YGsJT1Cw iJyW+7EseiuWEkFa2JFul6nsP8W1TmDobk2VXiYpc/BTm78hBlUhyg== -----END AGE ENCRYPTED FILE----- - recipient: age1438lvn7gh4he0rnj0xnvnx56l97mpz0vsv3wktj8utk65kqs8ycqftcxze enc: | -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtOUtYUXhOTzJlS2lHUTFz TGwyQ2Y2aEZWWnNmWjVqWWtXeHdmQlFPTFFzCnNSU2FDVFdsTC9hWWV4NEtNQ1FF bDRvOXJiR3hRb1Ryc0VuTkExQkxoMHMKLS0tIE1ETGJZc04yOHJkaTkwaTlyZW1j N1pacGpGZGtnNWdFSjZxbjVuVlRZVzAKjDaMO/oKlq4D1QHTlD9lBY+0w81/gybv 2+BSo93LG+bN+cNI9jYc9FU+t4GdlyhEcKQ4MTczgZyMaMOYkhgKcg== -----END AGE ENCRYPTED FILE-----
sops completely obfuscates this file.
this is now safe to send into git. or even over the internet. or even a website like this one. haha.
really, it is. these are real keys, and good encryption, and hopefully only intended targets can decrypt them. the reader can even send this doll encrypted messages with that op_doll age key, if one wanted.
what's happening is each "recipient" is getting an individual copy of the file encrypted with the recipient's public key.
the only way to get the data back out is to have one of these machine's private keys.
further sopping
we can do
sops secrets/machine-1/secrets.yaml and set up machine-specific secrets too.those won't be able to be decrypted by machine-2, nor vice versa, due to how we set up
.sops.yaml earlier.also, if one happens to rotate SSH keys or. delete/remake a server or somethin', we can ask sops to redo its work with
$> sops updatekeys -y secrets/global.yaml
put in a op_doll_2 as a new key in all the same spots, then run the command abovefinally, remove the old key, and run the above command again.get rotated !
nix sauce
266 or so lines in and we haven't even talked about nix yet.
sops-nix lets us use sops. from nix. incredible.
we use flakes, so we add
github:Mic92/sops-nix as an input, and work it into our machines.{ inputs, ... }: {
imports = [
inputs.sops-nix.nixosModules.sops
];
# lets us shorthand for global secrets
sops.defaultSopsFile = ../path/to/../secrets/global.yaml;
# allows sops-nix to decrypt using machine keys
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
}now say we wanna use that token... there's a few different ways.
reference as a file directly
nix is. weird. strings aren't strings. strings are files. and files are strings. and files are files. and secrets are files...
for example, if we want to set up an openvpn client, doll's provider would give an .ovpn file. toss that in sops as a yaml value (or json or whatever it guess)!! make it sopping. instantly.
our
secrets/machine-1/secrets.yaml: ovpn_file: | client dev tun proto udp remote 41.66.66.66 41666 #...
our
modules/openvpn.nix: { config, ... }: {
# the "ovpn_file" here matches the YAML key above.
sops.secrets.ovpn_file = {
sopsFile = ../secrets/machine-1/secrets.yaml
};
services.openvpn.servers.doll = {
config = ''
config ${config.sops.secrets.ovpn_file.path}
'';
}
}templates!
but wait haha there's usually like a login username and password right?
good catch, lets add those to our
secrets/machine-1/secrets.yaml: ovpn_username: doll ovpn_password: witchesRme4n ovpn_file: | #...
and lets make a template!
{ config, ... }: {
sops.secrets.ovpn_file = {
sopsFile = ../secrets/machine-1/secrets.yaml
};
# our two new friends
sops.secrets.ovpn_username = {
sopsFile = ../secrets/machine-1/secrets.yaml
};
sops.secrets.ovpn_password = {
sopsFile = ../secrets/machine-1/secrets.yaml
};
# this is just how openvpn likes it. no typos.
sops.templates.ovpn_credentials = {
content = ''
${config.sops.placeholder.ovpn_username}
${config.sops.placeholder.ovpn_password}
'';
};
services.openvpn.servers.doll = {
config = ''
config ${config.sops.secrets.ovpn_file.path}
auth-user-pass ${config.sops.templates.ovpn_credentials.path}
'';
}
}wow. it can do everything.
doll might notice the "consumer" side of sops-nix uses
config a lot. yep. its a little self-referential.secrets........ but what about automation
that silly
.sops.yaml third thing it mentioned huh? c:why did we tag the age keys with
op_ and m_? so we can find them. in a script.and then generate the yaml so when one adds a new machine its like one command. :>
we did this with some simple JS script doll could reference. (it runs via bun so no node_modules!!!)
this allows us to run a little
onboard-machine.js 41.66.66.66 machine-1 and pull the right key from a remote machineis doll soppy yet
sure hope so. ▖▘▌▘▘▌▌▌▖▘