Skip to content
Snippets Groups Projects
grafana.nix 5.18 KiB
Newer Older
# Grafana Server
#
# Scope: Beautiful plots of time series data retrieved from Prometheus
# See https://christine.website/blog/prometheus-grafana-loki-nixos-2020-11-20

{ config, lib, ... }:

let
  cfg = config.services.private-storage.monitoring.grafana;
  grafanaAuth = if (cfg.googleOAuthClientID == "") then {
                  anonymous.enable = true;
                } else {
                  google.enable = true;
                  # Grafana considers it "sign up" to let in a user it has
                  # never seen before.
                  google.allowSignUp = true;
Florian Sesser's avatar
Florian Sesser committed
                  google.clientSecretFile = cfg.googleOAuthClientSecretFile;
                  google.clientId = cfg.googleOAuthClientID;

in {
  options.services.private-storage.monitoring.grafana = {
    domain = lib.mkOption
    { type = lib.types.str;
      example = lib.literalExample "grafana.grid.private.storage";
      description = "The FQDN of the Grafana host";
    };
    prometheusUrl = lib.mkOption
    { type = lib.types.str;
      example = lib.literalExample "http://prometheus:9090/";
      default = "http://localhost:9090/";
      description = "The URL of the Prometheus host to access";
    };
    lokiUrl = lib.mkOption
    { type = lib.types.str;
      example = lib.literalExample "http://loki:3100/";
      default = "http://localhost:3100/";
      description = "The URL of the Loki host to access";
    };
    letsEncryptAdminEmail = lib.mkOption
    { type = lib.types.str;
      description = ''
        An email address to give to Let's Encrypt as an
        operational contact for the service's TLS certificate.
      '';
    };
    googleOAuthClientID = lib.mkOption
    { type = lib.types.str;
      example = lib.literalExample "grafana-staging-345678";
      default = "replace-by-your-client-id-or-set-empty-string-for-anonymous-access";
      description = "The GSuite OAuth2 SSO Client ID.  Empty string turns SSO auth off and anonymous (free for all) access on.";
    };
    googleOAuthClientSecretFile = lib.mkOption
    { type = lib.types.path;
      example = lib.literalExample "/var/secret/monitoring-gsuite-client-secret";
      default = /run/keys/grafana-google-sso.secret;
      description = "The path to the GSuite SSO secret file.";
    };
Florian Sesser's avatar
Florian Sesser committed
    adminPasswordFile = lib.mkOption
    { type = lib.types.path;
      example = lib.literalExample "/var/secret/monitoring-admin-password";
      default = /run/keys/grafana-admin.password;
      description = "A file containing the password for the Grafana Admin account.";
    };
    # Port 80 for ACME ssl retrieval only. 443 for nginx -> grafana.
    networking.firewall.allowedTCPPorts = [ 80 443 ];

    services.grafana = {
      enable = true;
      domain = cfg.domain;
      port = 2342;
      addr = "127.0.0.1";

      # No phoning home
      analytics.reporting.enable = false;

      # Force Grafana to believe it is reachable via https on the default port
      # number because that's where the nginx that forwards traffic to it is
      # listening.  Grafana's own server listens on an internal address that
      # doesn't matter to anyone except our nginx instance.
      rootUrl = "https://%(domain)s/";

      extraOptions = {
        # Defend against DNS rebinding attacks.
        SERVER_ENFORCE_DOMAIN = "true";
Florian Sesser's avatar
Florian Sesser committed
        # Same time zone for all users by default
        DATE_FORMATS_DEFAULT_TIMEZONE = "UTC";
      auth = {
        anonymous.org_role = "Admin";
        anonymous.org_name = "Main Org.";
      } // grafanaAuth;
Florian Sesser's avatar
Florian Sesser committed
      # Give users that come through GSuite SSO the highest possible privileges:
      users.autoAssignOrgRole = "Editor";

      # Read the admin password from a file in our secrets folder:
      security.adminPasswordFile = cfg.adminPasswordFile;

      provision = {
        enable = true;
        # See https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources
        datasources = [{
          name = "Prometheus";
          type = "prometheus";
          access = "proxy";
          url = cfg.prometheusUrl;
          isDefault = true;
        } {
          name = "Loki";
          type = "loki";
          access = "proxy";
          url = cfg.lokiUrl;
        }];
        # See https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards
        dashboards = [{
          name = "provisioned";
          options.path = ./grafana-config;
        }];
      };
    security.acme.email = cfg.letsEncryptAdminEmail;
    security.acme.acceptTerms = true;
    services.nginx = {
      enable = true;

      recommendedGzipSettings = true;
      recommendedOptimisation = true;
      recommendedProxySettings = true;
      recommendedTlsSettings = true;

      # Only allow PFS-enabled ciphers with AES256:
      sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL";

      virtualHosts.${config.services.grafana.domain} = {
        enableACME = true;
        locations."/" = {
          proxyPass = "http://127.0.0.1:${toString config.services.grafana.port}";
          proxyWebsockets = true;
        };

    # Let Grafana read from keys, if necessary.
    users.users.grafana.extraGroups = [ "keys" ];