monitoring: add sso integration

This commit is contained in:
ibizaman 2025-11-21 00:14:13 +01:00
parent 53d37404a7
commit 2425c1650b
5 changed files with 431 additions and 20 deletions

View file

@ -20,6 +20,7 @@ Template:
- Add a Grafana dashboard showing stats on backup jobs - 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. 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 ## Fixes

View file

@ -914,9 +914,6 @@
"blocks-monitoring-budget-alerts": [ "blocks-monitoring-budget-alerts": [
"blocks-monitoring.html#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-deluge-dashboard": [
"blocks-monitoring.html#blocks-monitoring-deluge-dashboard" "blocks-monitoring.html#blocks-monitoring-deluge-dashboard"
], ],
@ -968,6 +965,15 @@
"blocks-monitoring-options-shb.monitoring.grafanaPort": [ "blocks-monitoring-options-shb.monitoring.grafanaPort": [
"blocks-monitoring.html#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-options-shb.monitoring.lokiMajorVersion": [
"blocks-monitoring.html#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-options-shb.monitoring.ssl.systemdService": [
"blocks-monitoring.html#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-options-shb.monitoring.subdomain": [
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.subdomain" "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.subdomain"
], ],
@ -1052,6 +1121,15 @@
"blocks-monitoring-provisioning": [ "blocks-monitoring-provisioning": [
"blocks-monitoring.html#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": [
"blocks-postgresql.html#blocks-postgresql" "blocks-postgresql.html#blocks-postgresql"
], ],
@ -3386,6 +3464,9 @@
"services-karakeep-usage": [ "services-karakeep-usage": [
"services-karakeep.html#services-karakeep-usage" "services-karakeep.html#services-karakeep-usage"
], ],
"services-monitoring-features": [
"blocks-monitoring.html#services-monitoring-features"
],
"services-nextcloudserver": [ "services-nextcloudserver": [
"services-nextcloud.html#services-nextcloudserver" "services-nextcloud.html#services-nextcloudserver"
], ],

View file

@ -16,8 +16,22 @@ let
hostname = config.networking.hostName; hostname = config.networking.hostName;
domain = cfg.domain; domain = cfg.domain;
}; };
roleClaim = "grafana_groups";
oauthScopes = [
"openid"
"email"
"profile"
"groups"
"${roleClaim}"
];
in in
{ {
imports = [
../blocks/authelia.nix
../blocks/lldap.nix
];
options.shb.monitoring = { options.shb.monitoring = {
enable = lib.mkEnableOption "selfhostblocks.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 [ 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";
}
];
})
]; ];
} }

View file

@ -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. - Prometheus as the database for metrics.
- Loki as the database for logs. - 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 ```nix
shb.monitoring = { {
enable = true; shb.monitoring = {
subdomain = "grafana"; enable = true;
inherit domain; subdomain = "grafana";
contactPoints = [ "me@example.com" ]; inherit domain;
adminPassword.result = config.sops.secrets."monitoring/admin_password".result; contactPoints = [ "me@example.com" ];
secretKey.result = config.sops.secrets."monitoring/secret_key".result; 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 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: 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 Since all logs are now stored in Loki, you can probably reduce the systemd journal retention
time with: time with:

View file

@ -1,6 +1,7 @@
{ lib, ... }: { lib, ... }:
let let
password = "securepw"; password = "securepw";
oidcSecret = "oidcSecret";
commonTestScript = lib.shb.accessScript { commonTestScript = lib.shb.accessScript {
hasSSL = { node, ... }: !(isNull node.config.shb.monitoring.ssl); hasSSL = { node, ... }: !(isNull node.config.shb.monitoring.ssl);
@ -19,6 +20,11 @@ let
basic = basic =
{ config, ... }: { config, ... }:
{ {
imports = [
lib.shb.baseModule
../../modules/blocks/monitoring.nix
];
test = { test = {
subdomain = "g"; subdomain = "g";
}; };
@ -49,6 +55,110 @@ let
ssl = config.shb.certs.certs.selfsigned.n; 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 in
{ {
basic = lib.shb.runNixOSTest { basic = lib.shb.runNixOSTest {
@ -56,8 +166,6 @@ in
nodes.server = { nodes.server = {
imports = [ imports = [
lib.shb.baseModule
../../modules/blocks/monitoring.nix
basic basic
]; ];
}; };
@ -72,10 +180,8 @@ in
nodes.server = { nodes.server = {
imports = [ imports = [
lib.shb.baseModule
../../modules/blocks/monitoring.nix
lib.shb.certs
basic basic
lib.shb.certs
https https
]; ];
}; };
@ -84,4 +190,33 @@ in
testScript = commonTestScript; 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;
};
} }