Skip to content

Commit 2620c99

Browse files
martinpittmvollmer
authored andcommitted
realmd: Automatically set up Kerberos keytab for ws
When joining an IPA domain and the `ipa` tool is available (in package freeipa-client), set up Kerberos authentication for Cockpit's webserver: * HTTP clients like curl or web browsers use the HTTP/hostname Service Principal Name (SPN), so create it on demand for the host that runs cockpit-ws. * Request a keytab for ws, unless a key for that SPN already exists. This makes kerberos single-sign on work out of the box in the common case of IPA with an admin user/password. On leaving the domain, remove the corresponding SPNs from the ws keytab. Closes cockpit-project#8956
1 parent a762f8e commit 2620c99

File tree

3 files changed

+146
-32
lines changed

3 files changed

+146
-32
lines changed

doc/guide/sso.xml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
<para>To authenticate users, the server that Cockpit is running on must be
1414
joined to a domain. This can usually be accomplished using the
1515
<ulink url="http://freedesktop.org/software/realmd/docs/realm.html"><code>realm join example.com</code></ulink>
16-
1716
command.</para>
1817

1918
<para>The domain must be resolvable by DNS. For instance, the SRV records of the
@@ -26,7 +25,7 @@ _kerberos._udp.example.com has SRV record 0 100 88 dc.example.com
2625

2726
<para>The server running Cockpit should have a fully qualified name that ends with
2827
the domain name.</para>
29-
28+
3029
<para>There must be a valid Kerberos host key for the server in the <code>/etc/krb5.keytab</code>
3130
file. Alternatively, if you would like to use a different keytab, you can do so
3231
by placing it in <code>/etc/cockpit/krb5.keytab</code>. It may be necessary to
@@ -44,10 +43,21 @@ _kerberos._udp.example.com has SRV record 0 100 88 dc.example.com
4443
</varlistentry>
4544
</variablelist>
4645

47-
<para>The following command can be used to list the <code>/etc/krb5.keytab</code>:</para>
46+
<para>When joining an IPA domain with Cockpit and the <code>ipa</code> command line tool is
47+
available, both the service principal name and a <code>/etc/cockpit/krb5.keytab</code> get
48+
created automatically, so that Kerberos based single sign on into Cockpit works out of the
49+
box. If you want/need to do this by hand or in a script, the
50+
corresponding commands with IPA are:</para>
51+
52+
<programlisting>
53+
$ sudo ipa service-add --ok-as-delegate=true --force HTTP/[email protected]
54+
$ sudo ipa-getkeytab -p HTTP/[email protected] -k /etc/cockpit/krb5.keytab
55+
</programlisting>
56+
57+
<para>The following command can be used to list the <code>/etc/cockpit/krb5.keytab</code>:</para>
4858

4959
<programlisting>
50-
$ sudo klist -k
60+
$ sudo klist -k /etc/cockpit/krb5.keytab
5161
</programlisting>
5262

5363
<para>Lastly accounts from the domain must be resolvable to unix accounts on the server
@@ -60,7 +70,8 @@ [email protected]:*:381001109:381000513:User Name:/home/user:/bin/sh
6070

6171
<para>If you wish to delegate your kerberos credentials to Cockpit, and allow Cockpit
6272
to then connect to other machines using those credentials, you should enable delegation
63-
for the hosts running Cockpit, and in some cases the <code>HTTP</code> service as well.</para>
73+
for the hosts running Cockpit, and in some cases the <code>HTTP</code> service as well.
74+
When joining an IPA domain, this is enabled by default.</para>
6475

6576
</section>
6677

pkg/realmd/operation.js

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
var SERVICE = "org.freedesktop.realmd.Service";
1313
var PROVIDER = "org.freedesktop.realmd.Provider";
14+
var KERBEROS = "org.freedesktop.realmd.Kerberos";
1415
var KERBEROS_MEMBERSHIP = "org.freedesktop.realmd.KerberosMembership";
1516
var REALM = "org.freedesktop.realmd.Realm";
1617

@@ -28,6 +29,7 @@
2829
var checking = null;
2930
var checked = null;
3031
var kerberos_membership = null;
32+
var kerberos = null;
3133

