nixfiles/modules/nixos/secretsLib/lib.nix

354 lines
10 KiB
Nix

{
pkgs,
lib,
...
}: let
inherit (builtins) attrNames hasAttr isString toFile;
inherit (lib.lists) forEach unique flatten filter;
inherit (lib.strings) concatStringsSep optionalString;
inherit (lib.attrsets) mapAttrsToList filterAttrs;
inherit (pkgs) writeShellApplication;
genScripts = cfg: let
scriptBase = ''
set -e -o pipefail
${optionalString cfg.debug "set -x"}
set +u
# If sysroot is set then make sure it has trailing /
if [ -n "$SYSROOT" ]; then
if ! (echo "$SYSROOT" | grep -q "/$"); then
SYSROOT="$SYSROOT/"
fi
fi
# If sysroot is empty then make sure it is empty so it doesn't error
[ -z "$SYSROOT" ] && SYSROOT=
set -u
if [ -n "$SYSROOT" ]; then
echo "Using sysroot: $SYSROOT"
fi
${optionalString cfg.createSecretsDir ''
if [ ! -d "$SYSROOT${cfg.secretsDir}" ]; then
mkdir -p "$SYSROOT${cfg.secretsDir}"
chown "${userOrMappedID cfg.secretsDirUser}:${groupOrMappedID cfg.secretsDirGroup}" "$SYSROOT${cfg.secretsDir}"
fi
userLookupFailed=false
${concatStringsSep "\n" (forEach allUsersNotMappedToUID (user: ''
if ! getent passwd ${user} >/dev/null; then
echo "User ${user} could not be found on the host system"
userLookupFailed=true
fi
''))}
groupLookupFailed=false
${concatStringsSep "\n" (forEach allGroupsNotMappedToGID (group: ''
if ! getent group ${group} >/dev/null; then
echo "Group ${group} could not be found on the host system"
groupLookupFailed=true
fi
''))}
if $userLookupFailed; then
echo "Please add mappings in uidMap in order for this script to work"
fi
if $groupLookupFailed; then
echo "Please add mappings in gidMap in order for this script to work"
fi
if $userLookupFailed ̣ || $groupLookupFailed; then
exit 1
fi
''}
'';
allUsers = unique ([cfg.secretsDirUser]
++ flatten (
forEach (attrNames cfg.secrets) (name: cfg.secrets.${name}.user)
));
allGroups = unique ([cfg.secretsDirGroup]
++ flatten (
forEach (attrNames cfg.secrets) (name: cfg.secrets.${name}.group)
));
allUsersByName = filter isString allUsers;
allGroupsByName = filter isString allGroups;
allUsersNotMappedToUID = filter (name: !(hasAttr name cfg.uidMap)) allUsersByName;
allGroupsNotMappedToGID = filter (name: !(hasAttr name cfg.gidMap)) allGroupsByName;
isUserMapped = name: (hasAttr name cfg.uidMap);
isGroupMapped = name: (hasAttr name cfg.gidMap);
userOrMappedID = user:
if (isString user && (hasAttr user cfg.uidMap))
then (toString cfg.uidMap.${user})
else toString user;
groupOrMappedID = group:
if (isString group && (hasAttr group cfg.gidMap))
then (toString cfg.gidMap.${group})
else toString group;
manualSecrets = filterAttrs (_: secret: secret.manual) cfg.secrets;
nonManualSecrets = filterAttrs (_: secret: !secret.manual) cfg.secrets;
in {
initScript =
''
${scriptBase}
VAULT_ADDR_DEFAULT="${cfg.vaultURL}"
set +u
[ -z "$VAULT_ADDR" ] && export VAULT_ADDR="$VAULT_ADDR_DEFAULT"
set -u
kv_get() {
vault kv get -format json "$1"
}
simple_get() {
kv_get "$1" | jq ".data.data$2" -r
}
${cfg.extraFunctions}
''
+ (concatStringsSep "\n" (mapAttrsToList (_name: secret: let
secretPath = secret.path;
secretUser = userOrMappedID secret.user;
secretGroup = groupOrMappedID secret.group;
secretPermissions = secret.permissions;
in ''
if [[ ! -f "$SYSROOT${secretPath}" ]]; then
echo "Initializing Secret ${secretPath}"
else
echo "Updating Secret ${secretPath}"
fi
secretFile="$SYSROOT${secretPath}"
${secret.fetchScript}
chown ${secretUser}:${secretGroup} "$SYSROOT${secretPath}"
chmod ${secretPermissions} "$SYSROOT${secretPath}"
'')
nonManualSecrets))
+ (concatStringsSep "\n" (mapAttrsToList
(_name: secret: let
secretPath = secret.path;
secretUser = userOrMappedID secret.user;
secretGroup = groupOrMappedID secret.group;
secretPermissions = secret.permissions;
in ''
if [[ ! -f "$SYSROOT${secretPath}" ]]; then
echo "Manual Secret ${secretPath} Doesn't Exist; Please add before continuing"
exit 1
fi
echo "Updating Permissions on Manual Secret ${secretPath}"
chown ${secretUser}:${secretGroup} "$SYSROOT${secretPath}"
chmod ${secretPermissions} "$SYSROOT${secretPath}"
'')
manualSecrets))
+ ''
echo "Secrets Deployed"
'';
checkScript =
''
${scriptBase}
getUser() {
stat --format "%U" "$1" 2>/dev/null
}
getUserID() {
stat --format "%u" "$1" 2>/dev/null
}
getGroup() {
stat --format "%G" "$1" 2>/dev/null
}
getGroupID() {
stat --format "%g" "$1" 2>/dev/null
}
userNameMatches() {
[[ "$(getUser "$1")" == "$2" ]]
}
userIDMatches() {
[[ "$(getUserID "$1")" == "$2" ]]
}
groupNameMatches() {
[[ "$(getGroup "$1")" == "$2" ]]
}
groupIDMatches() {
[[ "$(getGroupID "$1")" == "$2" ]]
}
getPermissions() {
stat --format "%a" "$1" 2>/dev/null
}
emojiTick=""
emojiCross=""
${cfg.extraCheckFunctions}
GLOBAL_FAIL=false
''
+ (concatStringsSep "\n" (mapAttrsToList (_name: secret: let
secretPath = secret.path;
secretUser = secret.user;
secretUserMaybeMapped = userOrMappedID secretUser;
secretGroup = secret.group;
secretGroupMaybeMapped = groupOrMappedID secretGroup;
secretPermissions = secret.permissions;
userCheck =
if (isString secretUser && !isUserMapped secretUser)
then "userNameMatches \"${secretPath}\" ${secretUser}"
else "userIDMatches \"${secretPath}\" ${secretUserMaybeMapped}";
groupCheck =
if (isString secretGroup && !isGroupMapped secretGroup)
then "groupNameMatches \"${secretPath}\" ${secretGroup}"
else "groupIDMatches \"${secretPath}\" ${secretGroupMaybeMapped}";
in ''
LOCAL_FAIL=false
echo "Checking ${secretPath}"
# some variables which can be used by checkScript
# shellcheck disable=SC2034
secretFile="$SYSROOT${secretPath}"
if [[ -f "$SYSROOT${secretPath}" ]]; then
echo "$emojiTick File Exists"
else
echo "$emojiCross File Does Not Exist"
LOCAL_FAIL=true
fi
if getUserID "$SYSROOT${secretPath}" >/dev/null && ${userCheck}; then
echo "$emojiTick File Is Owned By Correct User"
else
echo "$emojiCross File Is Not Owned By Correct User (${toString secretUser})"
LOCAL_FAIL=true
fi
if getGroupID "$SYSROOT${secretPath}" >/dev/null && ${groupCheck}; then
echo "$emojiTick File Is Owned By Correct Group"
else
echo "$emojiCross File Is Not Owned By Correct Group (${toString secretGroup})"
LOCAL_FAIL=true
fi
if getPermissions "$SYSROOT${secretPath}" >/dev/null && [[ "$(getPermissions "$SYSROOT${secretPath}")" -eq "${secretPermissions}" ]]; then
echo "$emojiTick File Has Correct Permissions"
else
echo "$emojiCross File Does Not Have Correct Permissions (${secretPermissions})"
LOCAL_FAIL=true
fi
${optionalString (secret.checkScript != null) secret.checkScript}
if [[ "$LOCAL_FAIL" == "true" ]]; then
echo "$emojiCross File Did Not Pass The Vibe Check"
GLOBAL_FAIL=true
else
echo "$emojiTick File Passed The Vibe Check"
fi
echo
'')
cfg.secrets))
+ ''
if [[ "$GLOBAL_FAIL" == "true" ]]; then
echo "$emojiCross One Or More Secrets Did Not Pass The Vibe Check"
exit 1
else
echo "$emojiTick All Secrets Passed The Vibe Check"
fi
'';
};
defaultPackages = with pkgs; [vault jq];
in rec {
mkVaultLoginScript = cfg:
writeShellApplication {
name = "vault-login";
runtimeInputs = with pkgs; [
vault
getent
];
text = let
vaultLoginConfig = cfg.vaultLogin;
in ''
VAULT_ADDR="${vaultLoginConfig.vaultURL}" \
vault login -no-print -method=userpass \
username=${vaultLoginConfig.loginUsername} \
password="$(cat ${vaultLoginConfig.loginPasswordFile})"
'';
};
mkSecretsInitScript = cfg: mkSecretsInitScriptWithName cfg null;
mkSecretsInitScriptWithName = cfg: name: let
scriptName =
if name == null
then "secrets-init"
else "secrets-init-${name}";
scripts = genScripts cfg;
in
writeShellApplication {
name = scriptName;
runtimeInputs = defaultPackages ++ cfg.packages;
text = scripts.initScript;
};
mkSecretsCheckScript = cfg: mkSecretsCheckScriptWithName cfg null;
mkSecretsCheckScriptWithName = cfg: name: let
scriptName =
if name == null
then "secrets-check"
else "secrets-check-${name}";
scripts = genScripts cfg;
in
writeShellApplication {
name = scriptName;
runtimeInputs = defaultPackages ++ cfg.checkPackages;
text = scripts.checkScript;
};
genVaultPolicy = cfg: name: let
inherit (cfg) requiredVaultPaths;
policies = forEach requiredVaultPaths (policyConfig: let
path =
if isString policyConfig
then policyConfig
else policyConfig.path;
capabilities =
if isString policyConfig
then ["read" "list"]
else policyConfig.capabilities;
escapeString = str: "\"" + str + "\"";
in ''
path "${path}" {
capabilities = [${concatStringsSep "," (forEach capabilities escapeString)}]
}
'');
in
toFile "vault-policy-${name}.hcl" ''
${concatStringsSep "\n" policies}
'';
}