auth machine

This commit is contained in:
plasmagoat 2025-07-16 02:10:31 +02:00
parent 98dce86882
commit 851a9e18db
34 changed files with 2383 additions and 99 deletions

View file

@ -18,7 +18,7 @@
sops = {
age.keyFile = "/etc/sops/age.key";
defaultSopsFile = ../../secrets/secrets.yml;
defaultSopsFile = ../../secrets/secrets.yaml;
};
# home-manager = {

151
machines/auth/authelia.nix Normal file
View file

@ -0,0 +1,151 @@
{
config,
lib,
...
}: let
authelia = "authelia-procopius";
in {
networking.firewall.allowedTCPPorts = [
9091
];
services = {
authelia.instances.procopius = {
enable = true;
settings = {
theme = "auto";
authentication_backend.ldap = {
address = "ldap://localhost:3890";
base_dn = "dc=procopius,dc=dk";
users_filter = "(&({username_attribute}={input})(objectClass=person))";
groups_filter = "(member={dn})";
user = "uid=authelia,ou=people,dc=procopius,dc=dk";
};
access_control = {
default_policy = "deny";
# We want this rule to be low priority so it doesn't override the others
rules = lib.mkAfter [
{
domain = "*.procopius.dk";
policy = "one_factor";
}
];
};
storage.postgres = {
address = "unix:///run/postgresql";
database = authelia;
username = authelia;
# I'm using peer authentication, so this doesn't actually matter, but Authelia
# complains if I don't have it.
# https://github.com/authelia/authelia/discussions/7646
password = authelia;
};
session = {
redis.host = "/var/run/redis-procopius/redis.sock";
cookies = [
{
domain = "procopius.dk";
authelia_url = "https://authelia.procopius.dk";
# The period of time the user can be inactive for before the session is destroyed
inactivity = "1M";
# The period of time before the cookie expires and the session is destroyed
expiration = "3M";
# The period of time before the cookie expires and the session is destroyed
# when the remember me box is checked
remember_me = "1y";
}
];
};
notifier.smtp = {
address = "smtp://mail.procopius.dk";
username = "admin@procopius.dk";
sender = "auth@procopius.dk";
};
log.level = "info";
# identity_providers.oidc = {
# # https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter
# claims_policies = {
# # karakeep.id_token = ["email"];
# };
# cors = {
# endpoints = ["token"];
# allowed_origins_from_client_redirect_uris = true;
# };
# authorization_policies.default = {
# default_policy = "one_factor";
# rules = [
# {
# policy = "deny";
# subject = "group:lldap_strict_readonly";
# }
# ];
# };
# };
# Necessary for Traefik integration
# See https://www.authelia.com/integration/proxies/traefik/#implementation
server.endpoints.authz.forward-auth.implementation = "ForwardAuth";
};
# Templates don't work correctly when parsed from Nix, so our OIDC clients are defined here
# settingsFiles = [./oidc_clients.yaml];
secrets = with config.sops; {
jwtSecretFile = secrets."authelia/jwt_secret".path;
# oidcIssuerPrivateKeyFile = secrets."authelia/jwks".path;
# oidcHmacSecretFile = secrets."authelia/hmac_secret".path;
sessionSecretFile = secrets."authelia/session_secret".path;
storageEncryptionKeyFile = secrets."authelia/storage_encryption_key".path;
};
environmentVariables = with config.sops; {
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE =
secrets."authelia/lldap_authelia_password".path;
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = secrets."authelia/smtp_authelia_password".path;
};
};
# caddy = {
# virtualHosts."auth.procopius.cc".extraConfig = ''
# reverse_proxy :9091
# '';
# # A Caddy snippet that can be imported to enable Authelia in front of a service
# # Taken from https://www.authelia.com/integration/proxies/caddy/#subdomain
# extraConfig = ''
# (auth) {
# forward_auth :9091 {
# uri /api/authz/forward-auth
# copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
# }
# }
# '';
# };
};
# Give Authelia access to the Redis socket
users.users.${authelia}.extraGroups = ["redis-procopius"];
systemd.services.${authelia} = let
dependencies = [
"lldap.service"
"postgresql.service"
"redis-procopius.service"
];
in {
# Authelia requires LLDAP, PostgreSQL, and Redis to be running
after = dependencies;
requires = dependencies;
# Required for templating
serviceConfig.Environment = "X_AUTHELIA_CONFIG_FILTERS=template";
};
sops.secrets = {
"authelia/hmac_secret".owner = authelia;
"authelia/jwks".owner = authelia;
"authelia/jwt_secret".owner = authelia;
"authelia/session_secret".owner = authelia;
"authelia/storage_encryption_key".owner = authelia;
# The password for the `authelia` LLDAP user
"authelia/lldap_authelia_password".owner = authelia;
"authelia/smtp_authelia_password".owner = authelia;
smtp-password_authelia = {
owner = authelia;
key = "service_accounts/authelia/password";
};
};
}

View file

