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
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

View file

@ -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"
],

View file

@ -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";
}
];
})
];
}

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.
- 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:

View file

@ -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;
};
}