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-nix is based on sops, so unfortunately we need to start there.
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 age1lq5q5g5qjsdcc3k anyway? - 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.
important ^noe message
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
protip
put in a op_doll_2 as a new key in all the same spots, then run the command above
finally, 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 machine

is doll soppy yet

sure hope so. ▖▘▌▘▘▌▌▌▖▘