@ -0,0 +1,762 @@
#!/usr/bin/env bash
set -e
set -o pipefail
echo "$LLDAP_SET_PASSWORD_PATH"
LLDAP_ADMIN_PASSWORD="$(cat $LLDAP_ADMIN_PASSWORD_FILE)"
LLDAP_URL="${LLDAP_URL:-http://localhost:17170}"
LLDAP_ADMIN_USERNAME="${LLDAP_ADMIN_USERNAME:-admin}"
LLDAP_ADMIN_PASSWORD="${LLDAP_ADMIN_PASSWORD:-password}"
USER_SCHEMAS_DIR="${USER_SCHEMAS_DIR:-/bootstrap/user-schemas}"
GROUP_SCHEMAS_DIR="${GROUP_SCHEMAS_DIR:-/bootstrap/group-schemas}"
USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/bootstrap/user-configs}"
GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/bootstrap/group-configs}"
LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}"
DO_CLEANUP="${DO_CLEANUP:-false}"
# Fallback to support legacy defaults
if [[ ! -d $USER_CONFIGS_DIR ]] && [[ -d "/user-configs" ]]; then
USER_CONFIGS_DIR="/user-configs"
fi
if [[ ! -d $GROUP_CONFIGS_DIR ]] && [[ -d "/group-configs" ]]; then
GROUP_CONFIGS_DIR="/group-configs"
fi
check_install_dependencies() {
local commands=('curl' 'jq' 'jo')
local commands_not_found='false'
if ! hash "${commands[@]}" 2>/dev/null; then
if hash 'apk' 2>/dev/null && [[ $EUID -eq 0 ]]; then
apk add "${commands[@]}"
elif hash 'apt' 2>/dev/null && [[ $EUID -eq 0 ]]; then
apt update -yqq
apt install -yqq "${commands[@]}"
else
local command=''
for command in "${commands[@]}"; do
if ! hash "$command" 2>/dev/null; then
printf 'Command not found "%s"\n' "$command"
fi
done
commands_not_found='true'
fi
fi
if [[ "$commands_not_found" == 'true' ]]; then
return 1
fi
}
check_required_env_vars() {
local env_var_not_specified='false'
local dual_env_vars_list=(
'LLDAP_URL'
'LLDAP_ADMIN_USERNAME'
'LLDAP_ADMIN_PASSWORD'
)
local dual_env_var_name=''
for dual_env_var_name in "${dual_env_vars_list[@]}"; do
local dual_env_var_file_name="${dual_env_var_name}_FILE"
if [[ -z "${!dual_env_var_name}" ]] && [[ -z "${!dual_env_var_file_name}" ]]; then
printf 'Please specify "%s" or "%s" variable!\n' "$dual_env_var_name" "$dual_env_var_file_name" >&2
env_var_not_specified='true'
else
if [[ -n "${!dual_env_var_file_name}" ]]; then
declare -g "$dual_env_var_name"="$(cat "${!dual_env_var_file_name}")"
fi
fi
done
if [[ "$env_var_not_specified" == 'true' ]]; then
return 1
fi
}
check_configs_validity() {
local config_file='' config_invalid='false'
for config_file in "$@"; do
local error=''
if ! error="$(jq '.' -- "$config_file" 2>&1 >/dev/null)"; then
printf '%s: %s\n' "$config_file" "$error"
config_invalid='true'
fi
done
if [[ "$config_invalid" == 'true' ]]; then
return 1
fi
}
auth() {
local url="$1" admin_username="$2" admin_password="$3"
local response
response="$(curl --silent --request POST \
--url "$url/auth/simple/login" \
--header 'Content-Type: application/json' \
--data "$(jo -- username="$admin_username" password="$admin_password")")"
TOKEN="$(printf '%s' "$response" | jq --raw-output .token)"
}
make_query() {
local query_file="$1" variables_file="$2"
curl --silent --request POST \
--url "$LLDAP_URL/api/graphql" \
--header "Authorization: Bearer $TOKEN" \
--header 'Content-Type: application/json' \
--data @<(jq --slurpfile variables "$variables_file" '. + {"variables": $variables[0]}' "$query_file")
}
get_group_list() {
local query='{"query":"query GetGroupList {groups {id displayName}}","operationName":"GetGroupList"}'
make_query <(printf '%s' "$query") <(printf '{}')
}
get_group_array() {
get_group_list | jq --raw-output '.data.groups[].displayName'
}
group_exists() {
if [[ "$(get_group_list | jq --raw-output --arg displayName "$1" '.data.groups | any(.[]; select(.displayName == $displayName))')" == 'true' ]]; then
return 0
else
return 1
fi
}
get_group_id() {
get_group_list | jq --raw-output --arg displayName "$1" '.data.groups[] | if .displayName == $displayName then .id else empty end'
}
create_group() {
local group_name="$1"
if group_exists "$group_name"; then
printf 'Group "%s" (%s) already exists\n' "$group_name" "$(get_group_id "$group_name")"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateGroup($name: String!) {createGroup(name: $name) {id displayName}}","operationName":"CreateGroup"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- name="$group_name"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'Group "%s" (%s) successfully created\n' "$group_name" "$(printf '%s' "$response" | jq --raw-output '.data.createGroup.id')"
fi
}
delete_group() {
local group_name="$1" id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
id="$(get_group_id "$group_name")"
# shellcheck disable=SC2016
local query='{"query":"mutation DeleteGroupQuery($groupId: Int!) {deleteGroup(groupId: $groupId) {ok}}","operationName":"DeleteGroupQuery"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- groupId="$id"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'Group "%s" (%s) successfully deleted\n' "$group_name" "$id"
fi
}
get_user_details() {
local id="$1"
# shellcheck disable=SC2016
local query='{"query":"query GetUserDetails($id: String!) {user(userId: $id) {id email displayName firstName lastName creationDate uuid groups {id displayName} attributes {name value}}}","operationName":"GetUserDetails"}'
make_query <(printf '%s' "$query") <(jo -- id="$id")
}
user_in_group() {
local user_id="$1" group_name="$2"
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
if ! user_exists "$user_id"; then
printf 'User "%s" is not exists\n' "$user_id"
return
fi
if [[ "$(get_user_details "$user_id" | jq --raw-output --arg displayName "$group_name" '.data.user.groups | any(.[]; select(.displayName == $displayName))')" == 'true' ]]; then
return 0
else
return 1
fi
}
add_user_to_group() {
local user_id="$1" group_name="$2" group_id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
group_id="$(get_group_id "$group_name")"
if user_in_group "$user_id" "$group_name"; then
printf 'User "%s" already in group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation AddUserToGroup($user: String!, $group: Int!) {addUserToGroup(userId: $user, groupId: $group) {ok}}","operationName":"AddUserToGroup"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$user_id" group="$group_id"))"
error="$(printf '%s' "$response" | jq '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully added to the group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
fi
}
remove_user_from_group() {
local user_id="$1" group_name="$2" group_id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
group_id="$(get_group_id "$group_name")"
# shellcheck disable=SC2016
local query='{"operationName":"RemoveUserFromGroup","query":"mutation RemoveUserFromGroup($user: String!, $group: Int!) {removeUserFromGroup(userId: $user, groupId: $group) {ok}}"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$user_id" group="$group_id"))"
error="$(printf '%s' "$response" | jq '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully removed from the group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
fi
}
get_users_list() {
# shellcheck disable=SC2016
local query='{"query": "query ListUsersQuery($filters: RequestFilter) {users(filters: $filters) {id email displayName firstName lastName creationDate}}","operationName": "ListUsersQuery"}'
make_query <(printf '%s' "$query") <(jo -- filters=null)
}
user_exists() {
if [[ "$(get_users_list | jq --raw-output --arg id "$1" '.data.users | any(.[]; .id == $id)')" == 'true' ]]; then
return 0
else
return 1
fi
}
delete_user() {
local id="$1"
if ! user_exists "$id"; then
printf 'User "%s" is not exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query": "mutation DeleteUserQuery($user: String!) {deleteUser(userId: $user) {ok}}","operationName": "DeleteUserQuery"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$id"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully deleted\n' "$id"
fi
}
get_group_property_list() {
local query='{"query":"query GetGroupAttributesSchema { schema { groupSchema { attributes { name }}}}","operationName":"GetGroupAttributesSchema"}'
make_query <(printf '%s' "$query") <(printf '{}')
}
group_property_exists() {
if [[ "$(get_group_property_list | jq --raw-output --arg name "$1" '.data.schema.groupSchema.attributes | any(.[]; select(.name == $name))')" == 'true' ]]; then
return 0
else
return 1
fi
}
create_group_schema_property() {
local name="$1"
local attributeType="$2"
local isEditable="$3"
local isList="$4"
local isVisible="$5"
if group_property_exists "$name"; then
printf 'Group property "%s" already exists\n' "$name"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateGroupAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) {addGroupAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) {ok}}","operationName":"CreateGroupAttribute"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- name="$name" attributeType="$attributeType" isEditable="$isEditable" isList="$isList" isVisible="$isVisible"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'Group attribute "%s" successfully created\n' "$name"
fi
}
get_user_property_list() {
local query='{"query":"query GetUserAttributesSchema { schema { userSchema { attributes { name }}}}","operationName":"GetUserAttributesSchema"}'
make_query <(printf '%s' "$query") <(printf '{}')
}
user_property_exists() {
if [[ "$(get_user_property_list | jq --raw-output --arg name "$1" '.data.schema.userSchema.attributes | any(.[]; select(.name == $name))')" == 'true' ]]; then
return 0
else
return 1
fi
}
create_user_schema_property() {
local name="$1"
local attributeType="$2"
local isEditable="$3"
local isList="$4"
local isVisible="$5"
if user_property_exists "$name"; then
printf 'User property "%s" already exists\n' "$name"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateUserAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) {addUserAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) {ok}}","operationName":"CreateUserAttribute"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- name="$name" attributeType="$attributeType" isEditable="$isEditable" isList="$isList" isVisible="$isVisible"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User attribute "%s" successfully created\n' "$name"
fi
}
update_group_attributes() {
local group_id="$1"
local attributes_json="$2"
# shellcheck disable=SC2016
local query
query=$(jq -n -c \
--argjson groupId "$group_id" \
--argjson attributes "$attributes_json" '
{
"query": "mutation UpdateGroup($group: UpdateGroupInput!) {updateGroup(group: $group) {ok}}",
"operationName": "UpdateGroup",
"variables": {
"group": {
"id": $groupId,
"insertAttributes": $attributes
}
}
}
')
local response='' error=''
response="$(curl --silent --request POST \
--url "$LLDAP_URL/api/graphql" \
--header "Authorization: Bearer $TOKEN" \
--header 'Content-Type: application/json' \
--data "$query")"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf 'Error updating attributes for group ID "%s": %s\n' "$group_id" "$error"
else
printf 'Custom attributes for group ID "%s" successfully updated\n' "$group_id"
fi
}
update_user_attributes() {
local user_id="$1"
local attributes_json="$2"
local query
query=$(jq -n -c \
--arg userId "$user_id" \
--argjson attributes "$attributes_json" '
{
"query": "mutation UpdateUser($user: UpdateUserInput!) {updateUser(user: $user) {ok}}",
"operationName": "UpdateUser",
"variables":{
"user": {
"id":$userId,
"insertAttributes":$attributes
}
}
}
')
local response='' error=''
response="$(curl --silent --request POST \
--url "$LLDAP_URL/api/graphql" \
--header "Authorization: Bearer $TOKEN" \
--header 'Content-Type: application/json' \
--data "$query")"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf 'Error updating attributes for user "%s": %s\n' "$user_id" "$error"
else
printf 'Custom attributes for user "%s" successfully updated\n' "$user_id"
fi
}
extract_custom_group_attributes() {
extract_custom_attributes "$1" '"name"'
}
extract_custom_user_attributes() {
extract_custom_attributes "$1" '"id","email","password","displayName","firstName","lastName","groups","avatar_file","avatar_url","gravatar_avatar","weserv_avatar"'
}
extract_custom_attributes() {
local json_config="$1"
local standard_fields="$2"
# Extract all keys from the user config
local all_keys=$(echo "$json_config" | jq 'keys | .[]')
# Filter out standard fields
local custom_keys=$(echo "$all_keys" | jq -c --arg std_fields "$standard_fields" 'select(. | inside($std_fields) | not)')
# Build attribute array for GraphQL
local attributes_array="["
local first=true
while read -r key; do
if $first; then
first=false
else
attributes_array+=","
fi
key=$(echo "$key" | tr -d '"')
# If key is empty - this condition traps configurations without custom attributes
if [ -z "$key" ]; then continue; fi
# Get the value
local value=$(echo "$json_config" | jq --arg key "$key" '.[$key]')
# If the value is null, skip it
if echo "$value" | jq -e 'type == "null"' > /dev/null; then
continue
# Check if value is a JSON array
elif echo "$value" | jq -e 'type == "array"' > /dev/null; then
# For array types, ensure each element is a string, use compact JSON
local array_values=$(echo "$value" | jq -c 'map(tostring)')
attributes_array+="{\"name\":\"$key\",\"value\":$array_values}"
else
# For single values, make sure it's a string
local string_value=$(echo "$value" | jq 'tostring')
attributes_array+="{\"name\":\"$key\",\"value\":[$string_value]}"
fi
done < <(echo "$custom_keys")
attributes_array+="]"
echo "$attributes_array"
}
__common_user_mutation_query() {
local \
query="$1" \
id="${2:-null}" \
email="${3:-null}" \
displayName="${4:-null}" \
firstName="${5:-null}" \
lastName="${6:-null}" \
avatar_file="${7:-null}" \
avatar_url="${8:-null}" \
gravatar_avatar="${9:-false}" \
weserv_avatar="${10:-false}"
local variables_arr=(
'-s' "id=$id"
'-s' "email=$email"
'-s' "displayName=$displayName"
'-s' "firstName=$firstName"
'-s' "lastName=$lastName"
)
local temp_avatar_file=''
if [[ "$gravatar_avatar" == 'true' ]]; then
avatar_url="https://gravatar.com/avatar/$(printf '%s' "$email" | sha256sum | cut -d ' ' -f 1)?size=512"
fi
if [[ "$avatar_url" != 'null' ]]; then
temp_avatar_file="${TMP_AVATAR_DIR}/$(printf '%s' "$avatar_url" | md5sum | cut -d ' ' -f 1)"
if ! [[ -f "$temp_avatar_file" ]]; then
if [[ "$weserv_avatar" == 'true' ]]; then
avatar_url="https://wsrv.nl/?url=$avatar_url&output=jpg"
fi
curl --silent --location --output "$temp_avatar_file" "$avatar_url"
fi
avatar_file="$temp_avatar_file"
fi
if [[ "$avatar_file" == 'null' ]]; then
variables_arr+=('-s' 'avatar=null')
else
variables_arr+=("avatar=%$avatar_file")
fi
make_query <(printf '%s' "$query") <(jo -- user=:<(jo -- "${variables_arr[@]}"))
}
create_user() {
local id="$1"
if user_exists "$id"; then
printf 'User "%s" already exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateUser($user: CreateUserInput!) {createUser(user: $user) {id creationDate}}","operationName":"CreateUser"}'
local response='' error=''
response="$(__common_user_mutation_query "$query" "$@")"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully created\n' "$id"
fi
}
update_user() {
local id="$1"
if ! user_exists "$id"; then
printf 'User "%s" is not exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation UpdateUser($user: UpdateUserInput!) {updateUser(user: $user) {ok}}","operationName":"UpdateUser"}'
local response='' error=''
response="$(__common_user_mutation_query "$query" "$@")"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully updated\n' "$id"
fi
}
create_update_user() {
local id="$1"
if user_exists "$id"; then
update_user "$@"
else
create_user "$@"
fi
}
main() {
check_install_dependencies
check_required_env_vars
local user_config_files=("${USER_CONFIGS_DIR}"/*.json)
local group_config_files=("${GROUP_CONFIGS_DIR}"/*.json)
local user_schema_files=()
local group_schema_files=()
local file=''
[[ -d "$USER_SCHEMAS_DIR" ]] && for file in "${USER_SCHEMAS_DIR}"/*.json; do
user_schema_files+=("$file")
done
[[ -d "$GROUP_SCHEMAS_DIR" ]] && for file in "${GROUP_SCHEMAS_DIR}"/*.json; do
group_schema_files+=("$file")
done
if ! check_configs_validity "${group_config_files[@]}" "${user_config_files[@]}" "${group_schema_files[@]}" "${user_schema_files[@]}"; then
exit 1
fi
until curl --silent -o /dev/null "$LLDAP_URL"; do
printf 'Waiting lldap to start...\n'
sleep 10
done
auth "$LLDAP_URL" "$LLDAP_ADMIN_USERNAME" "$LLDAP_ADMIN_PASSWORD"
printf -- '\n--- group schemas ---\n'
local group_schema_config_row=''
[[ ${#group_schema_files[@]} -gt 0 ]] && while read -r group_schema_config_row; do
local field='' name='' attributeType='' isEditable='' isList='' isVisible=''
for field in 'name' 'attributeType' 'isEditable' 'isList' 'isVisible'; do
declare "$field"="$(printf '%s' "$group_schema_config_row" | jq --raw-output --arg field "$field" '.[$field]')"
done
create_group_schema_property "$name" "$attributeType" "$isEditable" "$isList" "$isVisible"
done < <(jq --compact-output '.[]' -- "${group_schema_files[@]}")
printf -- '--- group schemas ---\n'
printf -- '\n--- user schemas ---\n'
local user_schema_config_row=''
[[ ${#user_schema_files[@]} -gt 0 ]] && while read -r user_schema_config_row; do
local field='' name='' attributeType='' isEditable='' isList='' isVisible=''
for field in 'name' 'attributeType' 'isEditable' 'isList' 'isVisible'; do
declare "$field"="$(printf '%s' "$user_schema_config_row" | jq --raw-output --arg field "$field" '.[$field]')"
done
create_user_schema_property "$name" "$attributeType" "$isEditable" "$isList" "$isVisible"
done < <(jq --compact-output '.[]' -- "${user_schema_files[@]}")
printf -- '--- user schemas ---\n'
local redundant_groups=''
redundant_groups="$(get_group_list | jq '[ .data.groups[].displayName ]' | jq --compact-output '. - ["lldap_admin","lldap_password_manager","lldap_strict_readonly"]')"
printf -- '\n--- groups ---\n'
local group_config=''
while read -r group_config; do
local group_name=''
group_name="$(printf '%s' "$group_config" | jq --raw-output '.name')"
create_group "$group_name"
redundant_groups="$(printf '%s' "$redundant_groups" | jq --compact-output --arg name "$group_name" '. - [$name]')"
# Process custom attributes
printf -- '--- Processing custom attributes for group %s ---\n' "$group_name"
local attributes_json
attributes_json=$(extract_custom_group_attributes "$group_config")
if [[ "$attributes_json" != "[]" ]]; then
# Get the group ID
local group_id
group_id="$(get_group_id "$group_name")"
update_group_attributes "$group_id" "$attributes_json"
else
printf 'No custom attributes found for group "%s"\n' "$group_name"
fi
done < <(jq --compact-output '.' -- "${group_config_files[@]}")
printf -- '--- groups ---\n'
printf -- '\n--- redundant groups ---\n'
if [[ "$redundant_groups" == '[]' ]]; then
printf 'There are no redundant groups\n'
else
local group_name=''
while read -r group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
delete_group "$group_name"
else
printf '[WARNING] Group "%s" is not declared in config files\n' "$group_name"
fi
done < <(printf '%s' "$redundant_groups" | jq --raw-output '.[]')
fi
printf -- '--- redundant groups ---\n'
local redundant_users=''
redundant_users="$(get_users_list | jq '[ .data.users[].id ]' | jq --compact-output --arg admin_id "$LLDAP_ADMIN_USERNAME" '. - [$admin_id]')"
TMP_AVATAR_DIR="$(mktemp -d)"
local user_config=''
while read -r user_config; do
local field='' id='' email='' displayName='' firstName='' lastName='' avatar_file='' avatar_url='' gravatar_avatar='' weserv_avatar='' password=''
for field in 'id' 'email' 'displayName' 'firstName' 'lastName' 'avatar_file' 'avatar_url' 'gravatar_avatar' 'weserv_avatar' 'password'; do
declare "$field"="$(printf '%s' "$user_config" | jq --raw-output --arg field "$field" '.[$field]')"
done
printf -- '\n--- %s ---\n' "$id"
create_update_user "$id" "$email" "$displayName" "$firstName" "$lastName" "$avatar_file" "$avatar_url" "$gravatar_avatar" "$weserv_avatar"
redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')"
if [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$password"
fi
# Process custom attributes
printf -- '--- Processing custom attributes for user %s ---\n' "$id"
local attributes_json
attributes_json=$(extract_custom_user_attributes "$user_config")
if [[ "$attributes_json" != "[]" ]]; then
update_user_attributes "$id" "$attributes_json"
else
printf 'No custom attributes found for user "%s"\n' "$id"
fi
local redundant_user_groups=''
redundant_user_groups="$(get_user_details "$id" | jq '[ .data.user.groups[].displayName ]')"
local group=''
while read -r group; do
if [[ -n "$group" ]]; then
add_user_to_group "$id" "$group"
redundant_user_groups="$(printf '%s' "$redundant_user_groups" | jq --compact-output --arg group "$group" '. - [$group]')"
fi
done < <(printf '%s' "$user_config" | jq --raw-output '.groups | if . == null then "" else .[] end')
local user_group_name=''
while read -r user_group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
remove_user_from_group "$id" "$user_group_name"
else
printf '[WARNING] User "%s" is not declared as member of the "%s" group in the config files\n' "$id" "$user_group_name"
fi
done < <(printf '%s' "$redundant_user_groups" | jq --raw-output '.[]')
printf -- '--- %s ---\n' "$id"
done < <(jq --compact-output '.' -- "${user_config_files[@]}")
rm -r "$TMP_AVATAR_DIR"
printf -- '\n--- redundant users ---\n'
if [[ "$redundant_users" == '[]' ]]; then
printf 'There are no redundant users\n'
else
local id=''
while read -r id; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
delete_user "$id"
else
printf '[WARNING] User "%s" is not declared in config files\n' "$id"
fi
done < <(printf '%s' "$redundant_users" | jq --raw-output '.[]')
fi
printf -- '--- redundant users ---\n'
}
main "$@"

View file

@ -0,0 +1,39 @@
{
pkgs,
config,
...
}: {
systemd.services.lldap-bootstrap = {
description = "Bootstraps LLDAP users";
requires = ["lldap.service"];
serviceConfig = {
DynamicUser = true;
Type = "oneshot";
ProtectSystem = "strict";
ProtectHome = true;
PrivateUsers = true;
PrivateTmp = true;
LoadCredential = "inadyn.conf:${config.sops.templates."inadyn.conf".path}";
CacheDirectory = "inadyn";
ExecStart = ''
export LLDAP_URL=http://localhost:8080
export LLDAP_ADMIN_USERNAME=admin
export LLDAP_ADMIN_PASSWORD=changeme
export USER_CONFIGS_DIR="$(realpath ./configs/user)"
export GROUP_CONFIGS_DIR="$(realpath ./configs/group)"
export USER_SCHEMAS_DIR="$(realpath ./configs/user-schema)"
export GROUP_SCHEMAS_DIR="$(realpath ./configs/group-schema)"
export LLDAP_SET_PASSWORD_PATH="$(realpath ./lldap_set_password)"
export DO_CLEANUP=false
./bootstrap.sh
${pkgs.inadyn}/bin/inadyn \
--foreground \
--syslog \
--once \
--cache-dir ''${CACHE_DIRECTORY} \
--config ''${CREDENTIALS_DIRECTORY}/inadyn.conf
'';
};
};
}

View file

@ -0,0 +1,14 @@
{config, ...}: {
sops.templates."default-groups.json" = {
content = ''
{
"name": "git-user"
}
{
"name": "git-admin"
}
'';
path = "/bootstrap/group-configs/default-groups.json";
owner = "lldap";
};
}

View file

@ -0,0 +1,103 @@
{
config,
lib,
pkgs,
...
}: let
cfg = config.services.lldapBootstrap;
in {
imports = [
./user-configs.nix
./group-configs.nix
];
options.services.lldapBootstrap = {
enable = lib.mkEnableOption "LLDAP bootstrapping service.";
host = lib.mkOption {
type = lib.types.str;
default = "http://localhost:17170";
description = "The LLDAP host and port (e.g., 'localhost:17170').";
};
adminUsername = lib.mkOption {
type = lib.types.str;
default = "admin";
description = "The LLDAP admin username.";
};
adminPasswordFile = lib.mkOption {
type = lib.types.path;
description = "Path to the sops secret file containing the LLDAP admin password.";
default = "/run/secrets/lldap/admin_password";
example = "/run/secrets/lldap/admin_password";
};
# Add any other environment variables your bootstrap script might need
extraEnv = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = {};
description = "Additional environment variables to pass to the bootstrap script.";
};
# Option to control when the bootstrap service runs (e.g., OnUnitActive)
# Be careful with this, as you generally only want it to run once.
# We'll default to OneShot and disable unless specifically enabled and configured.
runOnce = lib.mkOption {
type = lib.types.bool;
default = true;
description = "If true, the service will run once and then disable itself on success.";
};
};
config = lib.mkIf cfg.enable {
environment.etc."bootstrap/bootstrap.sh" = {
source = ./bootstrap.sh;
user = "lldap";
group = "lldap";
mode = "0770";
};
environment.systemPackages = with pkgs; [
curl
jq
jo
];
systemd.services.lldap-bootstrap = {
description = "LLDAP Bootstrap Service";
# type = "oneshot";
after = ["network.target" "lldap.service"]; # Assuming your LLDAP service is called 'lldap.service'
wantedBy = ["multi-user.target"];
# Environment variables. Secrets will be read directly from the sops-nix managed paths.
environment =
{
LLDAP_URL = cfg.host;
LLDAP_ADMIN_USERNAME = cfg.adminUsername;
LLDAP_ADMIN_PASSWORD_FILE = cfg.adminPasswordFile;
LLDAP_SET_PASSWORD_PATH = "${pkgs.lldap}/bin/lldap_set_password";
}
// cfg.extraEnv; # Merge with any extra environment variables
# The command to execute. Ensure your script is executable.
# We use pkgs.writeScriptBin to embed the script directly into the Nix store
# This makes the service self-contained and ensures the script path is valid.
# script = ''
# /etc/bootstrap/bootstrap.sh
# '';
path = [pkgs.bash pkgs.curl pkgs.jq pkgs.jo];
# Optional: Control service behavior after successful run.
# If runOnce is true, disable the service after it successfully completes.
# This prevents it from running on every reboot if the bootstrap is a one-time operation.
serviceConfig = lib.mkIf cfg.runOnce {
Type = "oneshot";
User = "lldap";
Group = "lldap";
DynamicUser = false;
ExecStart = "/etc/bootstrap/bootstrap.sh";
};
};
};
}

View file

@ -0,0 +1,28 @@
{config, ...}: {
sops.secrets."service_accounts/authelia/password" = {};
sops.secrets."service_accounts/forgejo/password" = {};
sops.templates."service-accounts.json" = {
content = ''
{
"id": "authelia",
"email": "authelia@procopius.dk",
"password": "${config.sops.placeholder."service_accounts/authelia/password"}",
"displayName": "Authelia",
"groups": [
"lldap_password_manager"
]
}
{
"id": "forgejo",
"email": "forgejo@procopius.dk",
"password": "${config.sops.placeholder."service_accounts/forgejo/password"}",
"displayName": "Forgejo",
"groups": [
"lldap_password_manager"
]
}
'';
path = "/bootstrap/user-configs/service-accounts.json";
owner = "lldap";
};
}

View file

@ -0,0 +1,10 @@
{
imports = [
./lldap.nix
./authelia.nix
./postgres.nix
./redis.nix
];
system.stateVersion = "25.05";
}

61
machines/auth/lldap.nix Normal file
View file

@ -0,0 +1,61 @@
{
config,
lib,
...
}: let
cfg = config.services.lldap;
in {
imports = [
./bootstrap/lldap-bootstrap.nix
];
sops.secrets = {
"lldap/jwt_secret".owner = "lldap";
"lldap/key_seed".owner = "lldap";
"lldap/admin_password".owner = "lldap";
};
networking.firewall.allowedTCPPorts = [
cfg.settings.http_port
cfg.settings.ldap_port
];
services.lldapBootstrap.enable = true;
services.lldap = {
enable = true;
settings = {
ldap_base_dn = "dc=procopius,dc=dk";
ldap_user_email = "admin@procopius.dk";
database_url = "postgresql://lldap@localhost/lldap?host=/run/postgresql";
};
environment = {
LLDAP_JWT_SECRET_FILE = config.sops.secrets."lldap/jwt_secret".path;
LLDAP_KEY_SEED_FILE = config.sops.secrets."lldap/key_seed".path;
LLDAP_LDAP_USER_PASS_FILE = config.sops.secrets."lldap/admin_password".path;
};
};
systemd.services.lldap = let
dependencies = [
"postgresql.service"
];
in {
# LLDAP requires PostgreSQL to be running
after = dependencies;
requires = dependencies;
# DynamicUser screws up sops-nix ownership because
# the user doesn't exist outside of runtime.
serviceConfig.DynamicUser = lib.mkForce false;
};
# Setup a user and group for LLDAP
users = {
users.lldap = {
group = "lldap";
isSystemUser = true;
};
groups.lldap = {};
};
}

View file

@ -0,0 +1,23 @@
{lib, ...}: {
services.postgresql = {
enable = true;
ensureDatabases = [
"authelia-procopius"
"lldap"
];
ensureUsers = [
{
name = "authelia-procopius";
ensureDBOwnership = true;
}
{
name = "lldap";
ensureDBOwnership = true;
}
];
authentication = lib.mkForce ''
# TYPE DATABASE USER ADDRESS METHOD
local all all trust
'';
};
}

3
machines/auth/redis.nix Normal file
View file

@ -0,0 +1,3 @@
{
services.redis.servers.procopius.enable = true;
}

View file

@ -0,0 +1,580 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 3,
"links": [],
"panels": [
{
"cacheTimeout": null,
"datasource": null,
"description": "Number of successful results compared to the total number of results during the current interval",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 9,
"links": [],
"options": {
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"mappings": [
{
"id": 0,
"op": "=",
"text": "N/A",
"type": 1,
"value": "null"
}
],
"max": 1,
"min": 0,
"nullValueMode": "connected",
"thresholds": [
{
"color": "red",
"value": null
},
{
"color": "semi-dark-orange",
"value": 0.6
},
{
"color": "yellow",
"value": 0.8
},
{
"color": "dark-green",
"value": 0.95
}
],
"unit": "percentunit"
},
"override": {},
"values": false
},
"orientation": "horizontal",
"showThresholdLabels": false,
"showThresholdMarkers": false
},
"pluginVersion": "6.4.4",
"targets": [
{
"expr": "sum(rate(gatus_results_total{success=\"true\"}[30s])) by (key) / sum(rate(gatus_results_total[30s])) by (key)",
"hide": false,
"legendFormat": "{{key}}",
"refId": "B"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Success rate",
"type": "gauge"
},
{
"aliasColors": {},
"bars": false,
"cacheTimeout": null,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 11,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null as zero",
"options": {
"dataLinks": []
},
"percentage": false,
"pluginVersion": "6.4.4",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "gatus_results_duration_seconds",
"format": "time_series",
"instant": false,
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{key}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Response time",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"cacheTimeout": null,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 10,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "connected",
"options": {
"dataLinks": []
},
"percentage": false,
"pluginVersion": "6.4.4",
"pointradius": 2,
"points": true,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(gatus_results_total{success=\"true\"}[30s])) by (key) / sum(rate(gatus_results_total[30s])) by (key)",
"format": "time_series",
"instant": false,
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{key}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Success rate",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"description": "Number of results per minute",
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 2,
"interval": "",
"legend": {
"alignAsTable": false,
"avg": false,
"current": false,
"hideEmpty": false,
"hideZero": false,
"max": false,
"min": false,
"rightSide": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(gatus_results_total[5m])*60) by (key)",
"format": "time_series",
"hide": false,
"instant": false,
"interval": "30s",
"intervalFactor": 1,
"legendFormat": "{{key}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Total results per minute",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"decimals": null,
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(gatus_results_total{success=\"true\"}[5m])*60) by (key)",
"instant": false,
"interval": "30s",
"legendFormat": "{{key}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Successful results per minute",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"decimals": null,
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 16
},
"id": 3,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(gatus_results_total{success=\"false\"}[5m])*60) by (key)",
"interval": "30s",
"legendFormat": "{{key}} ",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Unsuccessful results per minute",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"refresh": "1m",
"schemaVersion": 20,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Gatus",
"uid": "KPI7Qj1Wk",
"version": 2
}

View file

@ -5,6 +5,7 @@
./influxdb.nix
./loki.nix
./grafana.nix
./gatus.nix
./jellyfin-exporter.nix
];

View file

@ -0,0 +1,32 @@
{
services.gatus = {
enable = true;
openFirewall = true;
settings = {
web.port = 8080;
metrics = true;
endpoints = [
{
name = "jellyfin";
url = "https://jellyfin.procopius.dk/health";
interval = "5m";
conditions = [
"[STATUS] == 200"
"[BODY] == Healthy"
"[RESPONSE_TIME] < 300"
];
}
{
name = "sonarr";
url = "https://sonarr.procopius.dk/health";
interval = "5m";
conditions = [
"[STATUS] == 200"
"[BODY] == Healthy"
"[RESPONSE_TIME] < 300"
];
}
];
};
};
}

View file

@ -5,6 +5,8 @@
lib,
...
}: {
# Add grafana user to the inlfuxdb2 group (for secret)
users.users.grafana.extraGroups = ["influxdb2"];
services.grafana.enable = true;
services.grafana.settings = {
server = {
@ -57,7 +59,7 @@
httpHeaderName1 = "Authorization";
};
secureJsonData = {
httpHeaderValue1 = "Token iY4MTuqUAVJbBkDUiMde";
httpHeaderValue1 = "$__file{${config.sops.secrets."influxdb/token".path}}";
};
}
];
@ -123,4 +125,11 @@
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/gatus.json" = {
source = ./dashboards/gatus.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
}

View file

@ -12,7 +12,8 @@ in {
};
sops.secrets."influxdb/token" = {
sopsFile = ../../secrets/secrets.yaml;
owner = "influxdb2";
group = "influxdb2";
mode = "0440";
};
networking.firewall.allowedTCPPorts = [8086];

View file

@ -75,6 +75,13 @@
relabel_configs = instance_relabel_config;
};
gatus_target = "${monitor_hostname}:8080";
gatus_job = {
job_name = "gatus";
static_configs = [{targets = [gatus_target];}];
relabel_configs = instance_relabel_config;
};
traefik_monitor_port = 8082;
traefik_job = {
job_name = "traefik";
@ -110,11 +117,11 @@
{
targets = [
"${media_hostname}:9707" # sonarr
"${media_hostname}:9708" # readarr
"${media_hostname}:9709" # radarr
"${media_hostname}:9710" # prowlarr
"${media_hostname}:9711" # lidarr
"${media_hostname}:9712" # bazarr
"${media_hostname}:9708" # radarr
"${media_hostname}:9709" # lidarr
"${media_hostname}:9710" # readarr
"${media_hostname}:9711" # prowlarr
# "${media_hostname}:9712" # bazarr
];
}
];
@ -158,6 +165,7 @@ in {
prometheus_job
alertmanager_job
grafana_job
gatus_job
traefik_job
forgejo_job
postgres_job

View file

@ -0,0 +1,80 @@
{
config,
pkgs,
...
}: let
promtail_port = 9080;
in {
networking.firewall.allowedTCPPorts = [promtail_port];
systemd.tmpfiles.rules = [
"d /var/lib/promtail 0755 promtail promtail -"
];
services.promtail = {
enable = true;
configuration = {
server = {
http_listen_port = promtail_port;
grpc_listen_port = 0;
};
positions = {
filename = "/var/lib/promtail/positions.yaml";
};
clients = [
{
url = "http://monitor.lab:3100/loki/api/v1/push";
}
];
scrape_configs = [
{
job_name = "journal";
journal = {
path = "/var/log/journal";
labels = {
job = "promtail";
host = config.networking.hostName;
env = "proxmox";
instance = "${config.networking.hostName}.lab";
};
};
relabel_configs = [
{
source_labels = ["__journal__systemd_unit"];
target_label = "unit";
}
{
source_labels = ["__journal__hostname"];
target_label = "host";
}
{
source_labels = ["__journal__systemd_user_unit"];
target_label = "user_unit";
}
{
source_labels = ["__journal__transport"];
target_label = "transport";
}
{
source_labels = ["__journal_priority_keyword"];
target_label = "severity";
}
];
}
# {
# job_name = "secure";
# static_configs = {
# targets = ["localhost"];
# labels = {
# job = "secure";
# host = config.networking.hostName;
# env = "proxmox";
# instance = "${config.networking.hostName}.lab";
# __path__ = "/var/log/secure";
# };
# };
# }
];
};
};
}