nixos/kerberos_server: use krb format generator, plus misc cleanup

- Introduce more possible options by using the krb format generator.
- Enforce package choice is using a correct package.
- Use meta attribute to decide implementation, allows for overriding the
  package.
- Make necessary changes to the format, to allow for multiple ACL files in
  heimdal.
- Add systemd target and slice for both implementations.
- Move state to `/var/lib`
- Add documentation
This commit is contained in:
h7x4 2023-12-09 01:16:54 +01:00
parent db4171f3e2
commit 195d155a1c
No known key found for this signature in database
GPG Key ID: 9F2F7D8250F35146
9 changed files with 278 additions and 139 deletions

View File

@ -77,8 +77,22 @@ in {
};
};
config = mkIf cfg.enable {
environment = {
config = {
assertions = mkIf (cfg.enable || config.services.kerberos_server.enable) [(let
implementation = cfg.package.passthru.implementation or "<NOT SET>";
in {
assertion = lib.elem implementation [ "krb5" "heimdal" ];
message = ''
`security.krb5.package` must be one of:
- krb5
- heimdal
Currently chosen implementation: ${implementation}
'';
})];
environment = mkIf cfg.enable {
systemPackages = [ cfg.package ];
etc."krb5.conf".source = format.generate "krb5.conf" cfg.settings;
};

View File

@ -7,17 +7,61 @@
let
inherit (lib) boolToString concatMapStringsSep concatStringsSep filter
isAttrs isBool isList mapAttrsToList mkOption singleton splitString;
inherit (lib.types) attrsOf bool coercedTo either int listOf oneOf path
str submodule;
inherit (lib.types) attrsOf bool coercedTo either enum int listOf oneOf
path str submodule;
in
{ }: {
type = let
section = attrsOf relation;
relation = either (attrsOf value) value;
{
enableKdcACLEntries ? false
}: rec {
sectionType = let
relation = oneOf [
(listOf (attrsOf value))
(attrsOf value)
value
];
value = either (listOf atom) atom;
atom = oneOf [int str bool];
in attrsOf relation;
type = let
aclEntry = submodule {
options = {
principal = mkOption {
type = str;
description = "Which principal the rule applies to";
};
access = mkOption {
type = either
(listOf (enum ["add" "cpw" "delete" "get" "list" "modify"]))
(enum ["all"]);
default = "all";
description = "The changes the principal is allowed to make.";
};
target = mkOption {
type = str;
default = "*";
description = "The principals that 'access' applies to.";
};
};
};
realm = submodule ({ name, ... }: {
freeformType = sectionType;
options = {
acl = mkOption {
type = listOf aclEntry;
default = [
{ principal = "*/admin"; access = "all"; }
{ principal = "admin"; access = "all"; }
];
description = ''
The privileges granted to a user.
'';
};
};
});
in submodule {
freeformType = attrsOf section;
freeformType = attrsOf sectionType;
options = {
include = mkOption {
default = [ ];
@ -40,7 +84,17 @@ in
'';
type = coercedTo path singleton (listOf path);
};
};
}
//
(lib.optionalAttrs enableKdcACLEntries {
realms = mkOption {
type = attrsOf realm;
description = ''
The realm(s) to serve keys for.
'';
};
});
};
generate = let
@ -71,6 +125,9 @@ in
${name} = {
${indent (concatStringsSep "\n" (mapAttrsToList formatValue relation))}
}''
else if isList relation
then
concatMapStringsSep "\n" (formatRelation name) relation
else formatValue name relation;
formatValue = name: value:

View File

@ -1,75 +1,59 @@
{config, lib, ...}:
{ config, pkgs, lib, ... }:
let
inherit (lib) mkOption mkIf types length attrNames;
inherit (lib) mkOption types;
cfg = config.services.kerberos_server;
kerberos = config.security.krb5.package;
inherit (config.security.krb5) package;
aclEntry = {
options = {
principal = mkOption {
type = types.str;
description = "Which principal the rule applies to";
};
access = mkOption {
type = types.either
(types.listOf (types.enum ["add" "cpw" "delete" "get" "list" "modify"]))
(types.enum ["all"]);
default = "all";
description = "The changes the principal is allowed to make.";
};
target = mkOption {
type = types.str;
default = "*";
description = "The principals that 'access' applies to.";
};
};
};
realm = {
options = {
acl = mkOption {
type = types.listOf (types.submodule aclEntry);
default = [
{ principal = "*/admin"; access = "all"; }
{ principal = "admin"; access = "all"; }
];
description = ''
The privileges granted to a user.
'';
};
};
};
format = import ../../../security/krb5/krb5-conf-format.nix { inherit pkgs lib; } { enableKdcACLEntries = true; };
in
{
imports = [
(lib.mkRenamedOptionModule [ "services" "kerberos_server" "realms" ] [ "services" "kerberos_server" "settings" "realms" ])
./mit.nix
./heimdal.nix
];
###### interface
options = {
services.kerberos_server = {
enable = lib.mkEnableOption "the kerberos authentication server";
realms = mkOption {
type = types.attrsOf (types.submodule realm);
settings = mkOption {
type = format.type;
description = ''
The realm(s) to serve keys for.
Settings for the kerberos server of choice.
See the following documentation:
- Heimdal: {manpage}`kdc.conf(5)`
- MIT Kerberos: <https://web.mit.edu/kerberos/krb5-1.21/doc/admin/conf_files/kdc_conf.html>
'';
default = { };
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ package ];
assertions = [
{
assertion = cfg.settings.realms != { };
message = "The server needs at least one realm";
}
{
assertion = lib.length (lib.attrNames cfg.settings.realms) <= 1;
message = "Only one realm per server is currently supported.";
}
];
###### implementation
systemd.slices.system-kerberos-server = { };
systemd.targets.kerberos-server = {
wantedBy = [ "multi-user.target" ];
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ kerberos ];
assertions = [{
assertion = length (attrNames cfg.realms) <= 1;
message = "Only one realm per server is currently supported.";
}];
meta = {
doc = ./kerberos-server.md;
};
}

View File

@ -1,68 +1,87 @@
{ pkgs, config, lib, ... } :
let
inherit (lib) mkIf concatStringsSep concatMapStrings toList mapAttrs
mapAttrsToList;
inherit (lib) mapAttrs;
cfg = config.services.kerberos_server;
kerberos = config.security.krb5.package;
stateDir = "/var/heimdal";
aclFiles = mapAttrs
(name: {acl, ...}: pkgs.writeText "${name}.acl" (concatMapStrings ((
{principal, access, target, ...} :
"${principal}\t${concatStringsSep "," (toList access)}\t${target}\n"
)) acl)) cfg.realms;
package = config.security.krb5.package;
kdcConfigs = mapAttrsToList (name: value: ''
database = {
dbname = ${stateDir}/heimdal
acl_file = ${value}
}
'') aclFiles;
kdcConfFile = pkgs.writeText "kdc.conf" ''
[kdc]
${concatStringsSep "\n" kdcConfigs}
'';
aclConfigs = lib.pipe cfg.settings.realms [
(mapAttrs (name: { acl, ... }: lib.concatMapStringsSep "\n" (
{ principal, access, target, ... }:
"${principal}\t${lib.concatStringsSep "," (lib.toList access)}\t${target}"
) acl))
(lib.mapAttrsToList (name: text:
{
dbname = "/var/lib/heimdal/heimdal";
acl_file = pkgs.writeText "${name}.acl" text;
}
))
];
finalConfig = cfg.settings // {
realms = mapAttrs (_: v: removeAttrs v [ "acl" ]) (cfg.settings.realms or { });
kdc = (cfg.settings.kdc or { }) // {
database = aclConfigs;
};
};
format = import ../../../security/krb5/krb5-conf-format.nix { inherit pkgs lib; } { enableKdcACLEntries = true; };
kdcConfFile = format.generate "kdc.conf" finalConfig;
in
{
# No documentation about correct triggers, so guessing at them.
config = lib.mkIf (cfg.enable && package.passthru.implementation == "heimdal") {
environment.etc."heimdal-kdc/kdc.conf".source = kdcConfFile;
systemd.tmpfiles.settings."10-heimdal" = let
databases = lib.pipe finalConfig.kdc.database [
(map (dbAttrs: dbAttrs.dbname or null))
(lib.filter (x: x != null))
lib.unique
];
in lib.genAttrs databases (_: {
d = {
user = "root";
group = "root";
mode = "0700";
};
});
config = mkIf (cfg.enable && kerberos == pkgs.heimdal) {
systemd.services.kadmind = {
description = "Kerberos Administration Daemon";
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -m 0755 -p ${stateDir}
'';
serviceConfig.ExecStart =
"${kerberos}/libexec/kadmind --config-file=/etc/heimdal-kdc/kdc.conf";
partOf = [ "kerberos-server.target" ];
wantedBy = [ "kerberos-server.target" ];
serviceConfig = {
ExecStart = "${package}/libexec/kadmind --config-file=/etc/heimdal-kdc/kdc.conf";
Slice = "system-kerberos-server.slice";
StateDirectory = "heimdal";
};
restartTriggers = [ kdcConfFile ];
};
systemd.services.kdc = {
description = "Key Distribution Center daemon";
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -m 0755 -p ${stateDir}
'';
serviceConfig.ExecStart =
"${kerberos}/libexec/kdc --config-file=/etc/heimdal-kdc/kdc.conf";
partOf = [ "kerberos-server.target" ];
wantedBy = [ "kerberos-server.target" ];
serviceConfig = {
ExecStart = "${package}/libexec/kdc --config-file=/etc/heimdal-kdc/kdc.conf";
Slice = "system-kerberos-server.slice";
StateDirectory = "heimdal";
};
restartTriggers = [ kdcConfFile ];
};
systemd.services.kpasswdd = {
description = "Kerberos Password Changing daemon";
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -m 0755 -p ${stateDir}
'';
serviceConfig.ExecStart = "${kerberos}/libexec/kpasswdd";
partOf = [ "kerberos-server.target" ];
wantedBy = [ "kerberos-server.target" ];
serviceConfig = {
ExecStart = "${package}/libexec/kpasswdd";
Slice = "system-kerberos-server.slice";
StateDirectory = "heimdal";
};
restartTriggers = [ kdcConfFile ];
};
environment.etc = {
# Can be set via the --config-file option to KDC
"heimdal-kdc/kdc.conf".source = kdcConfFile;
};
};
}

View File

@ -0,0 +1,55 @@
# kerberos_server {#module-services-kerberos-server}
Kerberos is a computer-network authentication protocol that works on the basis of tickets to allow nodes communicating over a non-secure network to prove their identity to one another in a secure manner.
This module provides both the MIT and Heimdal implementations of the a Kerberos server.
## Usage {#module-services-kerberos-server-usage}
To enable a Kerberos server:
```nix
{
security.krb5 = {
# Here you can choose between the MIT and Heimdal implementations.
package = pkgs.krb5;
# package = pkgs.heimdal;
# Optionally set up a client on the same machine as the server
enable = true;
settings = {
libdefaults.default_realm = "EXAMPLE.COM";
realms."EXAMPLE.COM" = {
kdc = "kerberos.example.com";
admin_server = "kerberos.example.com";
};
};
}
services.kerberos-server = {
enable = true;
settings = {
realms."EXAMPLE.COM" = {
acl = [{ principal = "adminuser"; access= ["add" "cpw"]; }];
};
};
};
}
```
## Notes {#module-services-kerberos-server-notes}
- The Heimdal documentation will sometimes assume that state is stored in `/var/heimdal`, but this module uses `/var/lib/heimdal` instead.
- Due to the heimdal implementation being chosen through `security.krb5.package`, it is not possible to have a system with one implementation of the client and another of the server.
- While `services.kerberos_server.settings` has a common freeform type between the two implementations, the actual settings that can be set can vary between the two implementations. To figure out what settings are available, you should consult the upstream documentation for the implementation you are using.
## Upstream Documentation {#module-services-kerberos-server-upstream-documentation}
- MIT Kerberos homepage: https://web.mit.edu/kerberos
- MIT Kerberos docs: https://web.mit.edu/kerberos/krb5-latest/doc/index.html
- Heimdal Kerberos GitHub wiki: https://github.com/heimdal/heimdal/wiki
- Heimdal kerberos doc manpages (Debian unstable): https://manpages.debian.org/unstable/heimdal-docs/index.html
- Heimdal Kerberos kdc manpages (Debian unstable): https://manpages.debian.org/unstable/heimdal-kdc/index.html
Note the version number in the URLs, it may be different for the latest version.

View File

@ -1,31 +1,37 @@
{ pkgs, config, lib, ... } :
let
inherit (lib) mkIf concatStrings concatStringsSep concatMapStrings toList
mapAttrs mapAttrsToList;
inherit (lib) mapAttrs;
cfg = config.services.kerberos_server;
kerberos = config.security.krb5.package;
stateDir = "/var/lib/krb5kdc";
package = config.security.krb5.package;
PIDFile = "/run/kdc.pid";
format = import ../../../security/krb5/krb5-conf-format.nix { inherit pkgs lib; } { enableKdcACLEntries = true; };
aclMap = {
add = "a"; cpw = "c"; delete = "d"; get = "i"; list = "l"; modify = "m";
all = "*";
};
aclFiles = mapAttrs
(name: {acl, ...}: (pkgs.writeText "${name}.acl" (concatMapStrings (
{principal, access, target, ...} :
let access_code = map (a: aclMap.${a}) (toList access); in
"${principal} ${concatStrings access_code} ${target}\n"
) acl))) cfg.realms;
kdcConfigs = mapAttrsToList (name: value: ''
${name} = {
acl_file = ${value}
}
'') aclFiles;
kdcConfFile = pkgs.writeText "kdc.conf" ''
[realms]
${concatStringsSep "\n" kdcConfigs}
'';
aclConfigs = lib.pipe cfg.settings.realms [
(mapAttrs (name: { acl, ... }: lib.concatMapStringsSep "\n" (
{ principal, access, target, ... }: let
access_code = map (a: aclMap.${a}) (lib.toList access);
in "${principal} ${lib.concatStrings access_code} ${target}"
) acl))
(lib.concatMapAttrs (name: text: {
${name} = {
acl_file = pkgs.writeText "${name}.acl" text;
};
}))
];
finalConfig = cfg.settings // {
realms = mapAttrs (n: v: (removeAttrs v [ "acl" ]) // aclConfigs.${n}) (cfg.settings.realms or { });
};
kdcConfFile = format.generate "kdc.conf" finalConfig;
env = {
# What Debian uses, could possibly link directly to Nix store?
KRB5_KDC_PROFILE = "/etc/krb5kdc/kdc.conf";
@ -33,36 +39,38 @@ let
in
{
config = mkIf (cfg.enable && kerberos == pkgs.krb5) {
config = lib.mkIf (cfg.enable && package.passthru.implementation == "krb5") {
environment = {
etc."krb5kdc/kdc.conf".source = kdcConfFile;
variables = env;
};
systemd.services.kadmind = {
description = "Kerberos Administration Daemon";
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -m 0755 -p ${stateDir}
'';
serviceConfig.ExecStart = "${kerberos}/bin/kadmind -nofork";
partOf = [ "kerberos-server.target" ];
wantedBy = [ "kerberos-server.target" ];
serviceConfig = {
ExecStart = "${package}/bin/kadmind -nofork";
Slice = "system-kerberos-server.slice";
StateDirectory = "krb5kdc";
};
restartTriggers = [ kdcConfFile ];
environment = env;
};
systemd.services.kdc = {
description = "Key Distribution Center daemon";
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -m 0755 -p ${stateDir}
'';
partOf = [ "kerberos-server.target" ];
wantedBy = [ "kerberos-server.target" ];
serviceConfig = {
Type = "forking";
PIDFile = PIDFile;
ExecStart = "${kerberos}/bin/krb5kdc -P ${PIDFile}";
ExecStart = "${package}/bin/krb5kdc -P ${PIDFile}";
Slice = "system-kerberos-server.slice";
StateDirectory = "krb5kdc";
};
restartTriggers = [ kdcConfFile ];
environment = env;
};
environment.etc = {
"krb5kdc/kdc.conf".source = kdcConfFile;
};
environment.variables = env;
};
}

View File

@ -4,7 +4,7 @@ import ../make-test-python.nix ({pkgs, ...}: {
nodes.machine = { config, libs, pkgs, ...}:
{ services.kerberos_server =
{ enable = true;
realms = {
settings.realms = {
"FOO.BAR".acl = [{principal = "admin"; access = ["add" "cpw"];}];
};
};

View File

@ -4,7 +4,7 @@ import ../make-test-python.nix ({pkgs, ...}: {
nodes.machine = { config, libs, pkgs, ...}:
{ services.kerberos_server =
{ enable = true;
realms = {
settings.realms = {
"FOO.BAR".acl = [{principal = "admin"; access = ["add" "cpw"];}];
};
};

View File

@ -89,6 +89,8 @@ stdenv.mkDerivation {
];
configureFlags = [
"--with-hdbdir=/var/lib/heimdal"
"--with-libedit-include=${libedit.dev}/include"
"--with-libedit-lib=${libedit}/lib"
"--with-berkeley-db-include=${db.dev}/include"