nixos/krb5: cleanup, fix and RFC42-ify

This replaces the krb5 module's options with RFC 42-style krb5.settings
option, while greatly simplifying the code and fixing a few bugs,
namely:

- #243068 krb5: Configuration silently gets ignored when set by
  multiple modules
- not being able to use mkIf etc. inside subattributes of
  krb5.libdefaults, e.g. krb5.libdefaults.default_realm = mkIf ...

See #144575.
Closes #243068.

Co-authored-by: h7x4 <h7x4@nani.wtf>
This commit is contained in:
Marco Rebhan 2023-07-13 01:21:59 +02:00
parent 808d2bd0ae
commit 92a541c0ed
No known key found for this signature in database
12 changed files with 252 additions and 484 deletions

View File

@ -94,6 +94,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
- `services.zfs.zed.enableMail` now uses the global `sendmail` wrapper defined by an email module
(such as msmtp or Postfix). It no longer requires using a special ZFS build with email support.
- The `krb5` module has been rewritten, moving all options but `krb5.enable` and `krb5.package` into `krb5.settings`.
- Gitea 1.21 upgrade has several breaking changes, including:
- Custom themes and other assets that were previously stored in `custom/public/*` now belong in `custom/public/assets/*`
- New instances of Gitea using MySQL now ignore the `[database].CHARSET` config option and always use the `utf8mb4` charset, existing instances should migrate via the `gitea doctor convert` CLI command.

View File

