Authelia and Nginx on Nixos

Does anyone have some experience working with Authelia on Nixos, specifically with Nginx? I have been playing around with it a bit and got something working, configs below. This is based on the following:

https://matwick.ca/authelia-nginx-sso/

I welcome any suggestions or corrections on how to improve this, as by no means do I know what I am doing.

There is one thing I would like to improve that I can’t seem to figure it out, making adding Authelia authorization to a site simpler. That is not having to copy so many extraConfig to each virtual hosts. Is there a way to create the equivalent of a snippet for the nix config. That is, define all the text to be included in the extraConfig once and then be able to add it to any virtual hosts in one or two lines?

Thank you

# /etc/nixos/nginx.nix
{config, pkgs, ...}:
{
  # Setup ACME
  security.acme = {
    acceptTerms = true;
    defaults = { 
      email = "admin@example.com";
      dnsProvider = "dynu";
      credentialsFile = "${pkgs.writeText "dynu-creds" ''
        DYNU_API_KEY=supersecretkey
      ''}";
    };
  };

  # Open Port in Firewall
  networking.firewall.allowedTCPPorts = [ 80 443 ];

  # Enable Nginx and set bassic settigns
  services.nginx = {
    enable = true;

    # Use recommended settings
    recommendedGzipSettings = true;
    recommendedOptimisation = true;
    recommendedProxySettings = true;
    recommendedTlsSettings = true;

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

    commonHttpConfig = ''
      # Add HSTS header with preloading to HTTPS requests.
      # Adding this header to HTTP requests is discouraged
      map $scheme $hsts_header {
          https   "max-age=31536000; includeSubdomains; preload";
      }
      add_header Strict-Transport-Security $hsts_header;
    '';
  };

  # base Virtual Host 
  services.nginx.virtualHosts."example.com" = {
    enableACME = true;
    forceSSL = true;
    acmeRoot = null;
  };
}
# /etc/nixos/authelia.nix
{ config, pkgs, ... }:

{
  services.authelia.instances.main = {
    enable = true;
    secrets = {
      jwtSecretFile = "${pkgs.writeText "jwtSecretFile" "supersecretkey"}";
      storageEncryptionKeyFile = "${pkgs.writeText "storageEncryptionKeyFile" "supersecretkey"}";
      sessionSecretFile = "${pkgs.writeText "sessionSecretFile" "supersecretkey"}";
    };
    settings = {
      theme = "dark";
      default_redirection_url = "https://example.com";
  
      server = {
        host = "127.0.0.1";
        port = 9091;
      };
  
      log = {
        level = "debug";
        format = "text";
      };
  
      authentication_backend = {
        file = {
          path = "/var/lib/authelia-main/users_database.yml";
        };
      };
  
      access_control = {
        default_policy = "deny";
        rules = [
          {
            domain = ["auth.example.com"];
            policy = "bypass";
          }
          {
            domain = ["*.example.com"];
            policy = "one_factor";
          }
        ];
      };
  
      session = {
        name = "authelia_session";
        expiration = "12h";
        inactivity = "45m";
        remember_me_duration = "1M";
        domain = "example.com";
        redis.host = "/run/redis-authelia-main/redis.sock";
      };
  
      regulation = {
        max_retries = 3;
        find_time = "5m";
        ban_time = "15m";
      };
  
      storage = {
        local = {
          path = "/var/lib/authelia-main/db.sqlite3";
        };
      };
  
      notifier = {
        disable_startup_check = false;
        filesystem = {
          filename = "/var/lib/authelia-main/notification.txt";
        };
      };
    };
  };
  services.redis.servers.authelia-main = {
    enable = true;
    user = "authelia-main";   
    port = 0;
    unixSocket = "/run/redis-authelia-main/redis.sock";
    unixSocketPerm = 600;
  };
  services.nginx.virtualHosts."auth.example.com" = {
    enableACME = true;
    forceSSL = true;
    acmeRoot = null;

    locations."/" = {
      proxyPass = "http://127.0.0.1:9091";
      proxyWebsockets = true;
    };
  };
}
# /etc/nixos/test.nix
{ config, pkgs, ... }:

