1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7with lib; let
8 cfg = config.security.ipa;
9 pyBool = x:
10 if x
11 then "True"
12 else "False";
13
14 ldapConf = pkgs.writeText "ldap.conf" ''
15 # Turning this off breaks GSSAPI used with krb5 when rdns = false
16 SASL_NOCANON on
17
18 URI ldaps://${cfg.server}
19 BASE ${cfg.basedn}
20 TLS_CACERT /etc/ipa/ca.crt
21 '';
22 nssDb =
23 pkgs.runCommand "ipa-nssdb"
24 {
25 nativeBuildInputs = [pkgs.nss.tools];
26 } ''
27 mkdir -p $out
28 certutil -d $out -N --empty-password
29 certutil -d $out -A --empty-password -n "${cfg.realm} IPA CA" -t CT,C,C -i ${cfg.certificate}
30 '';
31in {
32 options = {
33 security.ipa = {
34 enable = mkEnableOption (lib.mdDoc "FreeIPA domain integration");
35
36 certificate = mkOption {
37 type = types.package;
38 description = lib.mdDoc ''
39 IPA server CA certificate.
40
41 Use `nix-prefetch-url http://$server/ipa/config/ca.crt` to
42 obtain the file and the hash.
43 '';
44 example = literalExpression ''
45 pkgs.fetchurl {
46 url = http://ipa.example.com/ipa/config/ca.crt;
47 sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
48 };
49 '';
50 };
51
52 domain = mkOption {
53 type = types.str;
54 example = "example.com";
55 description = lib.mdDoc "Domain of the IPA server.";
56 };
57
58 realm = mkOption {
59 type = types.str;
60 example = "EXAMPLE.COM";
61 description = lib.mdDoc "Kerberos realm.";
62 };
63
64 server = mkOption {
65 type = types.str;
66 example = "ipa.example.com";
67 description = lib.mdDoc "IPA Server hostname.";
68 };
69
70 basedn = mkOption {
71 type = types.str;
72 example = "dc=example,dc=com";
73 description = lib.mdDoc "Base DN to use when performing LDAP operations.";
74 };
75
76 offlinePasswords = mkOption {
77 type = types.bool;
78 default = true;
79 description = lib.mdDoc "Whether to store offline passwords when the server is down.";
80 };
81
82 cacheCredentials = mkOption {
83 type = types.bool;
84 default = true;
85 description = lib.mdDoc "Whether to cache credentials.";
86 };
87
88 ifpAllowedUids = mkOption {
89 type = types.listOf types.str;
90 default = ["root"];
91 description = lib.mdDoc "A list of users allowed to access the ifp dbus interface.";
92 };
93
94 dyndns = {
95 enable = mkOption {
96 type = types.bool;
97 default = true;
98 description = lib.mdDoc "Whether to enable FreeIPA automatic hostname updates.";
99 };
100
101 interface = mkOption {
102 type = types.str;
103 example = "eth0";
104 default = "*";
105 description = lib.mdDoc "Network interface to perform hostname updates through.";
106 };
107 };
108
109 chromiumSupport = mkOption {
110 type = types.bool;
111 default = true;
112 description = lib.mdDoc "Whether to whitelist the FreeIPA domain in Chromium.";
113 };
114 };
115 };
116
117 config = mkIf cfg.enable {
118 assertions = [
119 {
120 assertion = !config.krb5.enable;
121 message = "krb5 must be disabled through `krb5.enable` for FreeIPA integration to work.";
122 }
123 {
124 assertion = !config.users.ldap.enable;
125 message = "ldap must be disabled through `users.ldap.enable` for FreeIPA integration to work.";
126 }
127 ];
128
129 environment.systemPackages = with pkgs; [krb5Full freeipa];
130
131 environment.etc = {
132 "ipa/default.conf".text = ''
133 [global]
134 basedn = ${cfg.basedn}
135 realm = ${cfg.realm}
136 domain = ${cfg.domain}
137 server = ${cfg.server}
138 host = ${config.networking.hostName}
139 xmlrpc_uri = https://${cfg.server}/ipa/xml
140 enable_ra = True
141 '';
142
143 "ipa/nssdb".source = nssDb;
144
145 "krb5.conf".text = ''
146 [libdefaults]
147 default_realm = ${cfg.realm}
148 dns_lookup_realm = false
149 dns_lookup_kdc = true
150 rdns = false
151 ticket_lifetime = 24h
152 forwardable = true
153 udp_preference_limit = 0
154
155 [realms]
156 ${cfg.realm} = {
157 kdc = ${cfg.server}:88
158 master_kdc = ${cfg.server}:88
159 admin_server = ${cfg.server}:749
160 default_domain = ${cfg.domain}
161 pkinit_anchors = FILE:/etc/ipa/ca.crt
162 }
163
164 [domain_realm]
165 .${cfg.domain} = ${cfg.realm}
166 ${cfg.domain} = ${cfg.realm}
167 ${cfg.server} = ${cfg.realm}
168
169 [dbmodules]
170 ${cfg.realm} = {
171 db_library = ${pkgs.freeipa}/lib/krb5/plugins/kdb/ipadb.so
172 }
173 '';
174
175 "openldap/ldap.conf".source = ldapConf;
176 };
177
178 environment.etc."chromium/policies/managed/freeipa.json" = mkIf cfg.chromiumSupport {
179 text = ''
180 { "AuthServerWhitelist": "*.${cfg.domain}" }
181 '';
182 };
183
184 system.activationScripts.ipa = stringAfter ["etc"] ''
185 # libcurl requires a hard copy of the certificate
186 if ! ${pkgs.diffutils}/bin/diff ${cfg.certificate} /etc/ipa/ca.crt > /dev/null 2>&1; then
187 rm -f /etc/ipa/ca.crt
188 cp ${cfg.certificate} /etc/ipa/ca.crt
189 fi
190
191 if [ ! -f /etc/krb5.keytab ]; then
192 cat <<EOF
193
194 In order to complete FreeIPA integration, please join the domain by completing the following steps:
195 1. Authenticate as an IPA user authorized to join new hosts, e.g. kinit admin@${cfg.realm}
196 2. Join the domain and obtain the keytab file: ipa-join
197 3. Install the keytab file: sudo install -m 600 krb5.keytab /etc/
198 4. Restart sssd systemd service: sudo systemctl restart sssd
199
200 EOF
201 fi
202 '';
203
204 services.sssd.config = ''
205 [domain/${cfg.domain}]
206 id_provider = ipa
207 auth_provider = ipa
208 access_provider = ipa
209 chpass_provider = ipa
210
211 ipa_domain = ${cfg.domain}
212 ipa_server = _srv_, ${cfg.server}
213 ipa_hostname = ${config.networking.hostName}.${cfg.domain}
214
215 cache_credentials = ${pyBool cfg.cacheCredentials}
216 krb5_store_password_if_offline = ${pyBool cfg.offlinePasswords}
217 ${optionalString ((toLower cfg.domain) != (toLower cfg.realm))
218 "krb5_realm = ${cfg.realm}"}
219
220 dyndns_update = ${pyBool cfg.dyndns.enable}
221 dyndns_iface = ${cfg.dyndns.interface}
222
223 ldap_tls_cacert = /etc/ipa/ca.crt
224 ldap_user_extra_attrs = mail:mail, sn:sn, givenname:givenname, telephoneNumber:telephoneNumber, lock:nsaccountlock
225
226 [sssd]
227 debug_level = 65510
228 services = nss, sudo, pam, ssh, ifp
229 domains = ${cfg.domain}
230
231 [nss]
232 homedir_substring = /home
233
234 [pam]
235 pam_pwd_expiration_warning = 3
236 pam_verbosity = 3
237
238 [sudo]
239 debug_level = 65510
240
241 [autofs]
242
243 [ssh]
244
245 [pac]
246
247 [ifp]
248 user_attributes = +mail, +telephoneNumber, +givenname, +sn, +lock
249 allowed_uids = ${concatStringsSep ", " cfg.ifpAllowedUids}
250 '';
251
252 services.ntp.servers = singleton cfg.server;
253 services.sssd.enable = true;
254 services.ntp.enable = true;
255
256 security.pki.certificateFiles = singleton cfg.certificate;
257 };
258}