diff --git a/CHANGELOG.md b/CHANGELOG.md index 67da086..198e76e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Template: - Add a Grafana dashboard showing stats on backup jobs and also an alert if a backup job did not run in the last 24 hours or never succeeded in the last 24 hours. +- Add SSO integration in Grafana. ## Fixes diff --git a/docs/redirects.json b/docs/redirects.json index fa5e51e..c1401c8 100644 --- a/docs/redirects.json +++ b/docs/redirects.json @@ -914,9 +914,6 @@ "blocks-monitoring-budget-alerts": [ "blocks-monitoring.html#blocks-monitoring-budget-alerts" ], - "blocks-monitoring-configuration": [ - "blocks-monitoring.html#blocks-monitoring-configuration" - ], "blocks-monitoring-deluge-dashboard": [ "blocks-monitoring.html#blocks-monitoring-deluge-dashboard" ], @@ -968,6 +965,15 @@ "blocks-monitoring-options-shb.monitoring.grafanaPort": [ "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.grafanaPort" ], + "blocks-monitoring-options-shb.monitoring.ldap": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.ldap" + ], + "blocks-monitoring-options-shb.monitoring.ldap.adminGroup": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.ldap.adminGroup" + ], + "blocks-monitoring-options-shb.monitoring.ldap.userGroup": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.ldap.userGroup" + ], "blocks-monitoring-options-shb.monitoring.lokiMajorVersion": [ "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.lokiMajorVersion" ], @@ -1043,6 +1049,69 @@ "blocks-monitoring-options-shb.monitoring.ssl.systemdService": [ "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.ssl.systemdService" ], + "blocks-monitoring-options-shb.monitoring.sso": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso" + ], + "blocks-monitoring-options-shb.monitoring.sso.authEndpoint": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.authEndpoint" + ], + "blocks-monitoring-options-shb.monitoring.sso.authorization_policy": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.authorization_policy" + ], + "blocks-monitoring-options-shb.monitoring.sso.clientID": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.clientID" + ], + "blocks-monitoring-options-shb.monitoring.sso.enable": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.enable" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecret": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecret" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request.group": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request.group" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request.mode": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request.mode" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request.owner": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request.owner" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request.restartUnits": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecret.request.restartUnits" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecret.result": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecret.result" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecret.result.path": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecret.result.path" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request.group": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request.group" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request.mode": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request.mode" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request.owner": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request.owner" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request.restartUnits": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.request.restartUnits" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.result": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.result" + ], + "blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.result.path": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.sso.sharedSecretForAuthelia.result.path" + ], "blocks-monitoring-options-shb.monitoring.subdomain": [ "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.subdomain" ], @@ -1052,6 +1121,15 @@ "blocks-monitoring-provisioning": [ "blocks-monitoring.html#blocks-monitoring-provisioning" ], + "blocks-monitoring-usage": [ + "blocks-monitoring.html#blocks-monitoring-usage" + ], + "blocks-monitoring-usage-log-optimization": [ + "blocks-monitoring.html#blocks-monitoring-usage-log-optimization" + ], + "blocks-monitoring-usage-smtp": [ + "blocks-monitoring.html#blocks-monitoring-usage-smtp" + ], "blocks-postgresql": [ "blocks-postgresql.html#blocks-postgresql" ], @@ -3386,6 +3464,9 @@ "services-karakeep-usage": [ "services-karakeep.html#services-karakeep-usage" ], + "services-monitoring-features": [ + "blocks-monitoring.html#services-monitoring-features" + ], "services-nextcloudserver": [ "services-nextcloud.html#services-nextcloudserver" ], diff --git a/modules/blocks/monitoring.nix b/modules/blocks/monitoring.nix index a07aeff..cc5e713 100644 --- a/modules/blocks/monitoring.nix +++ b/modules/blocks/monitoring.nix @@ -16,8 +16,22 @@ let hostname = config.networking.hostName; domain = cfg.domain; }; + + roleClaim = "grafana_groups"; + oauthScopes = [ + "openid" + "email" + "profile" + "groups" + "${roleClaim}" + ]; in { + imports = [ + ../blocks/authelia.nix + ../blocks/lldap.nix + ]; + options.shb.monitoring = { enable = lib.mkEnableOption "selfhostblocks.monitoring"; @@ -156,6 +170,84 @@ in } ); }; + + ldap = lib.mkOption { + description = '' + Setup LDAP integration. + ''; + default = { }; + type = lib.types.submodule { + options = { + userGroup = lib.mkOption { + type = lib.types.str; + description = "Group users must belong to to be able to login to Grafana."; + default = "monitoring_user"; + }; + adminGroup = lib.mkOption { + type = lib.types.str; + description = "Group users must belong to to be admins in Grafana."; + default = "monitoring_admin"; + }; + }; + }; + }; + + sso = lib.mkOption { + description = '' + Setup SSO integration. + ''; + default = { }; + type = lib.types.submodule { + options = { + enable = lib.mkEnableOption "SSO integration."; + + authEndpoint = lib.mkOption { + type = lib.types.str; + default = null; + description = "Endpoint to the SSO provider."; + example = "https://authelia.example.com"; + }; + + clientID = lib.mkOption { + type = lib.types.str; + description = "Client ID for the OIDC endpoint."; + default = "grafana"; + }; + + authorization_policy = lib.mkOption { + type = lib.types.enum [ + "one_factor" + "two_factor" + ]; + description = "Require one factor (password) or two factor (device) authentication."; + default = "one_factor"; + }; + + sharedSecret = lib.mkOption { + description = "OIDC shared secret for Grafana."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + owner = "grafana"; + restartUnits = [ + "grafana.service" + ]; + }; + }; + }; + + sharedSecretForAuthelia = lib.mkOption { + description = "OIDC shared secret for Authelia. Must be the same as `sharedSecret`"; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + ownerText = "config.shb.authelia.autheliaUser"; + owner = config.shb.authelia.autheliaUser; + }; + }; + }; + }; + }; + }; }; config = lib.mkMerge [ @@ -651,5 +743,65 @@ in }; }; }) + (lib.mkIf (cfg.enable && cfg.sso.enable) { + shb.lldap.ensureGroups = { + ${cfg.ldap.userGroup} = { }; + }; + + shb.authelia.extraDefinitions = { + user_attributes.${roleClaim}.expression = + # Roles are: None, Viewer, Editor, Admin, GrafanaAdmin + ''"${cfg.ldap.adminGroup}" in groups ? "Admin" : ("${cfg.ldap.userGroup}" in groups ? "Editor" : "Invalid")''; + }; + shb.authelia.extraOidcClaimsPolicies.${roleClaim} = { + custom_claims = { + "${roleClaim}" = { }; + }; + }; + shb.authelia.extraOidcScopes."${roleClaim}" = { + claims = [ "${roleClaim}" ]; + }; + + services.grafana.settings."auth.generic_oauth" = { + enabled = true; + name = "Authelia"; + icon = "signin"; + client_id = cfg.sso.clientID; + client_secret = "$__file{${cfg.sso.sharedSecret.result.path}}"; + scopes = oauthScopes; + empty_scopes = false; + allow_sign_up = true; + auto_login = true; + auth_url = "${cfg.sso.authEndpoint}/api/oidc/authorization"; + token_url = "${cfg.sso.authEndpoint}/api/oidc/token"; + # use_refresh_token = true; ? # https://grafana.com/docs/grafana/latest/setup-grafana/configure-access/configure-authentication/generic-oauth/#configure-generic-oauth-authentication-client-using-the-grafana-configuration-file + api_url = "${cfg.sso.authEndpoint}/api/oidc/userinfo"; + login_attribute_path = "preferred_username"; + groups_attribute_path = "groups"; + name_attribute_path = "name"; + use_pkce = true; + allow_assign_grafana_admin = true; + skip_org_role_sync = false; + role_attribute_path = roleClaim; + role_attribute_strict = true; + }; + + shb.authelia.oidcClients = [ + { + client_id = cfg.sso.clientID; + client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; + claims_policy = "${roleClaim}"; + scopes = oauthScopes; + authorization_policy = cfg.sso.authorization_policy; + redirect_uris = [ + "https://${cfg.subdomain}.${cfg.domain}/login/generic_oauth" + ]; + require_pkce = true; + pkce_challenge_method = "S256"; + response_types = [ "code" ]; + token_endpoint_auth_method = "client_secret_basic"; + } + ]; + }) ]; } diff --git a/modules/blocks/monitoring/docs/default.md b/modules/blocks/monitoring/docs/default.md index f900dd6..9d41ae4 100644 --- a/modules/blocks/monitoring/docs/default.md +++ b/modules/blocks/monitoring/docs/default.md @@ -8,24 +8,64 @@ This block sets up the monitoring stack for Self Host Blocks. It is composed of: - Prometheus as the database for metrics. - Loki as the database for logs. -## Configuration {#blocks-monitoring-configuration} +## Features {#services-monitoring-features} + +- Declarative [LDAP](#blocks-monitoring-options-shb.monitoring.ldap) Configuration. + - Needed LDAP groups are created automatically. +- Declarative [SSO](#blocks-monitoring-options-shb.monitoring.sso) Configuration. + - When SSO is enabled, login with user and password is disabled. + - Registration is enabled through SSO. +- Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy. +- Access through [HTTPS](#blocks-monitoring-options-shb.monitoring.ssl) using reverse proxy. + +## Usage {#blocks-monitoring-usage} + +The following snippet assumes a few blocks have been setup already: + +- the [secrets block](usage.html#usage-secrets) with SOPS, +- the [`shb.ssl` block](blocks-ssl.html#usage), +- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup). +- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup). ```nix -shb.monitoring = { - enable = true; - subdomain = "grafana"; - inherit domain; - contactPoints = [ "me@example.com" ]; - adminPassword.result = config.sops.secrets."monitoring/admin_password".result; - secretKey.result = config.sops.secrets."monitoring/secret_key".result; +{ + shb.monitoring = { + enable = true; + subdomain = "grafana"; + inherit domain; + contactPoints = [ "me@example.com" ]; + adminPassword.result = config.sops.secrets."monitoring/admin_password".result; + secretKey.result = config.sops.secrets."monitoring/secret_key".result; + + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + + sharedSecret.result = config.shb.sops.secret.oidcSecret.result; + sharedSecretForAuthelia.result = config.shb.sops.secret.oidcAutheliaSecret.result; + }; + }; + + shb.sops.secret."monitoring/admin_password".request = config.shb.monitoring.adminPassword.request; + shb.sops.secret."monitoring/secret_key".request = config.shb.monitoring.secretKey.request; + shb.sops.secret."monitoring/oidcSecret".request = config.shb.monitoring.sso.sharedSecret.request; + shb.sops.secret."monitoring/oidcAutheliaSecret" = { + request = config.shb.monitoring.sso.sharedSecretForAuthelia.request; + settings.key = "monitoring/oidcSecret"; + }; }; - -shb.sops.secret."monitoring/admin_password".request = config.shb.monitoring.adminPassword.request; -shb.sops.secret."monitoring/secret_key".request = config.shb.monitoring.secretKey.request; ``` +Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`. + With that, Grafana, Prometheus, Loki and Promtail are setup! You can access `Grafana` at -`grafana.example.com` with user `admin` and password ``. +`grafana.example.com` with user `admin` and the password from the sops key `monitoring/admin_password`. + +The [user](#services-open-webui-options-shb.open-webui.ldap.userGroup) +and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup) +LDAP groups are created automatically. + +### SMTP {#blocks-monitoring-usage-smtp} I recommend adding a STMP server configuration so you receive alerts by email: @@ -48,6 +88,8 @@ sops.secrets."monitoring/secret_key" = { }; ``` +### Log Optimization {#blocks-monitoring-usage-log-optimization} + Since all logs are now stored in Loki, you can probably reduce the systemd journal retention time with: diff --git a/test/blocks/monitoring.nix b/test/blocks/monitoring.nix index 957699a..a9912fd 100644 --- a/test/blocks/monitoring.nix +++ b/test/blocks/monitoring.nix @@ -1,6 +1,7 @@ { lib, ... }: let password = "securepw"; + oidcSecret = "oidcSecret"; commonTestScript = lib.shb.accessScript { hasSSL = { node, ... }: !(isNull node.config.shb.monitoring.ssl); @@ -19,6 +20,11 @@ let basic = { config, ... }: { + imports = [ + lib.shb.baseModule + ../../modules/blocks/monitoring.nix + ]; + test = { subdomain = "g"; }; @@ -49,6 +55,110 @@ let ssl = config.shb.certs.certs.selfsigned.n; }; }; + + ldap = + { config, ... }: + { + shb.monitoring = { + ldap = { + userGroup = "user_group"; + adminGroup = "admin_group"; + }; + }; + }; + + clientLoginSso = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + test = { + subdomain = "g"; + }; + + test.login = { + startUrl = "https://${config.test.fqdn}"; + usernameFieldLabelRegex = "Username"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('Welcome to Grafana')).to_be_visible()" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('Welcome to Grafana')).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "NotCharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "charlie"; + password = "CharliePassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" # I don't understand why this is not needed. Maybe it keeps somewhere the previous token? + "expect(page.get_by_text(re.compile('[Ll]ogin failed'))).to_be_visible(timeout=10000)" + ]; + } + ]; + }; + }; + + sso = + { config, ... }: + { + shb.monitoring = { + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + + sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result; + sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result; + }; + }; + + shb.hardcodedsecret.oidcSecret = { + request = config.shb.monitoring.sso.sharedSecret.request; + settings.content = oidcSecret; + }; + shb.hardcodedsecret.oidcAutheliaSecret = { + request = config.shb.monitoring.sso.sharedSecretForAuthelia.request; + settings.content = oidcSecret; + }; + }; in { basic = lib.shb.runNixOSTest { @@ -56,8 +166,6 @@ in nodes.server = { imports = [ - lib.shb.baseModule - ../../modules/blocks/monitoring.nix basic ]; }; @@ -72,10 +180,8 @@ in nodes.server = { imports = [ - lib.shb.baseModule - ../../modules/blocks/monitoring.nix - lib.shb.certs basic + lib.shb.certs https ]; }; @@ -84,4 +190,33 @@ in testScript = commonTestScript; }; + + sso = lib.shb.runNixOSTest { + name = "monitoring_sso"; + + nodes.client = { + imports = [ + clientLoginSso + ]; + + virtualisation.memorySize = 4096; + }; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + + # virtualisation.memorySize = 4096; + }; + + testScript = commonTestScript; + }; }