-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathvault-push-approles.nix
243 lines (209 loc) · 8.63 KB
/
vault-push-approles.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
# Generate and push approles to vault
{ writeShellScriptBin, writeText, jq, vault, coreutils, bash, lib }:
# Inputs: a flake with `nixosConfigurations`
# Usage:
# apps.x86_64-linux.vault-push-approles = { type = "app"; program = "${pkgs.vault-push-approles self}/bin/vault-push-approles"; }
{ nixosConfigurations ? { }, darwinConfigurations ? { }, ... }: rec {
# Overrideable functions
# Usage examples:
# pkgs.vault-push-approles self { approleCapabilities.aquarius-albali-borgbackup = [ "read" "write" ]; }
/* pkgs.vault-push-approles self (final: prev: {
approleCapabilitiesFor = { approleName, namespace, ... }@params:
if namespace == "albali" then [ "read" "write" ] else prev.approleCapabilitiesFor params;
})
*/
# pkgs.vault-push-approles { } { extraApproles = [ { ... } ] }
type = "derivation";
# `final` contains fixed-point functions after applying user-supplied overrides
overrideable = final: {
# Approles to upload in addition to the ones generated from
# vault-secrets' NixOS module definitions. Must contain all the
# options from top-level vault-secrets option and all the options
# from vault-secrets.secrets.<name> submodule, as well as
# "approleName" attribute
extraApproles = [ ];
# Render an attrset into a JSON file
renderJSON = name: content:
writeText "${name}.json" (builtins.toJSON content);
# Default approle parameters
approleParams = {
secret_id_ttl = "";
token_num_uses = 0;
token_ttl = "20m";
token_max_ttl = "30m";
secret_id_num_uses = 0;
};
# Generate an approle parameters attrset based on its name and other
# options from its secret definition
mkApprole = { approleName, ... }:
(final.approleParams // { token_policies = [ approleName ]; });
# Create a JSON (HCL) file with approle parameters in it from its secret definition
renderApprole = { approleName, ... }@params:
final.renderJSON "approle-${approleName}" (final.mkApprole params);
# An attrset mapping `approleName`s to capabilities required by those approles
approleCapabilities = { };
# Get capabilities for the given secret definition
approleCapabilitiesFor = { approleName, ... }:
final.approleCapabilities.${approleName} or [ "read" ];
# Generate an approle policy from its secret definition
mkPolicy = { approleName, name, vaultPrefix, ... }@params:
let
splitPrefix =
builtins.filter builtins.isString (builtins.split "/" vaultPrefix);
insertAt = lst: index: value:
(lib.lists.take index lst) ++ [ value ] ++ (lib.lists.drop index lst);
makePrefix = value:
builtins.concatStringsSep "/" (insertAt splitPrefix 1 value);
metadataPrefix = makePrefix "metadata";
dataPrefix = makePrefix "+";
in {
path = {
"${metadataPrefix}/${name}/*".capabilities = [ "list" ];
"${dataPrefix}/${name}/*".capabilities =
final.approleCapabilitiesFor params;
};
};
# Create a JSON (HCL) file with the approle's policy from its secret definition
renderPolicy = { approleName, ... }@params:
final.renderJSON "policy-${approleName}" (final.mkPolicy params);
};
__toString = self:
let
# Hooray fix point
final = lib.fix self.overrideable;
inherit (final) renderApprole renderPolicy renderJSON extraApproles;
# The script that writes the approle to the vault server
writeApprole = { approleName, vaultAddress, ... }@params:
let
approle = renderApprole params;
policy = renderPolicy params;
vaultWrite = ''
vault write "auth/approle/role/${approleName}" "@${approle}"
vault policy write "${approleName}" "${policy}"
'';
in ''
export VAULT_ADDR="${vaultAddress}"
${./vault-ensure-token.sh}
write() {
set -x
${vaultWrite}
set +x
}
# Ask the user what to do with the current approle
ask_write() {
if ! [[ "''${VAULT_PUSH_ALL_APPROLES:-}" == "true" ]]; then
read -rsn 1 -p "Write approle ${approleName} to ${vaultAddress}? [(a)ll/(y)es/(d)etails/(s)kip/(q)uit] "
echo
case "$REPLY" in
# Write all the approles including this one
A|a)
VAULT_PUSH_ALL_APPROLES=true
;;
# Write the current approle, ask for the next one
y)
;;
# Show details about this approle, ask about it again
d)
{
echo "* Merged attributes of this approle:"
cat "${renderJSON "merged" params}" | ${jq}/bin/jq .
echo "* Approle JSON (${approle}):"
cat ${approle} | ${jq}/bin/jq .
echo "* Policy JSON (${policy}):"
cat ${policy} | ${jq}/bin/jq
echo "* Will execute the following commands:"
echo '${vaultWrite}'
ask_write
return
} | ''${PAGER:-less}
;;
# Don't write the current approle, ask for the next one
s)
{
echo "* Skipping ${approleName}"
return
}
;;
# Quit
q)
exit 1
;;
*)
echo "* Unrecognized reply: $REPLY. Please try again"
ask_write
return
;;
esac
fi
write
echo
}
if [[ $# -eq 0 ]]; then
# If we don't get any arguments, ask about this approle
ask_write
elif [[ " $@ " =~ " ${approleName} " ]]; then
# If this approle is in the argument list, just upload it
write
fi
'';
# Get all approles for vault-secrets in configuration
approleParamsForMachine = cfg:
let
vs = cfg.config.vault-secrets;
prefix = lib.optionalString (!isNull vs.approlePrefix)
"${vs.approlePrefix}-";
in builtins.attrValues (builtins.mapAttrs (name: secret:
builtins.removeAttrs (vs // secret // {
approleName = "${prefix}${name}";
inherit name;
}) [ "__toString" "secrets" ]) vs.secrets);
# Find all configurations that have vault-secrets defined
configsWithSecrets = lib.filterAttrs (_: cfg:
cfg.config ? vault-secrets && cfg.config.vault-secrets.secrets != { })
(nixosConfigurations // darwinConfigurations);
# Get all approles for all NixOS configurations in the given flake
approleParamsForAllMachines =
builtins.mapAttrs (lib.const approleParamsForMachine)
configsWithSecrets;
# All approles for all NixOS configurations plus the extra approles
allApproleParams =
(builtins.concatLists (builtins.attrValues approleParamsForAllMachines)
++ extraApproles);
# Check whether all the elements in the list are unique
allUnique = lst:
let
allUnique' = builtins.foldl' ({ traversed, result }:
x:
if !result || builtins.elem x traversed then {
inherit traversed;
result = false;
} else {
traversed = traversed ++ [ x ];
result = true;
}) {
traversed = [ ];
result = true; # In an empty list, all elements are unique
};
in (allUnique' lst).result;
# A script to write all approles
writeAllApproles =
assert allUnique (map (x: x.approleName) allApproleParams);
lib.concatMapStringsSep "\n" writeApprole allApproleParams;
in writeShellScriptBin "vault-push-approles" ''
set -euo pipefail
export PATH=$PATH''${PATH:+':'}'${lib.makeBinPath [ jq vault coreutils bash ]}'
${writeAllApproles}
'';
# Allows to ergonomically override `overrideable` values with a simple function application
# Accepts either an attrset with override values, or a function of
# `final` (which will contain the final version of all the overrideable functions)
__functor = self: overrides:
self // {
overrideable = s:
(self.overrideable s) // (if builtins.isFunction overrides then
overrides s (self.overrideable s)
else
overrides);
};
__functionArgs = builtins.mapAttrs (_: _: false) (overrideable { });
}