Permalink
Browse files

ssh: Move to /etc/ssh/ssh_known_hosts

There is no real reason for maintaining our own
/var/lib/cockpit/known_hosts file, as ssh itself already has a global
one in /etc/ssh/ssh_known_hosts. Use that by default, but fallback to
the legacy file for (1) lookups if a host is not already known in the
former but known in the latter; and (2) for writing if the ws we talk to
is still an old version (by checking if ws still has the "ssh"
capability).

Move the determination and setting of the known hosts file into a new
set_knownhosts_file() function, as it is now reasonably complex, will
be extended further in the future with more sources of known hosts, and
avoids handling SSH_OPTIONS_KNOWNHOSTS in multiple different places.

Adjust the integration tests to the new path and add new tests for
covering the fallback to the legacy file.

Reviewed-by: Peter Volpe <pvolpe@redhat.com>
  • Loading branch information...
martinpitt authored and stefwalter committed Mar 3, 2017
1 parent 5ab677e commit a12f0729921906d92a0398b47af216a97cb6f6a9
View
@@ -78,7 +78,7 @@ has the same options as the other authentication sections with the following add
* ```host``` The default host to log into. Defaults to 127.0.0.1.
* ```allowUnknown```. By default cockpit will refuse to connect to any machines that
are not already present in it's known_hosts file (usually ```/var/lib/cockpit/known_hosts```).
are not already present in ssh's global known_hosts file (usually ```/etc/ssh/ssh_known_hosts```).
Set this to ```true``` is to allow those connections to proceed.
# Actions
@@ -117,7 +117,7 @@ The following environment variables are set by cockpit-ws when spawning an auth
The following environment variables are used to set options for the ```cockpit-ssh``` process.
* **COCKPIT_SSH_ALLOW_UNKNOWN**` Set to ```1``` to allow connecting to hosts that are not saved in the current knownhosts file. If not set cockpit will only connect to unknown hosts if either the remote_peer is local or if the ```Ssh-Login``` section in ```cockpit.conf``` has a ```allowUnknown``` option set to a truthy value (```1```, ```yes``` or ```true```).
* **COCKPIT_SSH_KNOWN_HOSTS_FILE** Path to knownhost files. Defaults to ```PACKAGE_LOCALSTATE_DIR/known_hosts```
* **COCKPIT_SSH_KNOWN_HOSTS_FILE** Path to knownhost files. Defaults to ```PACKAGE_SYSCONF_DIR/ssh/ssh_known_hosts```
* **COCKPIT_SSH_KNOWN_HOSTS_DATA** Known host data to validate against or '*' to skip validation```
* **COCKPIT_SSH_BRIDGE_COMMAND** Command to launch after a ssh connection is established. Defaults to ```cockpit-bridge``` if not provided.
* **COCKPIT_SSH_SUPPORTS_HOST_KEY_PROMPT** Set to ```1``` if caller supports prompting users for unknown host keys.
@@ -16,7 +16,7 @@
necessary APIs in libssh.</para>
<para>SSH host keys are stored in
<filename>/var/lib/cockpit/known_hosts</filename>.</para>
<filename>/etc/ssh/ssh_known_hosts</filename>.</para>
<para>The machine data is stored in
<filename>/etc/cockpit/machines.d/*.json</filename>. Settings in
View
@@ -6,7 +6,7 @@
var mod = { };
var known_hosts_path = "/var/lib/cockpit/known_hosts";
var known_hosts_path = "/etc/ssh/ssh_known_hosts";
/*
* We share the Machines state between multiple frames. Only
* one frame has the job of loading the state, usually index.js
@@ -726,6 +726,9 @@
if ($.inArray("ssh", caps) > -1) {
mod.allow_connection_string = $.inArray("connection-string", caps) != -1;
mod.has_auth_results = $.inArray("auth-method-results", caps) != -1;
known_hosts_path = "/var/lib/cockpit/known_hosts";
mod.known_hosts_path = known_hosts_path;
console.debug("Running against legacy ws with ssh, using legacy file", known_hosts_path);
} else {
mod.allow_connection_string = true;
mod.has_auth_results = true;
@@ -19,6 +19,7 @@
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <stdio.h>
@@ -268,7 +269,10 @@ cockpit_is_host_known (const gchar *known_hosts_file,
if (!file)
{
g_message ("failed to open known hosts file %s", known_hosts_file);
if (errno == ENOENT)
g_debug ("known hosts file %s does not exist", known_hosts_file);
else
g_message ("failed to open known hosts file %s: %m", known_hosts_file);
return FALSE;
}
View
@@ -51,6 +51,9 @@
#define AUTH_FD 3
/* we had a private one before moving to /etc/ssh/ssh_known_hosts */
#define LEGACY_KNOWN_HOSTS PACKAGE_LOCALSTATE_DIR "/known_hosts"
typedef struct {
const gchar *logname;
gchar *initial_auth_data;
@@ -72,6 +75,8 @@ typedef struct {
} CockpitSshData;
static gchar *tmp_knownhost_file;
static const gchar*
exit_code_problem (int exit_code)
{
@@ -451,13 +456,105 @@ prompt_for_host_key (CockpitSshData *data)
return ret;
}
static void cleanup_knownhosts_file (void)
{
if (tmp_knownhost_file)
{
g_unlink (tmp_knownhost_file);
g_free (tmp_knownhost_file);
}
}
/**
* set_knownhosts_file:
*
* Check the various ssh known hosts locations and set the appropriate one into
* SSH_OPTIONS_KNOWNHOSTS.
*
* Returns: error string or %NULL on success.
*/
static const gchar *
set_knownhosts_file (CockpitSshData *data,
const gchar* host,
const guint port)
{
gboolean host_known;
/* $COCKPIT_SSH_KNOWN_HOSTS_DATA has highest priority */
if (data->ssh_options->knownhosts_data)
{
FILE *fp = NULL;
tmp_knownhost_file = create_knownhosts_temp ();
if (!tmp_knownhost_file)
return "internal-error";
atexit (cleanup_knownhosts_file);
fp = fopen (tmp_knownhost_file, "a");
if (fp == NULL)
{
g_warning ("%s: couldn't open temporary known host file for data: %s",
data->logname, tmp_knownhost_file);
return "internal-error";
}
if (fputs (data->ssh_options->knownhosts_data, fp) < 0)
{
g_warning ("%s: couldn't write to data to temporary known host file: %s",
data->logname, g_strerror (errno));
fclose (fp);
return "internal-error";
}
fclose (fp);
data->ssh_options->knownhosts_file = tmp_knownhost_file;
}
/* now check the default global ssh file */
host_known = cockpit_is_host_known (data->ssh_options->knownhosts_file, host, port);
/* if we check the default system known hosts file (i. e. not during the test
* suite), also check the legacy file in /var/lib/cockpit; we need to do that
* even with allow_unknown_hosts as subsequent code relies on knownhosts_file */
if (!host_known && strcmp (data->ssh_options->knownhosts_file, cockpit_get_default_knownhosts ()) == 0)
{
host_known = cockpit_is_host_known (LEGACY_KNOWN_HOSTS, host, port);
if (host_known)
{
g_debug ("%s: not known in %s but in legacy file %s",
data->logname,
data->ssh_options->knownhosts_file,
LEGACY_KNOWN_HOSTS);
data->ssh_options->knownhosts_file = LEGACY_KNOWN_HOSTS;
}
}
/* TODO: Check more sources of known hosts here if !host_known */
g_debug ("%s: using known hosts file %s", data->logname, data->ssh_options->knownhosts_file);
if (ssh_options_set (data->session, SSH_OPTIONS_KNOWNHOSTS,
data->ssh_options->knownhosts_file) != SSH_OK)
{
g_warning ("Couldn't set knownhosts file location");
return "internal-error";
}
if (!data->ssh_options->allow_unknown_hosts && !host_known)
{
g_message ("%s: refusing to connect to unknown host: %s:%d",
data->logname, host, port);
return "unknown-host";
}
return NULL;
}
static const gchar *
verify_knownhost (CockpitSshData *data)
verify_knownhost (CockpitSshData *data,
const gchar* host,
const guint port)
{
FILE *fp = NULL;
const gchar *knownhosts_file;
gchar *tmp_knownhost_file = NULL;
const gchar *ret = "invalid-hostkey";
const gchar *r;
ssh_key key = NULL;
unsigned char *hash = NULL;
int state;
@@ -497,46 +594,10 @@ verify_knownhost (CockpitSshData *data)
ssh_clean_pubkey_hash (&hash);
}
if (data->ssh_options->knownhosts_data)
r = set_knownhosts_file (data, host, port);
if (r != NULL)
{
tmp_knownhost_file = create_knownhosts_temp ();
if (!tmp_knownhost_file)
{
ret = "internal-error";
goto done;
}
fp = fopen (tmp_knownhost_file, "a");
if (fp == NULL)
{
g_warning ("%s: couldn't open temporary known host file for data: %s",
data->logname, tmp_knownhost_file);
ret = "internal-error";
goto done;
}
if (fputs (data->ssh_options->knownhosts_data, fp) < 0)
{
g_warning ("%s: couldn't write to data to temporary known host file: %s",
data->logname, g_strerror (errno));
ret = "internal-error";
fclose (fp);
goto done;
}
fclose (fp);
knownhosts_file = tmp_knownhost_file;
}
else
{
knownhosts_file = data->ssh_options->knownhosts_file;
}
if (ssh_options_set (data->session, SSH_OPTIONS_KNOWNHOSTS,
knownhosts_file) != SSH_OK)
{
g_warning ("Couldn't set knownhosts file location");
ret = "internal-error";
ret = r;
goto done;
}
@@ -587,12 +648,6 @@ verify_knownhost (CockpitSshData *data)
}
done:
if (tmp_knownhost_file)
{
g_unlink (tmp_knownhost_file);
g_free (tmp_knownhost_file);
}
if (key)
ssh_key_free (key);
return ret;
@@ -1202,20 +1257,6 @@ cockpit_ssh_connect (CockpitSshData *data,
g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_PORT, &port) == 0);
g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_HOST, host) == 0);;
g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_KNOWNHOSTS,
data->ssh_options->knownhosts_file) == 0);
if (!data->ssh_options->allow_unknown_hosts)
{
if (!cockpit_is_host_known (data->ssh_options->knownhosts_file,
host, port))
{
g_message ("%s: refusing to connect to unknown host: %s:%d",
data->logname, host, port);
problem = "unknown-host";
goto out;
}
}
rc = ssh_connect (data->session);
if (rc != SSH_OK)
@@ -1230,7 +1271,7 @@ cockpit_ssh_connect (CockpitSshData *data,
if (!data->ssh_options->ignore_hostkey)
{
problem = verify_knownhost (data);
problem = verify_knownhost (data, host, port);
if (problem != NULL)
goto out;
}
@@ -21,7 +21,7 @@
#include "cockpitauthoptions.h"
static const gchar *default_knownhosts = PACKAGE_LOCALSTATE_DIR "/known_hosts";
static const gchar *default_knownhosts = PACKAGE_SYSCONF_DIR "/ssh/ssh_known_hosts";
static const gchar *default_command = "cockpit-bridge";
static const gchar *ignore_hosts_data = "*";
static const gchar *hostkey_mismatch_data = "* invalid key";
@@ -167,3 +167,9 @@ cockpit_ssh_options_to_env (CockpitSshOptions *options,
return env;
}
const gchar *
cockpit_get_default_knownhosts (void)
{
return default_knownhosts;
}
@@ -48,8 +48,10 @@ typedef struct {
CockpitSshOptions * cockpit_ssh_options_from_env (gchar **env);
gchar ** cockpit_ssh_options_to_env (CockpitSshOptions *options,
gchar **env);
gchar ** cockpit_ssh_options_to_env (CockpitSshOptions *options,
gchar **env);
const gchar * cockpit_get_default_knownhosts (void);
G_END_DECLS
@@ -64,7 +64,7 @@ test_ssh_options (void)
options = cockpit_ssh_options_from_env (env);
g_assert_null (options->knownhosts_data);
g_assert_null (options->krb5_ccache_name);
g_assert_cmpstr (options->knownhosts_file, ==, PACKAGE_LOCALSTATE_DIR "/known_hosts");
g_assert_cmpstr (options->knownhosts_file, ==, PACKAGE_SYSCONF_DIR "/ssh/ssh_known_hosts");
g_assert_cmpstr (options->command, ==, "cockpit-bridge");
g_assert_false (options->allow_unknown_hosts);
g_assert_false (options->supports_hostkey_prompt);
@@ -26,13 +26,13 @@ import re
def break_hostkey(m, address):
line = "{0} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJqfgO2FPiix1n2sCJCXbaffwog1Vvi3zRdmcAxG//5T".format(address)
m.execute("echo '{0}'> /var/lib/cockpit/known_hosts".format(line))
m.execute("echo '{0}'> /etc/ssh/ssh_known_hosts".format(line))
def fix_hostkey(m, key=None):
if not key:
key = '';
m.execute("echo '{0}' > /var/lib/cockpit/known_hosts".format(key))
m.execute("echo '{0}' > /etc/ssh/ssh_known_hosts".format(key))
def break_bridge(m):
m.execute("ln -snf /bin/false /usr/local/bin/cockpit-bridge")
@@ -250,8 +250,29 @@ class TestMultiMachine(MachineCase):
b.wait_not_present("a[data-address='{}']".format(m2.address))
b.logout()
# Falls back to legacy /var/lib/cockpit/known_hosts
m.execute("mkdir -p /var/lib/cockpit; mv /etc/ssh/ssh_known_hosts /var/lib/cockpit/known_hosts")
b.open("{0}={1}".format(root, m2.address))
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "alt-password")
b.click('#login-button')
b.expect_load()
b.logout()
# /etc/ssh/ssh_known_hosts trumps legacy /var/lib/cockpit/known_hosts; break key in the latter
m.execute("cp /var/lib/cockpit/known_hosts /etc/ssh/ssh_known_hosts; sed -i 's/AAA/BBB/' /var/lib/cockpit/known_hosts")
b.open("{0}={1}".format(root, m2.address))
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "alt-password")
b.click('#login-button')
b.expect_load()
b.logout()
m.execute("rm /var/lib/cockpit/known_hosts")
# Bad host key
m.execute("echo '{} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDgPMmTosSQ4NxMtq+aL2NKLC+W4I9/jbD1e74cnOKTW' > /var/lib/cockpit/known_hosts".format(m2.address))
m.execute("echo '{} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDgPMmTosSQ4NxMtq+aL2NKLC+W4I9/jbD1e74cnOKTW' > /etc/ssh/ssh_known_hosts".format(m2.address))
b.open("{0}={1}".format(root, m2.address))
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
@@ -267,7 +288,7 @@ class TestMultiMachine(MachineCase):
b.wait_in_text("#login-error-message", "Hostkey does not match")
# Clear host key.
m.execute("echo '' > /var/lib/cockpit/known_hosts")
m.execute("echo '' > /etc/ssh/ssh_known_hosts")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "alt-password")
b.click('#login-button')
@@ -335,7 +356,7 @@ class TestMultiMachine(MachineCase):
b.logout()
# Check hostkey isn't saved
self.assertFalse(m.execute("cat /var/lib/cockpit/known_hosts").strip())
self.assertFalse(m.execute("cat /etc/ssh/ssh_known_hosts").strip())
# Direct to machine2, via form
b.open("{0}other".format(root))
Oops, something went wrong.

0 comments on commit a12f072

Please sign in to comment.