# SPDX-FileCopyrightText: 2024 Tom Hubrecht # # SPDX-License-Identifier: EUPL-1.2 { config, lib, pkgs, ... }: let inherit (lib) attrsToList getExe' imap0 mapAttrsToList mkEnableOption mkIf mkOption optionalString ; inherit (lib.types) attrsOf bool enum package path str submodule ; settingsFormat = pkgs.formats.toml { }; pykanidm = pkgs.python3.pkgs.callPackage ./packages/pykanidm.nix { }; rlm_python = pkgs.callPackage ./packages/rlm_python.nix { inherit pykanidm; }; cfg = config.services.k-radius; acmeDirectory = config.security.acme.certs.${cfg.domain}.directory; in { options.services.k-radius = { enable = mkEnableOption "a freeradius service linked to kanidm."; domain = mkOption { type = str; description = "The domain used for the RADIUS server."; }; raddb = mkOption { type = path; default = "/var/lib/radius/raddb/"; description = "The location of the raddb directory."; }; settings = mkOption { inherit (settingsFormat) type; }; freeradius = mkOption { type = package; default = pkgs.freeradius.overrideAttrs (old: { buildInputs = (old.buildInputs or [ ]) ++ [ (pkgs.python3.withPackages (ps: [ ps.kanidm ])) ]; }); }; configDir = mkOption { type = path; default = "/var/lib/radius/raddb"; description = "The path of the freeradius server configuration directory."; }; authTokenFile = mkOption { type = path; description = "File to the auth token for the service account."; }; extra-mods = mkOption { type = attrsOf path; default = { }; description = "Additional files to be linked in mods-enabled."; }; extra-sites = mkOption { type = attrsOf path; default = { }; description = "Additional files to be linked in sites-enabled."; }; dictionary = mkOption { type = attrsOf (enum [ "abinary" "date" "ipaddr" "integer" "string" ]); default = { }; description = "Declare additionnal attributes to be listed in the dictionary."; }; radiusClients = mkOption { type = attrsOf (submodule { options = { secret = mkOption { type = path; }; ipaddr = mkOption { type = str; }; }; }); default = { }; description = "A mapping of clients and their authentication tokens."; }; checkConfiguration = mkOption { type = bool; description = "Check the configuration before starting the deamon. Useful for debugging."; default = false; }; }; config = mkIf cfg.enable { # Certificate setup services.nginx.virtualHosts.${cfg.domain} = { http2 = false; enableACME = true; forceSSL = true; }; users = { users.radius = { group = "radius"; description = "Radius daemon user"; isSystemUser = true; }; groups.radius = { }; }; systemd.services.radius = { description = "FreeRadius server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "acme-finished-${cfg.domain}.target" ]; wants = [ "network.target" ]; startLimitIntervalSec = 20; startLimitBurst = 5; preStart = '' raddb=${cfg.raddb} # Recreate the configuration directory rm -rf $raddb && mkdir -p $raddb cp -R --no-preserve=mode ${cfg.freeradius}/etc/raddb/* $raddb cp -R --no-preserve=mode ${rlm_python}/etc/raddb/* $raddb chmod -R u+w $raddb # disable auth via methods kanidm doesn't support rm $raddb/mods-available/sql rm $raddb/mods-enabled/{passwd,totp} # enable the python and cache modules ln -nsf $raddb/mods-available/python3 $raddb/mods-enabled/python3 ln -nsf $raddb/sites-available/check-eap-tls $raddb/sites-enabled/check-eap-tls # write the clients configuration > $raddb/clients.conf ${builtins.concatStringsSep "\n" ( builtins.attrValues ( builtins.mapAttrs ( name: { secret, ipaddr }: '' cat <> $raddb/clients.conf client ${name} { ipaddr = ${ipaddr} secret = $(cat "${secret}") proto = * } EOF '' ) cfg.radiusClients ) )} # Copy the kanidm configuration cat < /var/lib/radius/kanidm.toml auth_token = "$(cat "${cfg.authTokenFile}")" EOF cat ${settingsFormat.generate "kanidm.toml" cfg.settings} >> /var/lib/radius/kanidm.toml chmod u+w /var/lib/radius/kanidm.toml # Copy the certificates to the correct directory rm -rf $raddb/certs && mkdir -p $raddb/certs cp ${acmeDirectory}/chain.pem $raddb/certs/ca.pem ${lib.getExe pkgs.openssl} rehash $raddb/certs # Recreate the dh.pem file ${lib.getExe pkgs.openssl} dhparam -in $raddb/certs/ca.pem -out $raddb/certs/dh.pem 2048 cp ${acmeDirectory}/full.pem $raddb/certs/server.pem # Link the dictionary ln -nsf ${ pkgs.writeText "radius-dictionary" ( builtins.concatStringsSep "\n" ( imap0 (i: { name, value }: "ATTRIBUTE ${name} ${builtins.toString (3000 + i)} ${value}") ( attrsToList cfg.dictionary ) ) ) } $raddb/dictionary # Link extra-mods ${builtins.concatStringsSep "\n" ( mapAttrsToList (name: path: "ln -nsf ${path} $raddb/mods-enabled/${name}") cfg.extra-mods )} # Link extra-sites ${builtins.concatStringsSep "\n" ( mapAttrsToList (name: path: "ln -nsf ${path} $raddb/sites-enabled/${name}") cfg.extra-sites )} # Check the configuration ${optionalString cfg.checkConfiguration "${getExe' pkgs.freeradius "radiusd"} -C -d $raddb -l stdout"} ''; path = [ pkgs.openssl pkgs.gnused ]; environment = { KANIDM_RLM_CONFIG = "/var/lib/radius/kanidm.toml"; PYTHONPATH = rlm_python.pythonPath; }; serviceConfig = { ExecStart = "${cfg.freeradius}/bin/radiusd -X -f -d /var/lib/radius/raddb -l stdout"; ExecReload = [ "${cfg.freeradius}/bin/radiusd -C -d /var/lib/radius/raddb -l stdout" "${pkgs.coreutils}/bin/kill -HUP $MAINPID" ]; AmbientCapabilities = "CAP_NET_BIND_SERVICE"; DynamicUser = true; Group = "radius"; LogsDirectory = "radius"; ReadOnlyPaths = [ acmeDirectory ]; Restart = "on-failure"; RestartSec = 2; RuntimeDirectory = "radius"; StateDirectory = "radius"; SupplementaryGroups = [ "nginx" ]; User = "radius"; }; }; }; }