forked from rattboi/dotfiles
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathsecrets.nix
304 lines (292 loc) · 12 KB
/
secrets.nix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
################################################################################
# Manage secret generators and declare all secrets here.
#
# This makes heavy use of agenix-rekey to declare secrets that are encrypted per
# host by both a master key and host keys. These secrets do not wind up in the
# Nix store though they are manage declaratively.
#
# There are some challenges when a secret is managed or needed at _build_ time
# (as opposed to, for example, a service that reads in a secret when it starts).
# I don't have specific steps for getting around that yet.
#
# To decrypt a secret manually, use `rage --decrypt <file>.age`. You will be
# prompted for the master password's key (which is the 3rd one I've create, "-3"
# as a suffix). I don't know if the .pub file matters or not.
################################################################################
{
flake-inputs,
host-id,
}: { config, pkgs, lib, ... }: let
subject-string = subject:
''/C=${subject.country}\
/ST=${subject.state}\
/L=${subject.location}\
/O=${subject.organization}\
/OU=${subject.organizational-unit}''
;
validate-tls-settings = let
inherit (lib) isAttrs isInt isString;
inherit (lib.trivial) id throwIfNot;
in name: tls:
throwIfNot (isAttrs tls) "Secret '${name}' must have a `tls` attrset."
throwIfNot (isString tls.domain) "Secret '${name}' must have a `tls.domain` string."
throwIfNot (isInt tls.validity) "Secret '${name}' must have a `tls.validity` integer."
validate-tls-subject name tls.subject
id
;
validate-tls-subject = let
inherit (lib) isAttrs isString;
inherit (lib.trivial) id throwIfNot;
in name: subject:
throwIfNot (isAttrs subject) "Secret '${name}' must have a `tls.subject` attrset."
throwIfNot (isString subject.country) "Secret '${name}' must have a `tls.subject.country` string."
throwIfNot (isString subject.state) "Secret '${name}' must have a `tls.subject.state` string."
throwIfNot (isString subject.location) "Secret '${name}' must have a `tls.subject.location` string."
throwIfNot (isString subject.organization) "Secret '${name}' must have a `tls.subject.organization` string."
throwIfNot (isString subject.organizational-unit) "Secret '${name}' must have a `tls.subject.organizational-unit` string."
id
;
in {
nixpkgs.overlays = [
flake-inputs.agenix.overlays.default
# This lets us include the agenix-rekey package.
flake-inputs.agenix-rekey.overlays.default
];
# Grants us the "long-passphrase" generator.
# TODO: Help document pre-existing generators listed here:
# https://github.com/oddlama/agenix-rekey/blob/85df729446fca1b9f22097b03e0ae2427c3246e2/modules/agenix-rekey.nix#L557
age.generators.long-passphrase = {pkgs, ...}:
"${pkgs.xkcdpass}/bin/xkcdpass --numwords=10 --delimiter=' '"
;
# TODO: TLS has a means of allowing certain features as well as how they
# propagate. Allow this to be indicated and document how this is done. For
# specifics on the features themselves, refer to some external documentation.
# This applies to this generator as well as the other, related generators.
age.generators.tls-ca-root = { file, name, pkgs, secret, ... }: let
inherit (lib) isAttrs isString;
inherit (lib.trivial) throwIfNot;
inherit (secret) settings;
in
throwIfNot (isAttrs settings) "Secret '${name}' must have a `settings` attrset."
validate-tls-settings name settings.tls
'' \
set -euo pipefail
${pkgs.openssl}/bin/openssl req \
-new \
-newkey rsa:4096 \
-keyout root.key \
-x509 \
-nodes \
-out "$(dirname "${file}")/${name}.crt" \
-subj "/CN=${settings.tls.domain}${subject-string settings.tls.subject}" \
-days "${toString settings.tls.validity}"
cat root.key
rm root.key
'';
# srl files can created during the leaf certificate creation. The files
# track which serial numbers have been used (for auditing?) so openssl
# won't use the same one twice. This should be checked in.
age.generators.tls-signed-certificate = {
decrypt,
deps,
file,
name,
pkgs,
secret,
...
}: let
inherit (lib) isAttrs isString;
inherit (lib.trivial) throwIfNot;
inherit (secret) settings;
root-cert-dep = (builtins.elemAt deps 0);
in
throwIfNot (isAttrs settings) "Secret '${name}' must have a `settings` attrset."
throwIfNot (isString settings.fqdn) "Secret '${name}' is missing a `fqdn` string."
# throwIfNot (isAttrs settings.root-certificate) "Secret '${name}' is missing a `root-certificate` value."
''
set -euo pipefail
${decrypt} "${root-cert-dep.file}" > ca.key
cert_path="$(dirname "${root-cert-dep.file}")/${root-cert-dep.name}.crt"
out_file="$(dirname "${file}")/$(basename ${name} '.key').crt"
${pkgs.openssl}/bin/openssl req \
-new \
-newkey rsa:4096 \
-sha256 \
-nodes \
-keyout signing.key \
-out signing.crt \
-subj "/CN=${settings.fqdn}${subject-string settings.root-certificate.settings.tls.subject}" \
-addext "subjectAltName = DNS:${settings.fqdn}"
echo "subjectAltName = DNS:${settings.fqdn}" > san.cnf
${pkgs.openssl}/bin/openssl x509 \
-req \
-in signing.crt \
-CA $cert_path \
-CAkey ca.key \
-CAcreateserial \
-out $out_file \
-days 356 \
-extfile san.cnf
# Verify it works!
${pkgs.openssl}/bin/openssl verify \
-CAfile $cert_path \
$out_file \
1>&2
cat signing.key
rm ca.key
rm san.cnf
rm signing.{crt,key}
''
;
# Note that SSHA is the default - a _seeded_ SHA. This doesn't work across
# systems though, which defeats the use as we see it in agenix-rekey. So just
# fall back to SHA. This means that the server needs ` olcPasswordHash =
# "{SHA}";' configured. I can't seem to even get SHA working across systems,
# so I've fallen back to CLEARTEXT until I can figure out what's happening.
# It might be impossible though, since I have no real inputs besides the type
# and the password itself.
#
# I need to do some more reserach but I'm supposed to use decrypt to decrypt
# the actual values, which I wasn't doing with any of the hashes, so that
# might also present an issue I was experiencing.
age.generators.slapd-hashed = {
decrypt,
deps,
file,
name,
pkgs,
secret,
...
}:
# slappasswd doesn't do {CLEARTEXT}, but I think the prefix is still needed.
# It's vitally important that the newline is stripped!!! Otherwise that
# gets added to the base64 encoded value (which you can spot with a Cg or
# Cg== (equals are padding in base64, so you might see more, less, or none).
"${decrypt} ${(lib.escapeShellArg (builtins.elemAt deps 0).file)}"
# ''
# ${pkgs.openldap}/bin/slappasswd \
# -h '{CLEARTEXT}' \
# -s "$(cat ${(builtins.elemAt deps 0).file})"
# ''
;
age.rekey = {
# TODO: This is the host key, and we should call it that instead of the pub
# key. The .pub is the pub key, but we also have a private key and having
# that called the pub-key doesn't make sense. Make sure to capture other
# references, and rename what's already on disk.
hostPubkey = ../secrets/${host-id}-pub-key.pub;
masterIdentities = [
../secrets/agenix-master-key-3.age
];
# Must be relative to the flake.nix file.
localStorageDir = ../secrets/rekeyed/${host-id};
generatedSecretsDir = ../secrets/generated/${host-id};
# These fields are labeled as missing with:
# The option `age.rekey.userFlake' does not exist. Definition values:
# userFlake = flake-inputs.self;
# nodes = flake-inputs.self.nixosConfigurations;
storageMode = "local";
};
age.generators.ssh-ed25519-with-pub = {
file,
lib,
name,
pkgs,
...
}: ''
mkdir -p "$(dirname "${file}")"
(exec 3>&1;
${pkgs.openssh}/bin/ssh-keygen \
-q \
-t ed25519 \
-N "" \
-C ${lib.escapeShellArg "${name}"} \
-f ${name} \
<<<y >/dev/null 2>&1;
cp "${name}.pub" "$(dirname "${file}")"
echo copied public key ${name}.pub to "$(dirname "${file}")" 1>&2
cat "${name}"
rm "${name}"{,.pub}
true)
'';
# TODO: This doesn't quite work as expected. We need something to sync up
# with the /etc/ssh/ssh_host_ed25519_key and its .pub sibling. This will
# break for new hosts trying to get secrets. To fix it, overwrite the .pub
# file in this repository for the host in question with
# /etc/ssh/ssh_host_ed25519_key.pub and run `agenix rekey -a` to rekey the
# files.
# age.secrets."${host-id}-pub-key" = {
# generator.script = "ssh-ed25519-with-pub";
# rekeyFile = ../secrets/${host-id}-pub-key.age;
# };
# This is a catch22. This is needed at build time but isn't available until
# activation time. See bin/nix-host-new in this repository as well as
# bin/nix-host-key-install for putting this file in the right place.
# environment.etc."ssh/ssh_host_ed25519_key".file = config.age.secrets."${host-id}-pub-key".file;
age.secrets.proton-ca = {
rekeyFile = ../secrets/proton-ca.age;
settings = {
tls = {
domain = "proton";
subject = {
country = "US";
state = "Oregon";
location = "Portland";
organization = "Barnett family";
organizational-unit = "IT Department";
};
validity = 365 * 5;
};
};
generator.script = "tls-ca-root";
};
age.secrets.builder-key = {
generator.script = "ssh-ed25519-with-pub";
rekeyFile = ../secrets/builder-key.age;
# This used to be required due to some trouble I was having on nix-darwin
# but no longer (see secretsDir below for information about that).
# path = "/etc/nix/builder-agenix-key";
path = "/etc/nix/remote-builder_ed25519";
};
# If you're here because you can't find /run/agenix, you're probably on
# darwin/macOS and you don't have any host keys in /etc/ssh. You will have to
# generate them for this host (which can be done with `agenix-rekey
# generate`), and then have them copied to the correct location by running
# `nix-host-key-install` on the host in question, with this repository in
# place (which you have to have cloned to make the executable available
# anyways). Then you should find a /run/agenix. Additional notes can be
# found in the nix-host-key-install script.
#
# This value is left to help me find it again when I forget all of this again.
# I have a checklist executable started in nix-host-new.
age.secretsDir = "/run/agenix";
imports = [
# This should work equally for macOS. This and the darwinModules version
# both reference the same file.
flake-inputs.agenix.nixosModules.default
flake-inputs.agenix-rekey.nixosModules.default
];
environment.systemPackages = [
# This should remain out because agenix-rekey brings in a replacement
# agenix.
# flake-inputs.agenix.packages.${system}.default
pkgs.agenix-rekey
# Rage is a Rust based Age that claims a more consistent CLI API.
pkgs.rage
];
# These files are not actually temporary files, especially if the "-" at the
# end is included to keep systemd from cleaning up the directory (at some
# point?). See
# https://discourse.nixos.org/t/is-it-possible-to-declare-a-directory-creation-in-the-nixos-configuration/27846/6
# for Nix usage, and see
# https://www.freedesktop.org/software/systemd/man/latest/tmpfiles.d.html for
# the systemd documentation on the topic.
# Unfortunately, there might be a loading order problem with this and other
# build-time scripts. Can I use a variable for the directory so it is
# resolved before any secrets are used? Or perhaps just some settings in
# age.secretsDir to ensure the directory exists.
# Since this doesn't use a declarative approach, I am opting not to use it.
# systemd.tmpfiles.rules = [
# "d /etc/secrets 0770 nixbld nixbld -"
# ];
}