-
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathflake.nix
457 lines (438 loc) · 21.7 KB
/
flake.nix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = nixpkgs.legacyPackages.${system};
rustPlatform = pkgs.rustPlatform;
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
recipe = {lib, enableInteractive ? false}: rustPlatform.buildRustPackage {
pname = "himmelblau";
version = cargoToml.workspace.package.version;
src = with lib.fileset; toSource {
root = ./.;
fileset = difference (gitTracked ./.) (fileFilter
(file: file.hasExt "nix" || file.hasExt "md" || file == "Makefile") ./.);
};
outputs = [ "out" "man" ];
cargoLock = {
lockFile = ./Cargo.lock;
allowBuiltinFetchGit = true;
};
buildFeatures = lib.optionals enableInteractive [ "interactive" ];
nativeBuildInputs = [
pkgs.pkg-config rustPlatform.bindgenHook
];
buildInputs = with pkgs; [
talloc tevent ding-libs utf8proc
sqlite.dev openssl.dev libcap.dev
ldb.dev krb5.dev pcre2.dev
pam dbus.dev udev.dev
] ++ lib.optionals enableInteractive [
gobject-introspection.dev cairo.dev gdk-pixbuf.dev
libsoup.dev pango.dev atk.dev gtk3.dev webkitgtk_4_1
];
postBuild = "cp -r man $man/";
postInstall = "ln -s $out/lib/libnss_himmelblau.so $out/lib/libnss_himmelblau.so.2";
meta = with lib; {
description = "Himmelblau is an interoperability suite for Microsoft Azure Entra ID and Intune.";
homepage = "https://github.com/himmelblau-idm/himmelblau";
license = licenses.gpl3Plus;
maintainers = [{
name = "David Mulder";
email = "[email protected]";
github = "dmulder";
}];
platforms = platforms.linux;
};
};
in rec {
packages.himmelblau = pkgs.callPackage recipe {};
packages.himmelblau-desktop = pkgs.callPackage recipe { enableInteractive = true; };
packages.default = packages.himmelblau;
devShells.default = pkgs.mkShell {
name = "himmelblau-devshell";
inputsFrom = [ packages.himmelblau-desktop ];
nativeBuildInputs = with pkgs; [ rust-analyzer rustfmt clippy ];
};
}) // flake-utils.lib.eachDefaultSystemPassThrough (system: {
nixosModules.himmelblau = { pkgs, lib, config, ...}:
let cfg = config.services.himmelblau; in {
options = with lib; {
services.himmelblau = let
globalOptions = {
domains = mkOption {
type = types.listOf types.str;
example = [ "my.domain.com" ];
description = ''
REQUIRED: The list of configured domains. This must be specified, or no users
will be permitted to authenticate. The first user to authenticate to each
domain will be the owner of the device object in the directory. Typically
this would be the primary user of the device.
'';
};
debug = mkOption {
type = types.bool;
default = false;
description = ''
Configure whether the daemon will output debug messages to the journal.
'';
};
id_attr_map = mkOption {
type = types.enum [ "name" "uuid" ];
default = "name";
description = ''
Specify whether to map uid/gid based on the object name or the object uuid.
By object uuid mapping is the old default, but can cause authentication
issues over SSH. Mapping by name is recommeneded.
'';
};
enable_hello = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enroll users in Hello authentication. If disabled, MFA may be
required during each login. Disabling Hello authentication is recommeneded
when the host is public facing (such as via SSH).
WARNING: Hello authentication depends on openssl3. If your system does not
provide openssl3, Hello MUST be disabled or authentication will fail.
EL8 distros (such as Rocky Linux 8) DO NOT provide openssl3.
'';
};
hello_pin_min_length = mkOption {
type = types.int;
default = 6;
description = ''
The minimum length of the Hello authentication PIN. This PIN length cannot
be less than 6, and cannot exceed 32 characters. These are hard requirements
for the encryption algorithm.
'';
};
enable_sfa_fallback = mkOption {
type = types.bool;
default = false;
description = ''
Whether to permit attempting a SFA (password only) authentication when MFA
methods are unavailable. Sometimes this is possible when MFA has yet to be
configured. This is disabled by default.
'';
};
enable_experimental_mfa = mkOption {
type = types.bool;
default = true;
description = ''
This option enables the experimental MFA (multi-factor authentication) flow,
which permits Hello authentication. Note that this flow may fail in certain
edge cases. When disabled, the system will enforce the DAG (Device Authorization
Grant) flow for MFA, and Hello authentication will be disabled.
'';
};
cn_name_mapping = mkOption {
type = types.bool;
default = true;
description = ''
CN to UPN mapping allows users to simply enter the short form of their
username (`dave` instead of `[email protected]`). Himmelblau will only map CNs
to the primary domain (the first domain listed in the `domains` option
above). WARNING: CN mapping could mask local users, depending on your PAM
configuration.
'';
};
db_path = mkOption {
type = types.str;
default = "/var/cache/himmelblaud/himmelblau.cache.db";
description = "The location of the cache database";
};
hsm_pin_path = mkOption {
type = types.str;
default = "/var/lib/himmelblaud/hsm-pin";
description = "The location where the hsm pin will be stored";
};
socket_path = mkOption {
type = types.str;
default = "/var/run/himmelblaud/socket";
};
task_socket_path = mkOption {
type = types.str;
default = "/var/run/himmelblaud/task_sock";
};
broker_socket_path = mkOption {
type = types.str;
default = "/var/run/himmelblaud/broker_sock";
};
connection_timeout = mkOption {
type = types.ints.unsigned;
default = 2;
};
cache_timeout = mkOption {
type = types.ints.unsigned;
default = 300;
};
use_etc_skel = mkOption {
type = types.bool;
default = false;
};
selinux = mkOption {
type = types.bool;
default = false;
};
};
domainOptions = {
pam_allow_groups = mkOption {
default = null;
type = types.nullOr (types.listOf types.str);
example = [ "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" ];
description = ''
pam_allow_groups SHOULD be defined or else all users will be authorized by
pam account. The option should be set to a comma seperated list of Users and
Groups which are allowed access to the system. Groups MUST be specified by
Object ID, not by UPN. This is because Azure does not permit regular users
the right to read group names, only the Object IDs which they belong to.
'';
};
odc_provider = mkOption {
type = types.nullOr types.str;
default = null;
example = "odc.officeapps.live.com";
description = ''
If you have an ODC provider (the default being odc.officeapps.live.com), specify
the hostname for sending a federationProvider request. If the federationProvider
request is successful, the tenant_id and authority_host options do not need to
be specified.
'';
};
tenant_id = mkOption {
type = types.nullOr types.str;
default = null;
example = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
};
app_id = mkOption {
type = types.nullOr types.str;
default = null;
example = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
};
authority_host = mkOption {
type = types.str;
default = "login.microsoftonline.com";
};
local_groups = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "docker" ];
description = ''
A comma seperated list of local groups that every Entra Id user should be a
member of. For example, you may wish for all Entra Id users to be a member
of the sudo group. WARNING: This setting will not REMOVE group member entries
when groups are removed from this list. You must remove them manually.
'';
};
logon_script = mkOption {
type = types.nullOr types.str;
default = null;
example = "./logon.sh";
description = ''
Logon user script. This script will execute every time a user logs on. Two
environment variables are set: USERNAME, and ACCESS_TOKEN. The ACCESS_TOKEN
environment variable is an access token for the MS graph.
'';
};
logon_token_scopes = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = ''
The token scope config option sets the comma separated scopes that should be
requested for the ACCESS_TOKEN. ACCESS_TOKEN will be empty during offline logon.
The return code of the script determines how the authentication proceeds. 0 is
success, 1 is a soft failure and authentication will proceed, while 2 is a hard
failure causing authentication to fail.
'';
};
home_prefix = mkOption {
type = types.str;
default = "/home/";
};
home_attr = mkOption {
type = types.enum [ "UUID" "SPN" "CN" ];
default = "UUID";
};
home_alias = mkOption {
type = types.enum [ "UUID" "SPN" "CN" ];
default = "SPN";
};
shell = mkOption {
type = types.path;
default = "/run/current-system/sw/bin/bash";
};
idmap_range = mkOption {
type = types.str;
default = "5000000-5999999";
};
};
in {
enable = mkEnableOption "Himmelblau";
package = mkOption {
type = types.path;
default = self.packages.${system}.default;
description = "Package to use for Himmelblau service.";
};
debugFlag = mkOption {
type = types.bool;
default = false;
description = "Whether to pass the debug (-d) flag to the himmelblaud binary.";
};
pamServices = mkOption {
type = types.listOf types.str;
default = ["passwd" "login"];
description = "Which PAM services to add the himmelblau module to.";
};
settings = globalOptions // domainOptions; # settings submodule https://github.com/NixOS/rfcs/pull/42
domains = mkOption {
default = {};
type = types.attrsOf (types.submodule ({ name, config, ... }: { options = domainOptions; }));
description = ''
Setting to override the behaviour of himmelblau on a per-domain basis.
'';
};
};
};
config = lib.mkIf cfg.enable {
environment.etc."krb5.conf.d/krb5_himmelblau.conf".source = ./src/config/krb5_himmelblau.conf;
environment.etc."himmelblau/himmelblau.conf".text = with lib; let
mkValueString = v:
let err = t: v: abort ("mkValueString: ${t} not supported: ${toPretty {} v}");
in if isInt v then toString v
else if isFloat v then floatToString v
# convert derivations to store paths
else if isDerivation v then toString v
# we default to not quoting strings
else if isString v then v
else if true == v then "true"
else if false == v then "false"
# we space separate list elements, and recursively format them
else if isList v then concatMapStringsSep " " mkValueString v
# we don't support nulls, attrsets, or functions
else if null == v then err "null" v
else if isAttrs v then err "attrsets" v
else if isFunction v then err "functions" v
else err "this value is" (toString v);
# don't add null fields to config files
trimAttrs = filterAttrs (n: v: v != null);
# merge global section with named sections from domains
configFile = {global = trimAttrs cfg.settings; } // mapAttrs (k: v: trimAttrs v) cfg.domains;
# toINI generator generates configuration file from an attribute set
in generators.toINI { mkKeyValue = k: v: "${k} = ${mkValueString v}"; } configFile;
# Add himmelblau to the list of name services to lookup users/groups
system.nssModules = [ cfg.package ];
system.nssDatabases.passwd = [ "himmelblau" ]; # will be merged with entries from other modules
system.nssDatabases.group = [ "himmelblau" ]; # will be merged with entries from other modules
system.nssDatabases.shadow = [ "himmelblau" ]; # will be merged with entries from other modules
# Add entries for authenticating users via pam
security.pam.services = let
genServiceCfg = service: {
rules = let super = config.security.pam.services.${service}.rules; in {
account.himmelblau = {
order = super.account.unix.order - 10;
control = "sufficient";
modulePath = "${cfg.package}/lib/libpam_himmelblau.so";
settings.ignore_unknown_user = true;
settings.debug = cfg.debugFlag;
};
auth.himmelblau = {
order = super.auth.unix.order - 10;
control = "sufficient";
modulePath = "${cfg.package}/lib/libpam_himmelblau.so";
settings.debug = cfg.debugFlag;
};
session.himmelblau = {
order = super.session.unix.order - 10;
control = "optional";
modulePath = "${cfg.package}/lib/libpam_himmelblau.so";
settings.debug = cfg.debugFlag;
};
};
};
services = cfg.pamServices
++ lib.optional config.security.sudo.enable "sudo"
++ lib.optional config.security.doas.enable "doas"
++ lib.optional config.services.sshd.enable "sshd";
in lib.genAttrs services genServiceCfg;
systemd.services = let
commonServiceConfig = {
Type="notify";
UMask = "0027";
# SystemCallFilter = "@aio @basic-io @chown @file-system @io-event @network-io @sync";
NoNewPrivileges = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
MemoryDenyWriteExecute = true;
};
in {
himmelblaud = {
description = "Himmelblau Authentication Daemon";
wants = [ "chronyd.service" "ntpd.service" "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = commonServiceConfig // {
ExecStart = "${cfg.package}/bin/himmelblaud" + lib.optionalString cfg.debugFlag " -d";
Restart = "on-failure";
DynamicUser = "yes";
CacheDirectory = "himmelblaud"; # /var/cache/himmelblaud
RuntimeDirectory = "himmelblaud"; # /var/run/himmelblaud
StateDirectory = "himmelblaud"; # /var/lib/himmelblaud
PrivateTmp = true;
# We have to disable this to allow tpmrm0 access for tpm binding.
PrivateDevices = false;
};
};
himmelblaud-tasks = {
description = "Himmelblau Local Tasks";
bindsTo = [ "himmelblaud.service" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.shadow pkgs.bash ];
serviceConfig = commonServiceConfig // {
ExecStart = "${cfg.package}/bin/himmelblaud_tasks";
Restart = "on-failure";
User = "root";
ProtectSystem = "strict";
ReadWritePaths = "/home /var/run/himmelblaud /tmp /etc/krb5.conf.d /etc";
RestrictAddressFamilies = "AF_UNIX";
};
};
};
};
};
nixosConfigurations.testing = nixpkgs.lib.nixosSystem {
inherit system;
modules = [({pkgs, lib, ...}: {
imports = [ self.nixosModules.himmelblau ];
boot.isContainer = true; # stop nix flake check complaining about missing root fs
documentation.nixos.enable = false; # skip generating nixos docs
virtualisation.vmVariant = {
boot.isContainer = lib.mkForce false; # let vm variant create a virtual disk
virtualisation.graphics = false; # connect serial console to terminal
};
nix.nixPath = ["nixpkgs=${nixpkgs}"];
users.users.root.initialPassword = "test";
services.sshd.enable = true;
services.himmelblau = {
enable = true;
settings = {
domains = ["example.com"];
pam_allow_groups = [ "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" ];
};
domains."extra.com" = {
pam_allow_groups = [ "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" ];
shell = "/run/current-system/sw/bin/fish";
};
};
environment.systemPackages = with pkgs; [ pamtester ];
})];
};
});
}