depot/third_party/nixpkgs/nixos/modules/services/monitoring/grafana.nix

1801 lines
73 KiB
Nix

{ options, config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.grafana;
opt = options.services.grafana;
provisioningSettingsFormat = pkgs.formats.yaml { };
declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
useMysql = cfg.settings.database.type == "mysql";
usePostgresql = cfg.settings.database.type == "postgres";
# Prefer using the values from the default config file[0] directly. This way,
# people reading the NixOS manual can see them without cross-referencing the
# official documentation.
#
# However, if there is no default entry or if the setting is optional, use
# `null` as the default value. It will be turned into the empty string.
#
# If a setting is a list, always allow setting it as a plain string as well.
#
# [0]: https://github.com/grafana/grafana/blob/main/conf/defaults.ini
settingsFormatIni = pkgs.formats.ini {
listToValue = concatMapStringsSep " " (generators.mkValueStringDefault { });
mkKeyValue = generators.mkKeyValueDefault
{
mkValueString = v:
if v == null then ""
else generators.mkValueStringDefault { } v;
}
"=";
};
configFile = settingsFormatIni.generate "config.ini" cfg.settings;
mkProvisionCfg = name: attr: provisionCfg:
if provisionCfg.path != null
then provisionCfg.path
else
provisioningSettingsFormat.generate "${name}.yaml"
(if provisionCfg.settings != null
then provisionCfg.settings
else {
apiVersion = 1;
${attr} = [ ];
});
datasourceFileOrDir = mkProvisionCfg "datasource" "datasources" cfg.provision.datasources;
dashboardFileOrDir = mkProvisionCfg "dashboard" "providers" cfg.provision.dashboards;
generateAlertingProvisioningYaml = x:
if (cfg.provision.alerting."${x}".path == null)
then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings
else cfg.provision.alerting."${x}".path;
rulesFileOrDir = generateAlertingProvisioningYaml "rules";
contactPointsFileOrDir = generateAlertingProvisioningYaml "contactPoints";
policiesFileOrDir = generateAlertingProvisioningYaml "policies";
templatesFileOrDir = generateAlertingProvisioningYaml "templates";
muteTimingsFileOrDir = generateAlertingProvisioningYaml "muteTimings";
ln = { src, dir, filename }: ''
if [[ -d "${src}" ]]; then
pushd $out/${dir} &>/dev/null
lndir "${src}"
popd &>/dev/null
else
ln -sf ${src} $out/${dir}/${filename}.yaml
fi
'';
provisionConfDir = pkgs.runCommand "grafana-provisioning" { nativeBuildInputs = [ pkgs.xorg.lndir ]; } ''
mkdir -p $out/{alerting,datasources,dashboards,plugins}
${ln { src = datasourceFileOrDir; dir = "datasources"; filename = "datasource"; }}
${ln { src = dashboardFileOrDir; dir = "dashboards"; filename = "dashboard"; }}
${ln { src = rulesFileOrDir; dir = "alerting"; filename = "rules"; }}
${ln { src = contactPointsFileOrDir; dir = "alerting"; filename = "contactPoints"; }}
${ln { src = policiesFileOrDir; dir = "alerting"; filename = "policies"; }}
${ln { src = templatesFileOrDir; dir = "alerting"; filename = "templates"; }}
${ln { src = muteTimingsFileOrDir; dir = "alerting"; filename = "muteTimings"; }}
'';
# Get a submodule without any embedded metadata:
_filter = x: filterAttrs (k: v: k != "_module") x;
# https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources
grafanaTypes.datasourceConfig = types.submodule {
freeformType = provisioningSettingsFormat.type;
options = {
name = mkOption {
type = types.str;
description = "Name of the datasource. Required.";
};
type = mkOption {
type = types.str;
description = "Datasource type. Required.";
};
access = mkOption {
type = types.enum [ "proxy" "direct" ];
default = "proxy";
description = "Access mode. proxy or direct (Server or Browser in the UI). Required.";
};
uid = mkOption {
type = types.nullOr types.str;
default = null;
description = "Custom UID which can be used to reference this datasource in other parts of the configuration, if not specified will be generated automatically.";
};
url = mkOption {
type = types.str;
default = "";
description = "Url of the datasource.";
};
editable = mkOption {
type = types.bool;
default = false;
description = "Allow users to edit datasources from the UI.";
};
jsonData = mkOption {
type = types.nullOr types.attrs;
default = null;
description = "Extra data for datasource plugins.";
};
secureJsonData = mkOption {
type = types.nullOr types.attrs;
default = null;
description = ''
Datasource specific secure configuration. Please note that the contents of this option
will end up in a world-readable Nix store. Use the file provider
pointing at a reasonably secured file in the local filesystem
to work around that. Look at the documentation for details:
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
'';
};
};
};
# https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards
grafanaTypes.dashboardConfig = types.submodule {
freeformType = provisioningSettingsFormat.type;
options = {
name = mkOption {
type = types.str;
default = "default";
description = "A unique provider name.";
};
type = mkOption {
type = types.str;
default = "file";
description = "Dashboard provider type.";
};
options.path = mkOption {
type = types.path;
description = "Path grafana will watch for dashboards. Required when using the 'file' type.";
};
};
};
in
{
imports = [
(mkRemovedOptionModule [ "services" "grafana" "provision" "notifiers" ] ''
Notifiers (services.grafana.provision.notifiers) were removed in Grafana 11.
'')
(mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ])
(mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ])
(mkRenamedOptionModule [ "services" "grafana" "port" ] [ "services" "grafana" "settings" "server" "http_port" ])
(mkRenamedOptionModule [ "services" "grafana" "domain" ] [ "services" "grafana" "settings" "server" "domain" ])
(mkRenamedOptionModule [ "services" "grafana" "rootUrl" ] [ "services" "grafana" "settings" "server" "root_url" ])
(mkRenamedOptionModule [ "services" "grafana" "staticRootPath" ] [ "services" "grafana" "settings" "server" "static_root_path" ])
(mkRenamedOptionModule [ "services" "grafana" "certFile" ] [ "services" "grafana" "settings" "server" "cert_file" ])
(mkRenamedOptionModule [ "services" "grafana" "certKey" ] [ "services" "grafana" "settings" "server" "cert_key" ])
(mkRenamedOptionModule [ "services" "grafana" "socket" ] [ "services" "grafana" "settings" "server" "socket" ])
(mkRenamedOptionModule [ "services" "grafana" "database" "type" ] [ "services" "grafana" "settings" "database" "type" ])
(mkRenamedOptionModule [ "services" "grafana" "database" "host" ] [ "services" "grafana" "settings" "database" "host" ])
(mkRenamedOptionModule [ "services" "grafana" "database" "name" ] [ "services" "grafana" "settings" "database" "name" ])
(mkRenamedOptionModule [ "services" "grafana" "database" "user" ] [ "services" "grafana" "settings" "database" "user" ])
(mkRenamedOptionModule [ "services" "grafana" "database" "password" ] [ "services" "grafana" "settings" "database" "password" ])
(mkRenamedOptionModule [ "services" "grafana" "database" "path" ] [ "services" "grafana" "settings" "database" "path" ])
(mkRenamedOptionModule [ "services" "grafana" "database" "connMaxLifetime" ] [ "services" "grafana" "settings" "database" "conn_max_lifetime" ])
(mkRenamedOptionModule [ "services" "grafana" "security" "adminUser" ] [ "services" "grafana" "settings" "security" "admin_user" ])
(mkRenamedOptionModule [ "services" "grafana" "security" "adminPassword" ] [ "services" "grafana" "settings" "security" "admin_password" ])
(mkRenamedOptionModule [ "services" "grafana" "security" "secretKey" ] [ "services" "grafana" "settings" "security" "secret_key" ])
(mkRenamedOptionModule [ "services" "grafana" "server" "serveFromSubPath" ] [ "services" "grafana" "settings" "server" "serve_from_sub_path" ])
(mkRenamedOptionModule [ "services" "grafana" "smtp" "enable" ] [ "services" "grafana" "settings" "smtp" "enabled" ])
(mkRenamedOptionModule [ "services" "grafana" "smtp" "user" ] [ "services" "grafana" "settings" "smtp" "user" ])
(mkRenamedOptionModule [ "services" "grafana" "smtp" "password" ] [ "services" "grafana" "settings" "smtp" "password" ])
(mkRenamedOptionModule [ "services" "grafana" "smtp" "fromAddress" ] [ "services" "grafana" "settings" "smtp" "from_address" ])
(mkRenamedOptionModule [ "services" "grafana" "users" "allowSignUp" ] [ "services" "grafana" "settings" "users" "allow_sign_up" ])
(mkRenamedOptionModule [ "services" "grafana" "users" "allowOrgCreate" ] [ "services" "grafana" "settings" "users" "allow_org_create" ])
(mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrg" ] [ "services" "grafana" "settings" "users" "auto_assign_org" ])
(mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrgRole" ] [ "services" "grafana" "settings" "users" "auto_assign_org_role" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "disableLoginForm" ] [ "services" "grafana" "settings" "auth" "disable_login_form" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "enable" ] [ "services" "grafana" "settings" "auth.anonymous" "enabled" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_name" ] [ "services" "grafana" "settings" "auth.anonymous" "org_name" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_role" ] [ "services" "grafana" "settings" "auth.anonymous" "org_role" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "enable" ] [ "services" "grafana" "settings" "auth.azuread" "enabled" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowSignUp" ] [ "services" "grafana" "settings" "auth.azuread" "allow_sign_up" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "clientId" ] [ "services" "grafana" "settings" "auth.azuread" "client_id" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedDomains" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_domains" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedGroups" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_groups" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "google" "enable" ] [ "services" "grafana" "settings" "auth.google" "enabled" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "google" "allowSignUp" ] [ "services" "grafana" "settings" "auth.google" "allow_sign_up" ])
(mkRenamedOptionModule [ "services" "grafana" "auth" "google" "clientId" ] [ "services" "grafana" "settings" "auth.google" "client_id" ])
(mkRenamedOptionModule [ "services" "grafana" "analytics" "reporting" "enable" ] [ "services" "grafana" "settings" "analytics" "reporting_enabled" ])
(mkRemovedOptionModule [ "services" "grafana" "database" "passwordFile" ] ''
This option has been removed. Use 'services.grafana.settings.database.password' with file provider instead.
'')
(mkRemovedOptionModule [ "services" "grafana" "security" "adminPasswordFile" ] ''
This option has been removed. Use 'services.grafana.settings.security.admin_password' with file provider instead.
'')
(mkRemovedOptionModule [ "services" "grafana" "security" "secretKeyFile" ] ''
This option has been removed. Use 'services.grafana.settings.security.secret_key' with file provider instead.
'')
(mkRemovedOptionModule [ "services" "grafana" "smtp" "passwordFile" ] ''
This option has been removed. Use 'services.grafana.settings.smtp.password' with file provider instead.
'')
(mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "clientSecretFile" ] ''
This option has been removed. Use 'services.grafana.settings.azuread.client_secret' with file provider instead.
'')
(mkRemovedOptionModule [ "services" "grafana" "auth" "google" "clientSecretFile" ] ''
This option has been removed. Use 'services.grafana.settings.google.client_secret' with file provider instead.
'')
(mkRemovedOptionModule [ "services" "grafana" "extraOptions" ] ''
This option has been removed. Use 'services.grafana.settings' instead. For a detailed migration guide, please
review the release notes of NixOS 22.11.
'')
(mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "tenantId" ] "This option has been deprecated upstream.")
];
options.services.grafana = {
enable = mkEnableOption "grafana";
declarativePlugins = mkOption {
type = with types; nullOr (listOf path);
default = null;
description = "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed.";
example = literalExpression "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]";
# Make sure each plugin is added only once; otherwise building
# the link farm fails, since the same path is added multiple
# times.
apply = x: if isList x then lib.unique x else x;
};
package = mkPackageOption pkgs "grafana" { };
dataDir = mkOption {
description = "Data directory.";
default = "/var/lib/grafana";
type = types.path;
};
settings = mkOption {
description = ''
Grafana settings. See <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/>
for available options. INI format is used.
'';
default = { };
type = types.submodule {
freeformType = settingsFormatIni.type;
options = {
paths = {
plugins = mkOption {
description = "Directory where grafana will automatically scan and look for plugins";
default = if (cfg.declarativePlugins == null) then "${cfg.dataDir}/plugins" else declarativePlugins;
defaultText = literalExpression "if (cfg.declarativePlugins == null) then \"\${cfg.dataDir}/plugins\" else declarativePlugins";
type = types.path;
};
provisioning = mkOption {
description = ''
Folder that contains provisioning config files that grafana will apply on startup and while running.
Don't change the value of this option if you are planning to use `services.grafana.provision` options.
'';
default = provisionConfDir;
defaultText = "directory with links to files generated from services.grafana.provision";
type = types.path;
};
};
server = {
protocol = mkOption {
description = "Which protocol to listen.";
default = "http";
type = types.enum [ "http" "https" "h2" "socket" ];
};
http_addr = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Listening address.
::: {.note}
This setting intentionally varies from upstream's default to be a bit more secure by default.
:::
'';
};
http_port = mkOption {
description = "Listening port.";
default = 3000;
type = types.port;
};
domain = mkOption {
description = ''
The public facing domain name used to access grafana from a browser.
This setting is only used in the default value of the `root_url` setting.
If you set the latter manually, this option does not have to be specified.
'';
default = "localhost";
type = types.str;
};
enforce_domain = mkOption {
description = ''
Redirect to correct domain if the host header does not match the domain.
Prevents DNS rebinding attacks.
'';
default = false;
type = types.bool;
};
root_url = mkOption {
description = ''
This is the full URL used to access Grafana from a web browser.
This is important if you use Google or GitHub OAuth authentication (for the callback URL to be correct).
This setting is also important if you have a reverse proxy in front of Grafana that exposes it through a subpath.
In that case add the subpath to the end of this URL setting.
'';
default = "%(protocol)s://%(domain)s:%(http_port)s/";
type = types.str;
};
serve_from_sub_path = mkOption {
description = ''
Serve Grafana from subpath specified in the `root_url` setting.
By default it is set to `false` for compatibility reasons.
By enabling this setting and using a subpath in `root_url` above,
e.g. `root_url = "http://localhost:3000/grafana"`,
Grafana is accessible on `http://localhost:3000/grafana`.
If accessed without subpath, Grafana will redirect to an URL with the subpath.
'';
default = false;
type = types.bool;
};
router_logging = mkOption {
description = ''
Set to `true` for Grafana to log all HTTP requests (not just errors).
These are logged as Info level events to the Grafana log.
'';
default = false;
type = types.bool;
};
static_root_path = mkOption {
description = "Root path for static assets.";
default = "${cfg.package}/share/grafana/public";
defaultText = literalExpression ''"''${package}/share/grafana/public"'';
type = types.str;
};
enable_gzip = mkOption {
description = ''
Set this option to `true` to enable HTTP compression, this can improve transfer speed and bandwidth utilization.
It is recommended that most users set it to `true`. By default it is set to `false` for compatibility reasons.
'';
default = false;
type = types.bool;
};
cert_file = mkOption {
description = ''
Path to the certificate file (if `protocol` is set to `https` or `h2`).
'';
default = null;
type = types.nullOr types.str;
};
cert_key = mkOption {
description = ''
Path to the certificate key file (if `protocol` is set to `https` or `h2`).
'';
default = null;
type = types.nullOr types.str;
};
socket_gid = mkOption {
description = ''
GID where the socket should be set when `protocol=socket`.
Make sure that the target group is in the group of Grafana process and that Grafana process is the file owner before you change this setting.
It is recommended to set the gid as http server user gid.
Not set when the value is -1.
'';
default = -1;
type = types.int;
};
socket_mode = mkOption {
description = ''
Mode where the socket should be set when `protocol=socket`.
Make sure that Grafana process is the file owner before you change this setting.
'';
# I assume this value is interpreted as octal literal by grafana.
# If this was an int, people following tutorials or porting their
# old config could stumble across nix not having octal literals.
default = "0660";
type = types.str;
};
socket = mkOption {
description = ''
Path where the socket should be created when `protocol=socket`.
Make sure that Grafana has appropriate permissions before you change this setting.
'';
default = "/run/grafana/grafana.sock";
type = types.str;
};
cdn_url = mkOption {
description = ''
Specify a full HTTP URL address to the root of your Grafana CDN assets.
Grafana will add edition and version paths.
For example, given a cdn url like `https://cdn.myserver.com`
grafana will try to load a javascript file from `http://cdn.myserver.com/grafana-oss/7.4.0/public/build/app.<hash>.js`.
'';
default = null;
type = types.nullOr types.str;
};
read_timeout = mkOption {
description = ''
Sets the maximum time using a duration format (5s/5m/5ms)
before timing out read of an incoming request and closing idle connections.
0 means there is no timeout for reading the request.
'';
default = "0";
type = types.str;
};
};
database = {
type = mkOption {
description = "Database type.";
default = "sqlite3";
type = types.enum [ "mysql" "sqlite3" "postgres" ];
};
host = mkOption {
description = ''
Only applicable to MySQL or Postgres.
Includes IP or hostname and port or in case of Unix sockets the path to it.
For example, for MySQL running on the same host as Grafana: `host = "127.0.0.1:3306"`
or with Unix sockets: `host = "/var/run/mysqld/mysqld.sock"`
'';
default = "127.0.0.1:3306";
type = types.str;
};
name = mkOption {
description = "The name of the Grafana database.";
default = "grafana";
type = types.str;
};
user = mkOption {
description = "The database user (not applicable for `sqlite3`).";
default = "root";
type = types.str;
};
password = mkOption {
description = ''
The database user's password (not applicable for `sqlite3`).
Please note that the contents of this option
will end up in a world-readable Nix store. Use the file provider
pointing at a reasonably secured file in the local filesystem
to work around that. Look at the documentation for details:
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
'';
default = "";
type = types.str;
};
max_idle_conn = mkOption {
description = "The maximum number of connections in the idle connection pool.";
default = 2;
type = types.int;
};
max_open_conn = mkOption {
description = "The maximum number of open connections to the database.";
default = 0;
type = types.int;
};
conn_max_lifetime = mkOption {
description = ''
Sets the maximum amount of time a connection may be reused.
The default is 14400 (which means 14400 seconds or 4 hours).
For MySQL, this setting should be shorter than the `wait_timeout` variable.
'';
default = 14400;
type = types.int;
};
locking_attempt_timeout_sec = mkOption {
description = ''
For `mysql`, if the `migrationLocking` feature toggle is set,
specify the time (in seconds) to wait before failing to lock the database for the migrations.
'';
default = 0;
type = types.int;
};
log_queries = mkOption {
description = "Set to `true` to log the sql calls and execution times";
default = false;
type = types.bool;
};
ssl_mode = mkOption {
description = ''
For Postgres, use either `disable`, `require` or `verify-full`.
For MySQL, use either `true`, `false`, or `skip-verify`.
'';
default = "disable";
type = types.enum [ "disable" "require" "verify-full" "true" "false" "skip-verify" ];
};
isolation_level = mkOption {
description = ''
Only the MySQL driver supports isolation levels in Grafana.
In case the value is empty, the driver's default isolation level is applied.
'';
default = null;
type = types.nullOr (types.enum [ "READ-UNCOMMITTED" "READ-COMMITTED" "REPEATABLE-READ" "SERIALIZABLE" ]);
};
ca_cert_path = mkOption {
description = "The path to the CA certificate to use.";
default = null;
type = types.nullOr types.str;
};
client_key_path = mkOption {
description = "The path to the client key. Only if server requires client authentication.";
default = null;
type = types.nullOr types.str;
};
client_cert_path = mkOption {
description = "The path to the client cert. Only if server requires client authentication.";
default = null;
type = types.nullOr types.str;
};
server_cert_name = mkOption {
description = ''
The common name field of the certificate used by the `mysql` or `postgres` server.
Not necessary if `ssl_mode` is set to `skip-verify`.
'';
default = null;
type = types.nullOr types.str;
};
path = mkOption {
description = "Only applicable to `sqlite3` database. The file path where the database will be stored.";
default = "${cfg.dataDir}/data/grafana.db";
defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
type = types.path;
};
cache_mode = mkOption {
description = ''
For `sqlite3` only.
[Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database.
'';
default = "private";
type = types.enum [ "private" "shared" ];
};
wal = mkOption {
description = ''
For `sqlite3` only.
Setting to enable/disable [Write-Ahead Logging](https://sqlite.org/wal.html).
'';
default = false;
type = types.bool;
};
query_retries = mkOption {
description = ''
This setting applies to `sqlite3` only and controls the number of times the system retries a query when the database is locked.
'';
default = 0;
type = types.int;
};
transaction_retries = mkOption {
description = ''
This setting applies to `sqlite3` only and controls the number of times the system retries a transaction when the database is locked.
'';
default = 5;
type = types.int;
};
# TODO Add "instrument_queries" option when upgrading to grafana 10.0
# instrument_queries = mkOption {
# description = "Set to `true` to add metrics and tracing for database queries.";
# default = false;
# type = types.bool;
# };
};
security = {
disable_initial_admin_creation = mkOption {
description = "Disable creation of admin user on first start of Grafana.";
default = false;
type = types.bool;
};
admin_user = mkOption {
description = "Default admin username.";
default = "admin";
type = types.str;
};
admin_password = mkOption {
description = ''
Default admin password. Please note that the contents of this option
will end up in a world-readable Nix store. Use the file provider
pointing at a reasonably secured file in the local filesystem
to work around that. Look at the documentation for details:
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
'';
default = "admin";
type = types.str;
};
admin_email = mkOption {
description = "The email of the default Grafana Admin, created on startup.";
default = "admin@localhost";
type = types.str;
};
secret_key = mkOption {
description = ''
Secret key used for signing. Please note that the contents of this option
will end up in a world-readable Nix store. Use the file provider
pointing at a reasonably secured file in the local filesystem
to work around that. Look at the documentation for details:
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
'';
default = "SW2YcwTIb9zpOOhoPsMm";
type = types.str;
};
disable_gravatar = mkOption {
description = "Set to `true` to disable the use of Gravatar for user profile images.";
default = false;
type = types.bool;
};
data_source_proxy_whitelist = mkOption {
description = ''
Define a whitelist of allowed IP addresses or domains, with ports,
to be used in data source URLs with the Grafana data source proxy.
Format: `ip_or_domain:port` separated by spaces.
PostgreSQL, MySQL, and MSSQL data sources do not use the proxy and are therefore unaffected by this setting.
'';
default = [ ];
type = types.oneOf [ types.str (types.listOf types.str) ];
};
disable_brute_force_login_protection = mkOption {
description = "Set to `true` to disable [brute force login protection](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout).";
default = false;
type = types.bool;
};
cookie_secure = mkOption {
description = "Set to `true` if you host Grafana behind HTTPS.";
default = false;
type = types.bool;
};
cookie_samesite = mkOption {
description = ''
Sets the `SameSite` cookie attribute and prevents the browser from sending this cookie along with cross-site requests.
The main goal is to mitigate the risk of cross-origin information leakage.
This setting also provides some protection against cross-site request forgery attacks (CSRF),
[read more about SameSite here](https://owasp.org/www-community/SameSite).
Using value `disabled` does not add any `SameSite` attribute to cookies.
'';
default = "lax";
type = types.enum [ "lax" "strict" "none" "disabled" ];
};
allow_embedding = mkOption {
description = ''
When `false`, the HTTP header `X-Frame-Options: deny` will be set in Grafana HTTP responses
which will instruct browsers to not allow rendering Grafana in a `<frame>`, `<iframe>`, `<embed>` or `<object>`.
The main goal is to mitigate the risk of [Clickjacking](https://owasp.org/www-community/attacks/Clickjacking).
'';
default = false;
type = types.bool;
};
strict_transport_security = mkOption {
description = ''
Set to `true` if you want to enable HTTP `Strict-Transport-Security` (HSTS) response header.
Only use this when HTTPS is enabled in your configuration,
or when there is another upstream system that ensures your application does HTTPS (like a frontend load balancer).
HSTS tells browsers that the site should only be accessed using HTTPS.
'';
default = false;
type = types.bool;
};
strict_transport_security_max_age_seconds = mkOption {
description = ''
Sets how long a browser should cache HSTS in seconds.
Only applied if `strict_transport_security` is enabled.
'';
default = 86400;
type = types.int;
};
strict_transport_security_preload = mkOption {
description = ''
Set to `true` to enable HSTS `preloading` option.
Only applied if `strict_transport_security` is enabled.
'';
default = false;
type = types.bool;
};
strict_transport_security_subdomains = mkOption {
description = ''
Set to `true` to enable HSTS `includeSubDomains` option.
Only applied if `strict_transport_security` is enabled.
'';
default = false;
type = types.bool;
};
x_content_type_options = mkOption {
description = ''
Set to `false` to disable the `X-Content-Type-Options` response header.
The `X-Content-Type-Options` response HTTP header is a marker used by the server
to indicate that the MIME types advertised in the `Content-Type` headers should not be changed and be followed.
'';
default = true;
type = types.bool;
};
x_xss_protection = mkOption {
description = ''
Set to `false` to disable the `X-XSS-Protection` header,
which tells browsers to stop pages from loading when they detect reflected cross-site scripting (XSS) attacks.
'';
default = true;
type = types.bool;
};
content_security_policy = mkOption {
description = ''
Set to `true` to add the `Content-Security-Policy` header to your requests.
CSP allows to control resources that the user agent can load and helps prevent XSS attacks.
'';
default = false;
type = types.bool;
};
content_security_policy_report_only = mkOption {
description = ''
Set to `true` to add the `Content-Security-Policy-Report-Only` header to your requests.
CSP in Report Only mode enables you to experiment with policies by monitoring their effects without enforcing them.
You can enable both policies simultaneously.
'';
default = false;
type = types.bool;
};
# The options content_security_policy_template and
# content_security_policy_template are missing because I'm not sure
# how exactly the quoting of the default value works. See also
# https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L364
# https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L373
# These two options are lists joined with spaces:
# https://github.com/grafana/grafana/blob/916d9793aa81c2990640b55a15dee0db6b525e41/pkg/middleware/csrf/csrf.go#L37-L38
csrf_trusted_origins = mkOption {
description = ''
List of additional allowed URLs to pass by the CSRF check.
Suggested when authentication comes from an IdP.
'';
default = [ ];
type = types.oneOf [ types.str (types.listOf types.str) ];
};
csrf_additional_headers = mkOption {
description = ''
List of allowed headers to be set by the user.
Suggested to use for if authentication lives behind reverse proxies.
'';
default = [ ];
type = types.oneOf [ types.str (types.listOf types.str) ];
};
};
smtp = {
enabled = mkOption {
description = "Whether to enable SMTP.";
default = false;
type = types.bool;
};
host = mkOption {
description = "Host to connect to.";
default = "localhost:25";
type = types.str;
};
user = mkOption {
description = "User used for authentication.";
default = null;
type = types.nullOr types.str;
};
password = mkOption {
description = ''
Password used for authentication. Please note that the contents of this option
will end up in a world-readable Nix store. Use the file provider
pointing at a reasonably secured file in the local filesystem
to work around that. Look at the documentation for details:
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
'';
default = "";
type = types.str;
};
cert_file = mkOption {
description = "File path to a cert file.";
default = null;
type = types.nullOr types.str;
};
key_file = mkOption {
description = "File path to a key file.";
default = null;
type = types.nullOr types.str;
};
skip_verify = mkOption {
description = "Verify SSL for SMTP server.";
default = false;
type = types.bool;
};
from_address = mkOption {
description = "Address used when sending out emails.";
default = "admin@grafana.localhost";
type = types.str;
};
from_name = mkOption {
description = "Name to be used as client identity for EHLO in SMTP dialog.";
default = "Grafana";
type = types.str;
};
ehlo_identity = mkOption {
description = "Name to be used as client identity for EHLO in SMTP dialog.";
default = null;
type = types.nullOr types.str;
};
startTLS_policy = mkOption {
description = "StartTLS policy when connecting to server.";
default = null;
type = types.nullOr (types.enum [ "OpportunisticStartTLS" "MandatoryStartTLS" "NoStartTLS" ]);
};
};
users = {
allow_sign_up = mkOption {
description = ''
Set to false to prohibit users from being able to sign up / create user accounts.
The admin user can still create users.
'';
default = false;
type = types.bool;
};
allow_org_create = mkOption {
description = "Set to `false` to prohibit users from creating new organizations.";
default = false;
type = types.bool;
};
auto_assign_org = mkOption {
description = ''
Set to `true` to automatically add new users to the main organization (id 1).
When set to `false,` new users automatically cause a new organization to be created for that new user.
The organization will be created even if the `allow_org_create` setting is set to `false`.
'';
default = true;
type = types.bool;
};
auto_assign_org_id = mkOption {
description = ''
Set this value to automatically add new users to the provided org.
This requires `auto_assign_org` to be set to `true`.
Please make sure that this organization already exists.
'';
default = 1;
type = types.int;
};
auto_assign_org_role = mkOption {
description = ''
The role new users will be assigned for the main organization (if the `auto_assign_org` setting is set to `true`).
'';
default = "Viewer";
type = types.enum [ "Viewer" "Editor" "Admin" ];
};
verify_email_enabled = mkOption {
description = "Require email validation before sign up completes.";
default = false;
type = types.bool;
};
login_hint = mkOption {
description = "Text used as placeholder text on login page for login/username input.";
default = "email or username";
type = types.str;
};
password_hint = mkOption {
description = "Text used as placeholder text on login page for password input.";
default = "password";
type = types.str;
};
default_theme = mkOption {
description = "Sets the default UI theme. `system` matches the user's system theme.";
default = "dark";
type = types.enum [ "dark" "light" "system" ];
};
default_language = mkOption {
description = "This setting configures the default UI language, which must be a supported IETF language tag, such as `en-US`.";
default = "en-US";
type = types.str;
};
home_page = mkOption {
description = ''
Path to a custom home page.
Users are only redirected to this if the default home dashboard is used.
It should match a frontend route and contain a leading slash.
'';
default = "";
type = types.str;
};
viewers_can_edit = mkOption {
description = ''
Viewers can access and use Explore and perform temporary edits on panels in dashboards they have access to.
They cannot save their changes.
'';
default = false;
type = types.bool;
};
editors_can_admin = mkOption {
description = "Editors can administrate dashboards, folders and teams they create.";
default = false;
type = types.bool;
};
user_invite_max_lifetime_duration = mkOption {
description = ''
The duration in time a user invitation remains valid before expiring.
This setting should be expressed as a duration.
Examples: `6h` (hours), `2d` (days), `1w` (week).
The minimum supported duration is `15m` (15 minutes).
'';
default = "24h";
type = types.str;
};
# Lists are joined via space, so this option can't be a list.
# Users have to manually join their values.
hidden_users = mkOption {
description = ''
This is a comma-separated list of usernames.
Users specified here are hidden in the Grafana UI.
They are still visible to Grafana administrators and to themselves.
'';
default = "";
type = types.str;
};
};
analytics = {
reporting_enabled = mkOption {
description = ''
When enabled Grafana will send anonymous usage statistics to `stats.grafana.org`.
No IP addresses are being tracked, only simple counters to track running instances, versions, dashboard and error counts.
Counters are sent every 24 hours.
'';
default = true;
type = types.bool;
};
check_for_updates = mkOption {
description = ''
When set to `false`, disables checking for new versions of Grafana from Grafana's GitHub repository.
When enabled, the check for a new version runs every 10 minutes.
It will notify, via the UI, when a new version is available.
The check itself will not prompt any auto-updates of the Grafana software, nor will it send any sensitive information.
'';
default = false;
type = types.bool;
};
check_for_plugin_updates = mkOption {
description = ''
When set to `false`, disables checking for new versions of installed plugins from https://grafana.com.
When enabled, the check for a new plugin runs every 10 minutes.
It will notify, via the UI, when a new plugin update exists.
The check itself will not prompt any auto-updates of the plugin, nor will it send any sensitive information.
'';
default = cfg.declarativePlugins == null;
defaultText = literalExpression "cfg.declarativePlugins == null";
type = types.bool;
};
feedback_links_enabled = mkOption {
description = "Set to `false` to remove all feedback links from the UI.";
default = true;
type = types.bool;
};
};
};
};
};
provision = {
enable = mkEnableOption "provision";
datasources = mkOption {
description = ''
Declaratively provision Grafana's datasources.
'';
default = { };
type = types.submodule {
options.settings = mkOption {
description = ''
Grafana datasource configuration in Nix. Can't be used with
[](#opt-services.grafana.provision.datasources.path) simultaneously. See
<https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources>
for supported options.
'';
default = null;
type = types.nullOr (types.submodule {
options = {
apiVersion = mkOption {
description = "Config file version.";
default = 1;
type = types.int;
};
datasources = mkOption {
description = "List of datasources to insert/update.";
default = [ ];
type = types.listOf grafanaTypes.datasourceConfig;
};
deleteDatasources = mkOption {
description = "List of datasources that should be deleted from the database.";
default = [ ];
type = types.listOf (types.submodule {
options.name = mkOption {
description = "Name of the datasource to delete.";
type = types.str;
};
options.orgId = mkOption {
description = "Organization ID of the datasource to delete.";
type = types.int;
};
});
};
};
});
example = literalExpression ''
{
apiVersion = 1;
datasources = [{
name = "Graphite";
type = "graphite";
}];
deleteDatasources = [{
name = "Graphite";
orgId = 1;
}];
}
'';
};
options.path = mkOption {
description = ''
Path to YAML datasource configuration. Can't be used with
[](#opt-services.grafana.provision.datasources.settings) simultaneously.
Can be either a directory or a single YAML file. Will end up in the store.
'';
default = null;
type = types.nullOr types.path;
};
};
};
dashboards = mkOption {
description = ''
Declaratively provision Grafana's dashboards.
'';
default = { };
type = types.submodule {
options.settings = mkOption {
description = ''
Grafana dashboard configuration in Nix. Can't be used with
[](#opt-services.grafana.provision.dashboards.path) simultaneously. See
<https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards>
for supported options.
'';
default = null;
type = types.nullOr (types.submodule {
options.apiVersion = mkOption {
description = "Config file version.";
default = 1;
type = types.int;
};
options.providers = mkOption {
description = "List of dashboards to insert/update.";
default = [ ];
type = types.listOf grafanaTypes.dashboardConfig;
};
});
example = literalExpression ''
{
apiVersion = 1;
providers = [{
name = "default";
options.path = "/var/lib/grafana/dashboards";
}];
}
'';
};
options.path = mkOption {
description = ''
Path to YAML dashboard configuration. Can't be used with
[](#opt-services.grafana.provision.dashboards.settings) simultaneously.
Can be either a directory or a single YAML file. Will end up in the store.
'';
default = null;
type = types.nullOr types.path;
};
};
};
alerting = {
rules = {
path = mkOption {
description = ''
Path to YAML rules configuration. Can't be used with
[](#opt-services.grafana.provision.alerting.rules.settings) simultaneously.
Can be either a directory or a single YAML file. Will end up in the store.
'';
default = null;
type = types.nullOr types.path;
};
settings = mkOption {
description = ''
Grafana rules configuration in Nix. Can't be used with
[](#opt-services.grafana.provision.alerting.rules.path) simultaneously. See
<https://grafana.com/docs/grafana/latest/administration/provisioning/#rules>
for supported options.
'';
default = null;
type = types.nullOr (types.submodule {
options = {
apiVersion = mkOption {
description = "Config file version.";
default = 1;
type = types.int;
};
groups = mkOption {
description = "List of rule groups to import or update.";
default = [ ];
type = types.listOf (types.submodule {
freeformType = provisioningSettingsFormat.type;
options.name = mkOption {
description = "Name of the rule group. Required.";
type = types.str;
};
options.folder = mkOption {
description = "Name of the folder the rule group will be stored in. Required.";
type = types.str;
};
options.interval = mkOption {
description = "Interval that the rule group should be evaluated at. Required.";
type = types.str;
};
});
};
deleteRules = mkOption {
description = "List of alert rule UIDs that should be deleted.";
default = [ ];
type = types.listOf (types.submodule {
options.orgId = mkOption {
description = "Organization ID, default = 1";
default = 1;
type = types.int;
};
options.uid = mkOption {
description = "Unique identifier for the rule. Required.";
type = types.str;
};
});
};
};
});
example = literalExpression ''
{
apiVersion = 1;
groups = [{
orgId = 1;
name = "my_rule_group";
folder = "my_first_folder";
interval = "60s";
rules = [{
uid = "my_id_1";
title = "my_first_rule";
condition = "A";
data = [{
refId = "A";
datasourceUid = "-100";
model = {
conditions = [{
evaluator = {
params = [ 3 ];
type = "git";
};
operator.type = "and";
query.params = [ "A" ];
reducer.type = "last";
type = "query";
}];
datasource = {
type = "__expr__";
uid = "-100";
};
expression = "1==0";
intervalMs = 1000;
maxDataPoints = 43200;
refId = "A";
type = "math";
};
}];
dashboardUid = "my_dashboard";
panelId = 123;
noDataState = "Alerting";
for = "60s";
annotations.some_key = "some_value";
labels.team = "sre_team1";
}];
}];
deleteRules = [{
orgId = 1;
uid = "my_id_1";
}];
}
'';
};
};
contactPoints = {
path = mkOption {
description = ''
Path to YAML contact points configuration. Can't be used with
[](#opt-services.grafana.provision.alerting.contactPoints.settings) simultaneously.
Can be either a directory or a single YAML file. Will end up in the store.
'';
default = null;
type = types.nullOr types.path;
};
settings = mkOption {
description = ''
Grafana contact points configuration in Nix. Can't be used with
[](#opt-services.grafana.provision.alerting.contactPoints.path) simultaneously. See
<https://grafana.com/docs/grafana/latest/administration/provisioning/#contact-points>
for supported options.
'';
default = null;
type = types.nullOr (types.submodule {
options = {
apiVersion = mkOption {
description = "Config file version.";
default = 1;
type = types.int;
};
contactPoints = mkOption {
description = "List of contact points to import or update.";
default = [ ];
type = types.listOf (types.submodule {
freeformType = provisioningSettingsFormat.type;
options.name = mkOption {
description = "Name of the contact point. Required.";
type = types.str;
};
});
};
deleteContactPoints = mkOption {
description = "List of receivers that should be deleted.";
default = [ ];
type = types.listOf (types.submodule {
options.orgId = mkOption {
description = "Organization ID, default = 1.";
default = 1;
type = types.int;
};
options.uid = mkOption {
description = "Unique identifier for the receiver. Required.";
type = types.str;
};
});
};
};
});
example = literalExpression ''
{
apiVersion = 1;
contactPoints = [{
orgId = 1;
name = "cp_1";
receivers = [{
uid = "first_uid";
type = "prometheus-alertmanager";
settings.url = "http://test:9000";
}];
}];
deleteContactPoints = [{
orgId = 1;
uid = "first_uid";
}];
}
'';
};
};
policies = {
path = mkOption {
description = ''
Path to YAML notification policies configuration. Can't be used with
[](#opt-services.grafana.provision.alerting.policies.settings) simultaneously.
Can be either a directory or a single YAML file. Will end up in the store.
'';
default = null;
type = types.nullOr types.path;
};
settings = mkOption {
description = ''
Grafana notification policies configuration in Nix. Can't be used with
[](#opt-services.grafana.provision.alerting.policies.path) simultaneously. See
<https://grafana.com/docs/grafana/latest/administration/provisioning/#notification-policies>
for supported options.
'';
default = null;
type = types.nullOr (types.submodule {
options = {
apiVersion = mkOption {
description = "Config file version.";
default = 1;
type = types.int;
};
policies = mkOption {
description = "List of contact points to import or update.";
default = [ ];
type = types.listOf (types.submodule {
freeformType = provisioningSettingsFormat.type;
});
};
resetPolicies = mkOption {
description = "List of orgIds that should be reset to the default policy.";
default = [ ];
type = types.listOf types.int;
};
};
});
example = literalExpression ''
{
apiVersion = 1;
policies = [{
orgId = 1;
receiver = "grafana-default-email";
group_by = [ "..." ];
matchers = [
"alertname = Watchdog"
"severity =~ \"warning|critical\""
];
mute_time_intervals = [
"abc"
];
group_wait = "30s";
group_interval = "5m";
repeat_interval = "4h";
}];
resetPolicies = [
1
];
}
'';
};
};
templates = {
path = mkOption {
description = ''
Path to YAML templates configuration. Can't be used with
[](#opt-services.grafana.provision.alerting.templates.settings) simultaneously.
Can be either a directory or a single YAML file. Will end up in the store.
'';
default = null;
type = types.nullOr types.path;
};
settings = mkOption {
description = ''
Grafana templates configuration in Nix. Can't be used with
[](#opt-services.grafana.provision.alerting.templates.path) simultaneously. See
<https://grafana.com/docs/grafana/latest/administration/provisioning/#templates>
for supported options.
'';
default = null;
type = types.nullOr (types.submodule {
options = {
apiVersion = mkOption {
description = "Config file version.";
default = 1;
type = types.int;
};
templates = mkOption {
description = "List of templates to import or update.";
default = [ ];
type = types.listOf (types.submodule {
freeformType = provisioningSettingsFormat.type;
options.name = mkOption {
description = "Name of the template, must be unique. Required.";
type = types.str;
};
options.template = mkOption {
description = "Alerting with a custom text template";
type = types.str;
};
});
};
deleteTemplates = mkOption {
description = "List of alert rule UIDs that should be deleted.";
default = [ ];
type = types.listOf (types.submodule {
options.orgId = mkOption {
description = "Organization ID, default = 1.";
default = 1;
type = types.int;
};
options.name = mkOption {
description = "Name of the template, must be unique. Required.";
type = types.str;
};
});
};
};
});
example = literalExpression ''
{
apiVersion = 1;
templates = [{
orgId = 1;
name = "my_first_template";
template = "Alerting with a custom text template";
}];
deleteTemplates = [{
orgId = 1;
name = "my_first_template";
}];
}
'';
};
};
muteTimings = {
path = mkOption {
description = ''
Path to YAML mute timings configuration. Can't be used with
[](#opt-services.grafana.provision.alerting.muteTimings.settings) simultaneously.
Can be either a directory or a single YAML file. Will end up in the store.
'';
default = null;
type = types.nullOr types.path;
};
settings = mkOption {
description = ''
Grafana mute timings configuration in Nix. Can't be used with
[](#opt-services.grafana.provision.alerting.muteTimings.path) simultaneously. See
<https://grafana.com/docs/grafana/latest/administration/provisioning/#mute-timings>
for supported options.
'';
default = null;
type = types.nullOr (types.submodule {
options = {
apiVersion = mkOption {
description = "Config file version.";
default = 1;
type = types.int;
};
muteTimes = mkOption {
description = "List of mute time intervals to import or update.";
default = [ ];
type = types.listOf (types.submodule {
freeformType = provisioningSettingsFormat.type;
options.name = mkOption {
description = "Name of the mute time interval, must be unique. Required.";
type = types.str;
};
});
};
deleteMuteTimes = mkOption {
description = "List of mute time intervals that should be deleted.";
default = [ ];
type = types.listOf (types.submodule {
options.orgId = mkOption {
description = "Organization ID, default = 1.";
default = 1;
type = types.int;
};
options.name = mkOption {
description = "Name of the mute time interval, must be unique. Required.";
type = types.str;
};
});
};
};
});
example = literalExpression ''
{
apiVersion = 1;
muteTimes = [{
orgId = 1;
name = "mti_1";
time_intervals = [{
times = [{
start_time = "06:00";
end_time = "23:59";
}];
weekdays = [
"monday:wednesday"
"saturday"
"sunday"
];
months = [
"1:3"
"may:august"
"december"
];
years = [
"2020:2022"
"2030"
];
days_of_month = [
"1:5"
"-3:-1"
];
}];
}];
deleteMuteTimes = [{
orgId = 1;
name = "mti_1";
}];
}
'';
};
};
};
};
};
config = mkIf cfg.enable {
warnings =
let
doesntUseFileProvider = opt: defaultValue:
let regex = "${optionalString (defaultValue != null) "^${defaultValue}$|"}^\\$__(file|env)\\{.*}$|^\\$[^_\\$][^ ]+$";
in builtins.match regex opt == null;
# Ensure that no custom credentials are leaked into the Nix store. Unless the default value
# is specified, this can be achieved by using the file/env provider:
# https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#variable-expansion
passwordWithoutFileProvider = optional
(
doesntUseFileProvider cfg.settings.database.password "" ||
doesntUseFileProvider cfg.settings.security.admin_password "admin"
)
''
Grafana passwords will be stored as plaintext in the Nix store!
Use file provider or an env-var instead.
'';
# Ensure that `secureJsonData` of datasources provisioned via `datasources.settings`
# only uses file/env providers.
secureJsonDataWithoutFileProvider = optional
(
let
datasourcesToCheck = optionals
(cfg.provision.datasources.settings != null)
cfg.provision.datasources.settings.datasources;
declarationUnsafe = { secureJsonData, ... }:
secureJsonData != null
&& any (flip doesntUseFileProvider null) (attrValues secureJsonData);
in
any declarationUnsafe datasourcesToCheck
)
''
Declarations in the `secureJsonData`-block of a datasource will be leaked to the
Nix store unless a file-provider or an env-var is used!
'';
in
passwordWithoutFileProvider
++ secureJsonDataWithoutFileProvider
;
environment.systemPackages = [ cfg.package ];
assertions = [
{
assertion = cfg.provision.datasources.settings == null || cfg.provision.datasources.path == null;
message = "Cannot set both datasources settings and datasources path";
}
{
assertion =
let
prometheusIsNotDirect = opt: all
({ type, access, ... }: type == "prometheus" -> access != "direct")
opt;
in
cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources;
message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
}
{
assertion = cfg.provision.dashboards.settings == null || cfg.provision.dashboards.path == null;
message = "Cannot set both dashboards settings and dashboards path";
}
{
assertion = cfg.provision.alerting.rules.settings == null || cfg.provision.alerting.rules.path == null;
message = "Cannot set both rules settings and rules path";
}
{
assertion = cfg.provision.alerting.contactPoints.settings == null || cfg.provision.alerting.contactPoints.path == null;
message = "Cannot set both contact points settings and contact points path";
}
{
assertion = cfg.provision.alerting.policies.settings == null || cfg.provision.alerting.policies.path == null;
message = "Cannot set both policies settings and policies path";
}
{
assertion = cfg.provision.alerting.templates.settings == null || cfg.provision.alerting.templates.path == null;
message = "Cannot set both templates settings and templates path";
}
{
assertion = cfg.provision.alerting.muteTimings.settings == null || cfg.provision.alerting.muteTimings.path == null;
message = "Cannot set both mute timings settings and mute timings path";
}
];
systemd.services.grafana = {
description = "Grafana Service Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
exec ${cfg.package}/bin/grafana server -homepath ${cfg.dataDir} -config ${configFile}
'';
serviceConfig = {
WorkingDirectory = cfg.dataDir;
User = "grafana";
Restart = "on-failure";
RuntimeDirectory = "grafana";
RuntimeDirectoryMode = "0755";
# Hardening
AmbientCapabilities = lib.mkIf (cfg.settings.server.http_port < 1024) [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = if (cfg.settings.server.http_port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
DeviceAllow = [ "" ];
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "full";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
# Upstream grafana is not setting SystemCallFilter for compatibility
# reasons, see https://github.com/grafana/grafana/pull/40176
SystemCallFilter = [
"@system-service"
"~@privileged"
] ++ lib.optionals (cfg.settings.server.protocol == "socket") [ "@chown" ];
UMask = "0027";
};
preStart = ''
ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir}
ln -fs ${cfg.package}/share/grafana/tools ${cfg.dataDir}
'';
};
users.users.grafana = {
uid = config.ids.uids.grafana;
description = "Grafana user";
home = cfg.dataDir;
createHome = true;
group = "grafana";
};
users.groups.grafana = { };
};
}