{
  services.nginx.virtualHosts."test.example.com" = {
    enableACME = true;
    forceSSL = true;
    acmeRoot = null;
    
    extraConfig = ''
      # Virtual endpoint created by nginx to forward auth requests.
      location /authelia {
        internal;
        set $upstream_authelia http://127.0.0.1:9091/api/verify;
        proxy_pass_request_body off;
        proxy_pass $upstream_authelia;    
        proxy_set_header Content-Length "";

        # Timeout if the real server is dead
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;

        # [REQUIRED] Needed by Authelia to check authorizations of the resource.
        # Provide either X-Original-URL and X-Forwarded-Proto or
        # X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri or both.
        # Those headers will be used by Authelia to deduce the target url of the     user.
        # Basic Proxy Config
        client_body_buffer_size 128k;
        proxy_set_header Host $host;
        proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr; 
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header X-Forwarded-Uri $request_uri;
        proxy_set_header X-Forwarded-Ssl on;
        proxy_redirect  http://  $scheme://;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_cache_bypass $cookie_session;
        proxy_no_cache $cookie_session;
        proxy_buffers 4 32k;

        # Advanced Proxy Config
        send_timeout 5m;
        proxy_read_timeout 240;
        proxy_send_timeout 240;
        proxy_connect_timeout 240;
      }
    '';

    locations."/" = {
      root = "/var/www/test/";
      index = "index.html";
      extraConfig = ''
        # Basic Authelia Config
        # Send a subsequent request to Authelia to verify if the user is authenticated
        # and has the right permissions to access the resource.
        auth_request /authelia;
        # Set the `target_url` variable based on the request. It will be used to build the portal
        # URL with the correct redirection parameter.
        auth_request_set $target_url $scheme://$http_host$request_uri;
        # Set the X-Forwarded-User and X-Forwarded-Groups with the headers
        # returned by Authelia for the backends which can consume them.
        # This is not safe, as the backend must make sure that they come from the
        # proxy. In the future, it's gonna be safe to just use OAuth.
        auth_request_set $user $upstream_http_remote_user;
        auth_request_set $groups $upstream_http_remote_groups;
        auth_request_set $name $upstream_http_remote_name;
        auth_request_set $email $upstream_http_remote_email;
        proxy_set_header Remote-User $user;
        proxy_set_header Remote-Groups $groups;
        proxy_set_header Remote-Name $name;
        proxy_set_header Remote-Email $email;
        # If Authelia returns 401, then nginx redirects the user to the login portal.
        # If it returns 200, then the request pass through to the backend.
        # For other type of errors, nginx will handle them as usual.
        error_page 401 =302 https://auth.example.com/?rd=$target_url;
      '';
    };
  };
}

2 Likes

I just found this while searching something but I wanted to tell you that you should never do this. This ends up globally readable in the store which is an issue for several reasons. Use somtehing like sops-nix or agenix which will populate your secrets only after system build time.

Good to know

For this part I was larger just doing it for test as I did not what to put this out on the internet and therefore the Http challenge was not available. So I can’t say I put much thought into this at the time as I was not planning on using it long term.

Cheers

Just in case you’re still looking for an answer to your original question, here is a suggestion: You can extend the services.nginx module and add your own options under services.nginx.virtualHosts.<name>. For example, you can create an option services.nginx.virtualHosts.<name>.enableAuthelia, which if set to true, creates the location /authelia for your virtual host, and adds some text to extraConfig.

To do that, you can use a file with the following content or similar:

{ config, lib, ... }:

let
  vhostOptions = {config,...}: {
    options = {
      enableAuthelia = lib.mkEnableOption "Enable authelia location";
    };
    config =
      lib.mkIf config.enableAuthelia {
        locations."/authelia".extraConfig = ''
          PUT WHATEVER CONFIGURATION YOU WANT, HERE
        '';
      };
  };
in
{
  options.services.nginx.virtualHosts = lib.mkOption {
    type = lib.types.attrsOf (lib.types.submodule vhostOptions);
  };
}

You can then enable this with something like:

services.nginx.virtualHosts."auth.example.com" = {
  ...
  enableAuthelia = true;
  ...
};

If you don’t want to deal with secrets being moved between your local machine and the remote, you can also generate them if they don’t already exist within the authelia systemd service:

{ pkgs, ... }: {
  ...
  services.authelia.instances.main.secrets = {
    jwtSecretFile = "/var/lib/authelia-main/jwt-secret";
    storageEncryptionKeyFile = "/var/lib/authelia-main/storage-encryption-file";
    sessionSecretFile = "/var/lib/authelia-main/session-secret-file";
  };
  systemd.services.authelia-main.preStart = ''
    [ -f /var/lib/authelia-main/jwt-secret ] || {
      "${pkgs.openssl}/bin/openssl" rand -base64 32 > /var/lib/authelia-main/jwt-secret
    }
    [ -f /var/lib/authelia-main/storage-encryption-file ] || {
      "${pkgs.openssl}/bin/openssl" rand -base64 32 > /var/lib/authelia-main/storage-encryption-file
    }
    [ -f /var/lib/authelia-main/session-secret-file ] || {
      "${pkgs.openssl}/bin/openssl" rand -base64 32 > /var/lib/authelia-main/session-secret-file
    }
  '';
  ...
}