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