@ -1,116 +1,59 @@
{ config, lib, pkgs, ... }:
with lib;
let
inherit (lib) mdDoc mkIf mkOption mkPackageOption mkRemovedOptionModule;
inherit (lib.types) bool;
mkRemovedOptionModule' = name: reason: mkRemovedOptionModule ["krb5" name] reason;
mkRemovedOptionModuleCfg = name: mkRemovedOptionModule' name ''
The option `krb5.${name}' has been removed. Use `krb5.settings.${name}' for
structured configuration.
'';
cfg = config.krb5;
# This is to provide support for old configuration options (as much as is
# reasonable). This can be removed after 18.03 was released.
defaultConfig = {
libdefaults = optionalAttrs (cfg.defaultRealm != null)
{ default_realm = cfg.defaultRealm; };
realms = optionalAttrs (lib.all (value: value != null) [
cfg.defaultRealm cfg.kdc cfg.kerberosAdminServer
]) {
${cfg.defaultRealm} = {
kdc = cfg.kdc;
admin_server = cfg.kerberosAdminServer;
};
};
domain_realm = optionalAttrs (lib.all (value: value != null) [
cfg.domainRealm cfg.defaultRealm
]) {
".${cfg.domainRealm}" = cfg.defaultRealm;
${cfg.domainRealm} = cfg.defaultRealm;
};
};
mergedConfig = (recursiveUpdate defaultConfig {
inherit (config.krb5)
kerberos libdefaults realms domain_realm capaths appdefaults plugins
extraConfig config;
});
filterEmbeddedMetadata = value: if isAttrs value then
(filterAttrs
(attrName: attrValue: attrName != "_module" && attrValue != null)
value)
else value;
indent = " ";
mkRelation = name: value:
if (isList value) then
concatMapStringsSep "\n" (mkRelation name) value
else "${name} = ${mkVal value}";
mkVal = value:
if (value == true) then "true"
else if (value == false) then "false"
else if (isInt value) then (toString value)
else if (isAttrs value) then
let configLines = concatLists
(map (splitString "\n")
(mapAttrsToList mkRelation value));
in
(concatStringsSep "\n${indent}"
([ "{" ] ++ configLines))
+ "\n}"
else value;
mkMappedAttrsOrString = value: concatMapStringsSep "\n"
(line: if builtins.stringLength line > 0
then "${indent}${line}"
else line)
(splitString "\n"
(if isAttrs value then
concatStringsSep "\n"
(mapAttrsToList mkRelation value)
else value));
format = import ./krb5-conf-format.nix { inherit pkgs lib; } { };
in {
###### interface
imports = [
(mkRemovedOptionModuleCfg "libdefaults")
(mkRemovedOptionModuleCfg "realms")
(mkRemovedOptionModuleCfg "domain_realm")
(mkRemovedOptionModuleCfg "capaths")
(mkRemovedOptionModuleCfg "appdefaults")
(mkRemovedOptionModuleCfg "plugins")
(mkRemovedOptionModuleCfg "config")
(mkRemovedOptionModuleCfg "extraConfig")
(mkRemovedOptionModule' "kerberos" ''
The option `krb5.kerberos' has been moved to `krb5.package'.
'')
];
options = {
krb5 = {
enable = mkEnableOption (lib.mdDoc "building krb5.conf, configuration file for Kerberos V");
kerberos = mkOption {
type = types.package;
default = pkgs.krb5;
defaultText = literalExpression "pkgs.krb5";
example = literalExpression "pkgs.heimdal";
description = lib.mdDoc ''
The Kerberos implementation that will be present in
`environment.systemPackages` after enabling this
service.
'';
enable = mkOption {
default = false;
description = mdDoc "Enable and configure Kerberos utilities";
type = bool;
};
libdefaults = mkOption {
type = with types; either attrs lines;
default = {};
apply = attrs: filterEmbeddedMetadata attrs;
example = literalExpression ''
{
package = mkPackageOption pkgs "krb5" {
example = "heimdal";
};
settings = mkOption {
default = { };
type = format.type;
description = mdDoc ''
Structured contents of the {file}`krb5.conf` file. See
{manpage}`krb5.conf(5)` for details about configuration.
'';
example = {
include = [ "/run/secrets/secret-krb5.conf" ];
includedir = [ "/run/secrets/secret-krb5.conf.d" ];
libdefaults = {
default_realm = "ATHENA.MIT.EDU";
};
'';
description = lib.mdDoc ''
Settings used by the Kerberos V5 library.
'';
};
realms = mkOption {
type = with types; either attrs lines;
default = {};
example = literalExpression ''
{
realms = {
"ATHENA.MIT.EDU" = {
admin_server = "athena.mit.edu";
kdc = [
@ -119,251 +62,25 @@ in {
];
};
};
'';
apply = attrs: filterEmbeddedMetadata attrs;
description = lib.mdDoc "Realm-specific contact information and settings.";
};
domain_realm = mkOption {
type = with types; either attrs lines;
default = {};
example = literalExpression ''
{
"example.com" = "EXAMPLE.COM";
".example.com" = "EXAMPLE.COM";
domain_realm = {
"mit.edu" = "ATHENA.MIT.EDU";
};
'';
apply = attrs: filterEmbeddedMetadata attrs;
description = lib.mdDoc ''
Map of server hostnames to Kerberos realms.
'';
};
capaths = mkOption {
type = with types; either attrs lines;
default = {};
example = literalExpression ''
{
"ATHENA.MIT.EDU" = {
"EXAMPLE.COM" = ".";
};
"EXAMPLE.COM" = {
"ATHENA.MIT.EDU" = ".";
};
logging = {
kdc = "SYSLOG:NOTICE";
admin_server = "SYSLOG:NOTICE";
default = "SYSLOG:NOTICE";
};
'';
apply = attrs: filterEmbeddedMetadata attrs;
description = lib.mdDoc ''
Authentication paths for non-hierarchical cross-realm authentication.
'';
};
appdefaults = mkOption {
type = with types; either attrs lines;
default = {};
example = literalExpression ''
{
pam = {
debug = false;
ticket_lifetime = 36000;
renew_lifetime = 36000;
max_timeout = 30;
timeout_shift = 2;
initial_timeout = 1;
};
};
'';
apply = attrs: filterEmbeddedMetadata attrs;
description = lib.mdDoc ''
Settings used by some Kerberos V5 applications.
'';
};
plugins = mkOption {
type = with types; either attrs lines;
default = {};
example = literalExpression ''
{
ccselect = {
disable = "k5identity";
};
};
'';
apply = attrs: filterEmbeddedMetadata attrs;
description = lib.mdDoc ''
Controls plugin module registration.
'';
};
extraConfig = mkOption {
type = with types; nullOr lines;
default = null;
example = ''
[logging]
kdc = SYSLOG:NOTICE
admin_server = SYSLOG:NOTICE
default = SYSLOG:NOTICE
'';
description = lib.mdDoc ''
These lines go to the end of `krb5.conf` verbatim.
`krb5.conf` may include any of the relations that are
valid for `kdc.conf` (see `man kdc.conf`),
but it is not a recommended practice.
'';
};
config = mkOption {
type = with types; nullOr lines;
default = null;
example = ''
[libdefaults]
default_realm = EXAMPLE.COM
[realms]
EXAMPLE.COM = {
admin_server = kerberos.example.com
kdc = kerberos.example.com
default_principal_flags = +preauth
}
[domain_realm]
example.com = EXAMPLE.COM
.example.com = EXAMPLE.COM
[logging]
kdc = SYSLOG:NOTICE
admin_server = SYSLOG:NOTICE
default = SYSLOG:NOTICE
'';
description = lib.mdDoc ''
Verbatim `krb5.conf` configuration. Note that this
is mutually exclusive with configuration via
`libdefaults`, `realms`,
`domain_realm`, `capaths`,
`appdefaults`, `plugins` and
`extraConfig` configuration options. Consult
`man krb5.conf` for documentation.
'';
};
defaultRealm = mkOption {
type = with types; nullOr str;
default = null;
example = "ATHENA.MIT.EDU";
description = lib.mdDoc ''
DEPRECATED, please use
`krb5.libdefaults.default_realm`.
'';
};
domainRealm = mkOption {
type = with types; nullOr str;
default = null;
example = "athena.mit.edu";
description = lib.mdDoc ''
DEPRECATED, please create a map of server hostnames to Kerberos realms
in `krb5.domain_realm`.
'';
};
kdc = mkOption {
type = with types; nullOr str;
default = null;
example = "kerberos.mit.edu";
description = lib.mdDoc ''
DEPRECATED, please pass a `kdc` attribute to a realm
in `krb5.realms`.
'';
};
kerberosAdminServer = mkOption {
type = with types; nullOr str;
default = null;
example = "kerberos.mit.edu";
description = lib.mdDoc ''
DEPRECATED, please pass an `admin_server` attribute
to a realm in `krb5.realms`.
'';
};
};
};
};
###### implementation
config = mkIf cfg.enable {
environment.systemPackages = [ cfg.kerberos ];
environment.etc."krb5.conf".text = if isString cfg.config
then cfg.config
else (''
[libdefaults]
${mkMappedAttrsOrString mergedConfig.libdefaults}
[realms]
${mkMappedAttrsOrString mergedConfig.realms}
[domain_realm]
${mkMappedAttrsOrString mergedConfig.domain_realm}
[capaths]
${mkMappedAttrsOrString mergedConfig.capaths}
[appdefaults]
${mkMappedAttrsOrString mergedConfig.appdefaults}
[plugins]
${mkMappedAttrsOrString mergedConfig.plugins}
'' + optionalString (mergedConfig.extraConfig != null)
("\n" + mergedConfig.extraConfig));
warnings = flatten [
(optional (cfg.defaultRealm != null) ''
The option krb5.defaultRealm is deprecated, please use
krb5.libdefaults.default_realm.
'')
(optional (cfg.domainRealm != null) ''
The option krb5.domainRealm is deprecated, please use krb5.domain_realm.
'')
(optional (cfg.kdc != null) ''
The option krb5.kdc is deprecated, please pass a kdc attribute to a
realm in krb5.realms.
'')
(optional (cfg.kerberosAdminServer != null) ''
The option krb5.kerberosAdminServer is deprecated, please pass an
admin_server attribute to a realm in krb5.realms.
'')
];
assertions = [
{ assertion = !((builtins.any (value: value != null) [
cfg.defaultRealm cfg.domainRealm cfg.kdc cfg.kerberosAdminServer
]) && ((builtins.any (value: value != {}) [
cfg.libdefaults cfg.realms cfg.domain_realm cfg.capaths
cfg.appdefaults cfg.plugins
]) || (builtins.any (value: value != null) [
cfg.config cfg.extraConfig
])));
message = ''
Configuration of krb5.conf by deprecated options is mutually exclusive
with configuration by section. Please migrate your config using the
attributes suggested in the warnings.
'';
}
{ assertion = !(cfg.config != null
&& ((builtins.any (value: value != {}) [
cfg.libdefaults cfg.realms cfg.domain_realm cfg.capaths
cfg.appdefaults cfg.plugins
]) || (builtins.any (value: value != null) [
cfg.extraConfig cfg.defaultRealm cfg.domainRealm cfg.kdc
cfg.kerberosAdminServer
])));
message = ''
Configuration of krb5.conf using krb.config is mutually exclusive with
configuration by section. If you want to mix the two, you can pass
lines to any configuration section or lines to krb5.extraConfig.
'';
}
];
environment = {
systemPackages = [ cfg.package ];
etc."krb5.conf".source = format.generate "krb5.conf" cfg.settings;
};
};
}

View File

@ -0,0 +1,88 @@
{ pkgs, lib, ... }:
# Based on
# - https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html
# - https://manpages.debian.org/unstable/heimdal-docs/krb5.conf.5heimdal.en.html
let
inherit (lib) boolToString concatMapStringsSep concatStringsSep filter
isAttrs isBool isList mapAttrsToList mdDoc mkOption singleton splitString;
inherit (lib.types) attrsOf bool coercedTo either int listOf oneOf path
str submodule;
in
{ }: {
type = let
section = attrsOf relation;
relation = either (attrsOf value) value;
value = either (listOf atom) atom;
atom = oneOf [int str bool];
in submodule {
freeformType = attrsOf section;
options = {
include = mkOption {
default = [ ];
description = mdDoc ''
Files to include in the Kerberos configuration.
'';
type = coercedTo path singleton (listOf path);
};
includedir = mkOption {
default = [ ];
description = mdDoc ''
Directories containing files to include in the Kerberos configuration.
'';
type = coercedTo path singleton (listOf path);
};
module = mkOption {
default = [ ];
description = mdDoc ''
Modules to obtain Kerberos configuration from.
'';
type = coercedTo path singleton (listOf path);
};
};
};
generate = let
indent = str: concatMapStringsSep "\n" (line: " " + line) (splitString "\n" str);
formatToplevel = args @ {
include ? [ ],
includedir ? [ ],
module ? [ ],
...
}: let
sections = removeAttrs args [ "include" "includedir" "module" ];
in concatStringsSep "\n" (filter (x: x != "") [
(concatStringsSep "\n" (mapAttrsToList formatSection sections))
(concatMapStringsSep "\n" (m: "module ${m}") module)
(concatMapStringsSep "\n" (i: "include ${i}") include)
(concatMapStringsSep "\n" (i: "includedir ${i}") includedir)
]);
formatSection = name: section: ''
[${name}]
${indent (concatStringsSep "\n" (mapAttrsToList formatRelation section))}
'';
formatRelation = name: relation:
if isAttrs relation
then ''
${name} = {
${indent (concatStringsSep "\n" (mapAttrsToList formatValue relation))}
}''
else formatValue name relation;
formatValue = name: value:
if isList value
then concatMapStringsSep "\n" (formatAtom name) value
else formatAtom name value;
formatAtom = name: atom: let
v = if isBool atom then boolToString atom else toString atom;
in "${name} = ${v}";
in
name: value: pkgs.writeText name ''
${formatToplevel value}
'';
}

View File

@ -3,7 +3,7 @@
let
inherit (lib) mkOption mkIf types length attrNames;
cfg = config.services.kerberos_server;
kerberos = config.krb5.kerberos;
kerberos = config.krb5.package;
aclEntry = {
options = {

View File

@ -4,7 +4,7 @@ let
inherit (lib) mkIf concatStringsSep concatMapStrings toList mapAttrs
mapAttrsToList;
cfg = config.services.kerberos_server;
kerberos = config.krb5.kerberos;
kerberos = config.krb5.package;
stateDir = "/var/heimdal";
aclFiles = mapAttrs
(name: {acl, ...}: pkgs.writeText "${name}.acl" (concatMapStrings ((

View File

@ -4,7 +4,7 @@ let
inherit (lib) mkIf concatStrings concatStringsSep concatMapStrings toList
mapAttrs mapAttrsToList;
cfg = config.services.kerberos_server;
kerberos = config.krb5.kerberos;
kerberos = config.krb5.package;
stateDir = "/var/lib/krb5kdc";
PIDFile = "/run/kdc.pid";
aclMap = {

View File

@ -9,14 +9,16 @@ import ../make-test-python.nix ({pkgs, ...}: {
};
krb5 = {
enable = true;
kerberos = pkgs.heimdal;
libdefaults = {
default_realm = "FOO.BAR";
};
realms = {
"FOO.BAR" = {
admin_server = "machine";
kdc = "machine";
package = pkgs.heimdal;
settings = {
libdefaults = {
default_realm = "FOO.BAR";
};
realms = {
"FOO.BAR" = {
admin_server = "machine";
kdc = "machine";
};
};
};
};

View File

@ -9,14 +9,16 @@ import ../make-test-python.nix ({pkgs, ...}: {
};
krb5 = {
enable = true;
kerberos = pkgs.krb5;
libdefaults = {
default_realm = "FOO.BAR";
};
realms = {
"FOO.BAR" = {
admin_server = "machine";
kdc = "machine";
package = pkgs.krb5;
settings = {
libdefaults = {
default_realm = "FOO.BAR";
};
realms = {
"FOO.BAR" = {
admin_server = "machine";
kdc = "machine";
};
};
};
};

View File

@ -1,5 +1,4 @@
{ system ? builtins.currentSystem }:
{
example-config = import ./example-config.nix { inherit system; };
deprecated-config = import ./deprecated-config.nix { inherit system; };
}

View File

@ -1,50 +0,0 @@
# Verifies that the configuration suggested in deprecated example values
# will result in the expected output.
import ../make-test-python.nix ({ pkgs, ...} : {
name = "krb5-with-deprecated-config";
meta = with pkgs.lib.maintainers; {
maintainers = [ eqyiel ];
};
nodes.machine =
{ ... }: {
krb5 = {
enable = true;
defaultRealm = "ATHENA.MIT.EDU";
domainRealm = "athena.mit.edu";
kdc = "kerberos.mit.edu";
kerberosAdminServer = "kerberos.mit.edu";
};
};
testScript =
let snapshot = pkgs.writeText "krb5-with-deprecated-config.conf" ''
[libdefaults]
default_realm = ATHENA.MIT.EDU
[realms]
ATHENA.MIT.EDU = {
admin_server = kerberos.mit.edu
kdc = kerberos.mit.edu
}
[domain_realm]
.athena.mit.edu = ATHENA.MIT.EDU
athena.mit.edu = ATHENA.MIT.EDU
[capaths]
[appdefaults]
[plugins]
'';
in ''
machine.succeed(
"diff /etc/krb5.conf ${snapshot}"
)
'';
})

View File

@ -11,79 +11,60 @@ import ../make-test-python.nix ({ pkgs, ...} : {
{ pkgs, ... }: {
krb5 = {
enable = true;
kerberos = pkgs.krb5;
libdefaults = {
default_realm = "ATHENA.MIT.EDU";
};
realms = {
"ATHENA.MIT.EDU" = {
admin_server = "athena.mit.edu";
kdc = [
"athena01.mit.edu"
"athena02.mit.edu"
];
package = pkgs.krb5;
settings = {
includedir = [
"/etc/krb5.conf.d"
];
include = [
"/etc/krb5-extra.conf"
];
libdefaults = {
default_realm = "ATHENA.MIT.EDU";
};
realms = {
"ATHENA.MIT.EDU" = {
admin_server = "athena.mit.edu";
kdc = [
"athena01.mit.edu"
"athena02.mit.edu"
];
};
};
domain_realm = {
"example.com" = "EXAMPLE.COM";
".example.com" = "EXAMPLE.COM";
};
capaths = {
"ATHENA.MIT.EDU" = {
"EXAMPLE.COM" = ".";
};
"EXAMPLE.COM" = {
"ATHENA.MIT.EDU" = ".";
};
};
appdefaults = {
pam = {
debug = false;
ticket_lifetime = 36000;
renew_lifetime = 36000;
max_timeout = 30;
timeout_shift = 2;
initial_timeout = 1;
};
};
plugins.ccselect.disable = "k5identity";
logging = {
kdc = "SYSLOG:NOTICE";
admin_server = "SYSLOG:NOTICE";
default = "SYSLOG:NOTICE";
};
};
domain_realm = {
"example.com" = "EXAMPLE.COM";
".example.com" = "EXAMPLE.COM";
};
capaths = {
"ATHENA.MIT.EDU" = {
"EXAMPLE.COM" = ".";
};
"EXAMPLE.COM" = {
"ATHENA.MIT.EDU" = ".";
};
};
appdefaults = {
pam = {
debug = false;
ticket_lifetime = 36000;
renew_lifetime = 36000;
max_timeout = 30;
timeout_shift = 2;
initial_timeout = 1;
};
};
plugins = {
ccselect = {
disable = "k5identity";
};
};
extraConfig = ''
[logging]
kdc = SYSLOG:NOTICE
admin_server = SYSLOG:NOTICE
default = SYSLOG:NOTICE
'';
};
};
testScript =
let snapshot = pkgs.writeText "krb5-with-example-config.conf" ''
[libdefaults]
default_realm = ATHENA.MIT.EDU
[realms]
ATHENA.MIT.EDU = {
admin_server = athena.mit.edu
kdc = athena01.mit.edu
kdc = athena02.mit.edu
}
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM
[capaths]
ATHENA.MIT.EDU = {
EXAMPLE.COM = .
}
EXAMPLE.COM = {
ATHENA.MIT.EDU = .
}
[appdefaults]
pam = {
debug = false
@ -94,15 +75,40 @@ import ../make-test-python.nix ({ pkgs, ...} : {
timeout_shift = 2
}
[capaths]
ATHENA.MIT.EDU = {
EXAMPLE.COM = .
}
EXAMPLE.COM = {
ATHENA.MIT.EDU = .
}
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM
[libdefaults]
default_realm = ATHENA.MIT.EDU
[logging]
admin_server = SYSLOG:NOTICE
default = SYSLOG:NOTICE
kdc = SYSLOG:NOTICE
[plugins]
ccselect = {
disable = k5identity
}
[logging]
kdc = SYSLOG:NOTICE
admin_server = SYSLOG:NOTICE
default = SYSLOG:NOTICE
[realms]
ATHENA.MIT.EDU = {
admin_server = athena.mit.edu
kdc = athena01.mit.edu
kdc = athena02.mit.edu
}
include /etc/krb5-extra.conf
includedir /etc/krb5.conf.d
'';
in ''
machine.succeed(

View File

@ -1,15 +1,17 @@
import ../make-test-python.nix ({ pkgs, lib, ... }:
let
krb5 =
{ enable = true;
domain_realm."nfs.test" = "NFS.TEST";
krb5 = {
enable = true;
settings = {
domain_realm."nfs.test" = "NFS.TEST";
libdefaults.default_realm = "NFS.TEST";
realms."NFS.TEST" =
{ admin_server = "server.nfs.test";
kdc = "server.nfs.test";
};
realms."NFS.TEST" = {
admin_server = "server.nfs.test";
kdc = "server.nfs.test";
};
};
};
hosts =
''