3234
/* If in an operation first time cancel is clicked, cancel operation */
3335
$(".realms-op-cancel").on("click", function() {
@@ -161,12 +163,15 @@
161163

162164
realm = null;
163165
kerberos_membership = null;
166+
kerberos = null;
164167

165168
dfd.reject(new Error(message));
166169
} else {
167170
kerberos_membership = realmd.proxy(KERBEROS_MEMBERSHIP, path);
168171
$(kerberos_membership).on("changed", update);
169172

173+
kerberos = realmd.proxy(KERBEROS, path);
174+
170175
realm = realmd.proxy(REALM, path);
171176
$(realm).on("changed", update);
172177
realm.wait(function() {
@@ -320,6 +325,71 @@
320325
return creds;
321326
}
322327

328+
// Request and install a kerberos keytab for cockpit-ws (with IPA)
329+
// This is opportunistic: Some realms might not use IPA, or an unsupported auth mechanism
330+
function install_ws_keytab() {
331+
// skip this on remote ssh hosts, only set up ws hosts
332+
if (cockpit.transport.host !== "localhost")
333+
return cockpit.resolve();
334+
335+
if (auth !== "password/administrator") {
336+
console.log("Installing kerberos keytab not supported for auth mode", auth);
337+
return cockpit.resolve();
338+
}
339+
340+
var user = $(".realms-op-admin").val();
341+
var password = $(".realms-op-admin-password").val();
342+
343+
// ipa-getkeytab needs root to create the file
344+
var script = 'set -eu; [ $(id -u) = 0 ] || exit 0; ';
345+
// not an IPA setup? cannot handle this
346+
script += 'type ipa >/dev/null 2>&1 || exit 0; ';
347+
348+
// IPA operations require auth; read password from stdin to avoid quoting issues
349+
// if kinit fails, we can't handle this setup, exit cleanly
350+
script += 'kinit ' + user + '@' + kerberos.RealmName + ' || exit 0; ';
351+
352+
// create a kerberos Service Principal Name for cockpit-ws, unless already present
353+
script += 'service="HTTP/$(hostname -f)@' + kerberos.RealmName + '"; ' +
354+
'ipa service-show "$service" || ipa service-add --ok-as-delegate=true --force "$service"; ';
355+
356+
// add cockpit-ws key, unless already present
357+
script += 'mkdir -p /etc/cockpit; ';
358+
script += 'klist -k /etc/cockpit/krb5.keytab | grep -qF "$service" || ' +
359+
'ipa-getkeytab -p HTTP/$(hostname -f) -k /etc/cockpit/krb5.keytab; ';
360+
361+
// use a temporary keytab to avoid interfering with the system one
362+
var proc = cockpit.script(script, [], { superuser: "require", err: "message",
363+
environ: ["KRB5CCNAME=/run/cockpit/keytab-setup"] });
364+
proc.input(password);
365+
return proc;
366+
}
367+
368+
// Remove SPN from cockpit-ws keytab
369+
function cleanup_ws_keytab() {
370+
// skip this on remote ssh hosts, only set up ws hosts
371+
if (cockpit.transport.host !== "localhost")
372+
return cockpit.resolve();
373+
374+
var dfd = cockpit.defer();
375+
376+
kerberos = realmd.proxy(KERBEROS, realm.path);
377+
kerberos.wait()
378+
.done(function() {
379+
cockpit.script('[ ! -e /etc/cockpit/krb5.keytab ] || ipa-rmkeytab -k /etc/cockpit/krb5.keytab -p ' +
380+
'"HTTP/$(hostname -f)@' + kerberos.RealmName + '"',
381+
[], { superuser: "require", err: "message" })
382+
.done(dfd.resolve)
383+
.fail(function(ex) {
384+
console.log("Failed to clean up SPN from /etc/cockpit/krb5.keytab:", JSON.stringify(ex));
385+
dfd.resolve();
386+
});
387+
})
388+
.fail(dfd.resolve); // no Kerberos domain? nevermind then
389+
390+
return dfd.promise();
391+
}
392+
323393
var unique = 1;
324394

325395
function perform() {
@@ -351,14 +421,14 @@
351421
if (computer_ou)
352422
options["computer-ou"] = cockpit.variant('s', computer_ou);
353423
if (kerberos_membership.valid) {
354-
call = kerberos_membership.call("Join", [ credentials(), options ]);
424+
call = kerberos_membership.call("Join", [ credentials(), options ]).then(install_ws_keytab);
355425
} else {
356426
busy(null);
357427
$(".realms-op-message").empty().text(_("Joining this domain is not supported"));
358428
$(".realms-op-error").show();
359429
}
360430
} else if (mode == 'leave') {
361-
call = realm.Deconfigure(options);
431+
call = cleanup_ws_keytab().then(function() { realm.Deconfigure(options); });
362432
}
363433

364434
if (!call) {
@@ -372,7 +442,7 @@
372442
if (ex.name == "org.freedesktop.realmd.Error.Cancelled") {
373443
$(dialog).modal("hide");
374444
} else {
375-
console.log("Failed to join domain: " + realm.Name + ": " + ex);
445+
console.log("Failed to " + mode + " domain: " + realm.Name + ": " + ex);
376446
$(".realms-op-message").empty().text(ex + " ");
377447
$(".realms-op-error").show();
378448
if (diagnostics) {

test/verify/check-realms

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,34 @@ import parent
2222
from testlib import *
2323

2424
import re
25+
import subprocess
26+
27+
WAIT_KRB_SCRIPT = """
28+
# HACK: This needs to work, but may take a minute
29+
for x in $(seq 1 60); do
30+
if getent passwd [email protected]; then
31+
break
32+
fi
33+
if systemctl --quiet is-failed sssd.service; then
34+
systemctl status --lines=100 sssd.service >&2
35+
exit 1
36+
fi
37+
sleep $x
38+
done
39+
40+
# This directory should be owned by the domain user
41+
getent passwd [email protected]
42+
chown -R [email protected] /home/admin
43+
44+
# HACK: This needs to work but may take a minute
45+
for x in $(seq 1 60); do
46+
if ssh -oStrictHostKeyChecking=no -oBatchMode=yes -l [email protected] x0.cockpit.lan true; then
47+
break
48+
fi
49+
sleep $x
50+
done
51+
"""
52+
2553

2654
@skipImage("No realmd available", "continuous-atomic", "fedora-atomic", "rhel-atomic")
2755
@skipImage("No freeipa available", "debian-stable", "debian-testing")
@@ -67,6 +95,31 @@ class TestRealms(MachineCase):
6795
# Check that this has worked
6896
wait_number_domains(1)
6997

98+
# should not have any leftover tickets from the joining
99+
m.execute("! klist")
100+
m.execute("! su -c klist admin")
101+
102+
# validate Kerberos setup for ws; requires FreeIPA with the "ipa" tool
103+
if m.image not in ["rhel-7-5", "ubuntu-1604", "ubuntu-stable"]:
104+
m.execute("echo foobarfoo | kinit -f [email protected]")
105+
m.execute(script=WAIT_KRB_SCRIPT, timeout=300)
106+
107+
# should have added SPN to ws keytab
108+
output = m.execute(['klist', '-k', '/etc/cockpit/krb5.keytab'])
109+
self.assertIn('HTTP/[email protected]', output)
110+
111+
# kerberos login should work
112+
output = m.execute(['curl', '-s', '--negotiate', '--delegation', 'always', '-u', ':', "-D", "-",
113+
'http://x0.cockpit.lan:9090/cockpit/login'])
114+
self.assertIn("HTTP/1.1 200 OK", output)
115+
self.assertIn('"csrf-token"', output)
116+
elif m.image != "rhel-7-5": # on 7.5 with older ws curl succeeds
117+
# curl negotiation should fail without a proper ws keytab; this provides a hint
118+
# when FreeIPA becomes new enough on the images above to start working
119+
self.assertRaisesRegexp(subprocess.CalledProcessError, "returned non-zero exit status 6", m.execute,
120+
['curl', '-s', '--negotiate', '--delegation', 'always', '-u:', '-D-',
121+
'http://x0.cockpit.lan:9090/cockpit/login'])
122+
70123
# Leave the domain
71124
b.click("#system-info-domain a")
72125
b.wait_popup("realms-op")
@@ -76,6 +129,9 @@ class TestRealms(MachineCase):
76129
b.wait_popdown("realms-op")
77130
wait_number_domains(0)
78131

132+
# should have cleaned up ws keytab
133+
m.execute("! klist -k /etc/cockpit/krb5.keytab | grep COCKPIT.LAN")
134+
79135
# Send a wrong password
80136
b.click("#system-info-domain a")
81137
b.wait_popup("realms-op")
@@ -167,30 +223,6 @@ else
167223
curl --insecure -s --negotiate -u : https://f0.cockpit.lan/ipa/json --header 'Referer: https://f0.cockpit.lan/ipa' --header "Content-Type: application/json" --header "Accept: application/json" --data '{"params": [["HTTP/[email protected]"], {"raw": false, "all": false, "version": "2.101", "force": true, "no_members": false, "ipakrbokasdelegate": true}], "method": "service_add", "id": 0}'
168224
fi
169225
ipa-getkeytab -p HTTP/x0.cockpit.lan -k %(keytab)s
170-
171-
# HACK: This needs to work, but may take a minute
172-
for x in $(seq 1 60); do
173-
if getent passwd [email protected]; then
174-
break
175-
fi
176-
if systemctl --quiet is-failed sssd.service; then
177-
systemctl status --lines=100 sssd.service >&2
178-
exit 1
179-
fi
180-
sleep $x
181-
done
182-
183-
# This directory should be owned by the domain user
184-
getent passwd [email protected]
185-
chown -R [email protected] /home/admin
186-
187-
# HACK: This needs to work but may take a minute
188-
for x in $(seq 1 60); do
189-
if ssh -oStrictHostKeyChecking=no -oBatchMode=yes -l [email protected] x0.cockpit.lan true; then
190-
break
191-
fi
192-
sleep $x
193-
done
194226
"""
195227

196228
# This is here because our test framework can't run ipa VM's twice
@@ -210,6 +242,7 @@ class TestKerberos(MachineCase):
210242
# no nss-myhostname there
211243
self.machine.execute("echo '10.111.113.1 x0.cockpit.lan' >> /etc/hosts")
212244
self.machine.execute(script=JOIN_SCRIPT % args, timeout=1800)
245+
self.machine.execute(script=WAIT_KRB_SCRIPT, timeout=300)
213246

214247
def testNegotiate(self):
215248
self.allow_authorize_journal_messages()

0 commit comments

Comments
 (0)