Provide mitigation options for SSH redirection vulnerability

The less intrusive options that give most immediate benefit for least
cost are enabled by default:
* Prevent server admins resetting SSH host key
* Block sync if multiple servers have the same SSH host key

An additional option for improved security is included to provide
hostname verification, either based on `hostname -f` or on an explicitly
defined '.hostnames' file.

Resolves: SSH redirection security issue reported by Tobias Josefowitz
of Opera Software
This commit is contained in:
Thomas Pike
2017-10-19 19:01:36 +02:00
parent ca1562c327
commit c184b031bd
8 changed files with 125 additions and 6 deletions

View File

@@ -7,6 +7,41 @@ logo = /logo-header-opera.png
; " < $gt;
footer = 'Developed by <a href="https://www.opera.com/">Opera Software</a>.'
[security]
; It is important that SKA is able to verify that it has connected to the
; server that it expected to connect to (otherwise it could be tricked into
; syncing the wrong keys to a server). The simplest way to accomplish this is
; through SSH host key verification. Setting either of the 2 options below to
; '0' can weaken the protection that SSH host key verification provides.
; Determine who can reset a server's SSH host key in SKA:
; 0: Allow server admins to reset the SSH host key for servers that they
; administer
; 1: Full SKA admin access is required to reset a server's host key
host_key_reset_restriction = 1
; Determine what happens if multiple servers have the same SSH host key:
; 0: Allow sync to proceed
; 1: Abort sync of affected servers and report an error
; It is not recommended to leave this set to '0' indefinitely
host_key_collision_protection = 1
; Hostname verification is a supplement to SSH host key verification for
; making sure that the sync process has connected to the server that it
; expected to.
; Determine how hostname verification is performed:
; 0: Do not perform hostname verification
; 1: Compare with the result of `hostname -f`
; 2: Compare with /var/local/keys-sync/.hostnames, fall back to `hostname -f`
; if the file does not exist
; 3: Compare with /var/local/keys-sync/.hostnames, abort sync if the file
; does not exist
; The last option provides the most solid verification, as a server will only
; be synced to if it has been explicitly allowed on the server itself.
hostname_verification = 0
[defaults]
; This setting will cause new servers to always have a managed account called
; "root" and for that account to be automatically added into the

View File

@@ -308,6 +308,7 @@ class Server extends Record {
if(is_null($this->id)) throw new BadMethodCallException('Server must be in directory before accounts can be added');
$account_name = $account->name;
if($account_name === '') throw new AccountNameInvalid('Account name cannot be empty');
if(substr($account_name, 0, 1) === '.') throw new AccountNameInvalid('Account name cannot begin with .');
$sync_status = is_null($account->sync_status) ? 'not synced yet' : $account->sync_status;
$this->database->begin_transaction();
$stmt = $this->database->prepare("INSERT INTO entity SET type = 'server account'");

View File

@@ -123,6 +123,7 @@ class ServerDirectory extends DBDirectory {
$where[] = "hostname REGEXP '".$this->database->escape_string($value)."'";
break;
case 'ip_address':
case 'rsa_key_fingerprint':
$where[] = "server.$field = '".$this->database->escape_string($value)."'";
break;
case 'admin':

View File

@@ -285,6 +285,16 @@ function sync_server($id, $only_username = null, $preview = false) {
return;
}
}
if(!isset($config['security']) || !isset($config['security']['host_key_collision_protection']) || $config['security']['host_key_collision_protection'] == 1) {
$matching_servers = $server_dir->list_servers(array(), array('rsa_key_fingerprint' => $server->rsa_key_fingerprint, 'key_management' => array('keys')));
if(count($matching_servers) > 1) {
echo date('c')." {$hostname}: Multiple hosts with same host key.\n";
$server->sync_report('sync failure', 'Multiple hosts with same host key');
$server->delete_all_sync_requests();
report_all_accounts_failed($keyfiles);
return;
}
}
try {
ssh2_auth_pubkey_file($connection, $attempt, 'config/keys-sync.pub', 'config/keys-sync');
echo date('c')." {$hostname}: Logged in as $attempt.\n";
@@ -327,6 +337,41 @@ function sync_server($id, $only_username = null, $preview = false) {
$account_errors = 0;
$cleanup_errors = 0;
if(isset($config['security']) && isset($config['security']['hostname_verification']) && $config['security']['hostname_verification'] >= 1) {
// Verify that we have mutual agreement with the server that we sync to it with this hostname
$allowed_hostnames = null;
if($config['security']['hostname_verification'] >= 2) {
// 2+ = Compare with /var/local/keys-sync/.hostnames
try {
$allowed_hostnames = array_map('trim', file("ssh2.sftp://$sftp/var/local/keys-sync/.hostnames"));
} catch(ErrorException $e) {
if($config['security']['hostname_verification'] >= 3) {
// 3+ = Abort if file does not exist
echo date('c')." {$hostname}: Hostnames file missing.\n";
$server->sync_report('sync failure', 'Hostnames file missing');
$server->delete_all_sync_requests();
report_all_accounts_failed($keyfiles);
return;
} else {
$allowed_hostnames = null;
}
}
}
if(is_null($allowed_hostnames)) {
$stream = ssh2_exec($connection, '/bin/hostname -f');
stream_set_blocking($stream, true);
$allowed_hostnames = array(trim(stream_get_contents($stream)));
fclose($stream);
}
if(!in_array($hostname, $allowed_hostnames)) {
echo date('c')." {$hostname}: Hostname check failed (allowed: ".implode(", ", $allowed_hostnames).").\n";
$server->sync_report('sync failure', 'Hostname check failed');
$server->delete_all_sync_requests();
report_all_accounts_failed($keyfiles);
return;
}
}
if($legacy && isset($keyfiles['root'])) {
// Legacy sync (only if using root account)
$keyfile = $keyfiles['root'];
@@ -408,7 +453,7 @@ function sync_server($id, $only_username = null, $preview = false) {
if(is_null($only_username)) {
// Clean up directory
foreach($sha1sums as $file => $sha1sum) {
if($file != '' && $file != 'keys-sync') {
if($file != '' && $file != 'keys-sync' && $file != '.hostnames') {
try {
if(ssh2_sftp_unlink($sftp, "$keydir/$file")) {
echo date('c')." {$hostname}: Removed unknown file: {$file}\n";

View File

@@ -16,6 +16,7 @@
##
$admin_mail = $this->get('admin_mail');
$baseurl = $this->get('baseurl');
$security_config = $this->get('security_config');
?>
<div class="panel-group" id="help">
<h1>Help</h1>
@@ -106,6 +107,10 @@ $baseurl = $this->get('baseurl');
<dd>SSH key authority was unable to establish an SSH connection to your server. This could indicate that the server is offline or otherwise unreachable, or that the SSH server is not running.</dd>
<dt>SSH host key verification failed</dt>
<dd>SSH key authority was able to open an SSH connection to your server, but the host key no longer matches the one that is on record for your server. If this is expected (eg. your server has been migrated to a new host), you can reset the host key on the "Settings" page of your server. Press the "Clear" button for the host key fingerprint and then "Save changes".</dd>
<?php if(!isset($security_config['host_key_collision_protection']) || $security_config['host_key_collision_protection'] == 1) { ?>
<dt>SSH host key collision</dt>
<dd>Your server has the same SSH host key as another server. This should be corrected by regenerating the SSH host keys on one or both of the affected servers.</dd>
<?php } ?>
<dt>SSH authentication failed</dt>
<dd>Although SSH key authority was able to connect to your server via SSH, it failed to log in. See the guides for setting up <a data-toggle="collapse" data-parent="#help" href="#sync_setup">full account syncing</a> or <a data-toggle="collapse" data-parent="#help" href="#legacy_sync_setup">legacy root account syncing</a>.</dd>
<dt>SFTP subsystem failed</dt>
@@ -122,6 +127,12 @@ $baseurl = $this->get('baseurl');
</dd>
<dt>Multiple hosts with same IP address</dt>
<dd>At least one other host managed by SSH Key Authority resolves to the same IP address as your server. SSH Key Authority will refuse to sync to either server until this is resolved.</dd>
<?php if(isset($security_config['hostname_verification']) && $security_config['hostname_verification'] >= 3) { ?>
<dt>Hostnames file missing</dt>
<dd>The <code>/var/local/keys-sync/.hostnames</code> file does not exist on the server. SSH Key Authority uses the contents of this file to verify that it is allowed to sync to your server.</dd>
<dt>Hostname check failed</dt>
<dd>The server name was not found in <code>/var/local/keys-sync/.hostnames</code> when SSH Key Authority tried to sync to your server.</dd>
<?php } ?>
</dl>
</div>
</div>
@@ -163,6 +174,9 @@ $baseurl = $this->get('baseurl');
<li>Create <code>/var/local/keys-sync/keys-sync</code> file (owned by keys-sync, permissions 0644) with the following SSH key in it:
<pre><?php out($this->get('keys-sync-pubkey'))?></pre>
</li>
<?php if(isset($security_config['hostname_verification']) && $security_config['hostname_verification'] >= 3) { ?>
<li>Create <code>/var/local/keys-sync/.hostnames</code> text file (owned by keys-sync, permissions 0644) with the server's hostname in it</li>
<?php } ?>
</ol>
<h5>Verify Stage 1 success</h5>
<p>Once Stage 1 has been deployed to your server, trigger a resync from SSH Key Authority. The server should no longer have the "Key directory does not exist" warning after syncing (the "Using legacy sync method" warning is expected at this point instead). You can check the contents of the <code>/var/local/keys-sync</code> directory to make sure that the access looks right.</p>

View File

@@ -42,11 +42,23 @@
</dd>
</dl>
</form>
<?php if($this->get('server')->ip_address && count($this->get('matching_servers')) > 1) { ?>
<?php if($this->get('server')->ip_address && count($this->get('matching_servers_by_ip')) > 1) { ?>
<div class="alert alert-danger">
<p>The hostname <?php out($this->get('server')->hostname)?> resolves to the same IP address as the following:</p>
<ul>
<?php foreach($this->get('matching_servers') as $matched_server) { ?>
<?php foreach($this->get('matching_servers_by_ip') as $matched_server) { ?>
<?php if($matched_server->hostname != $this->get('server')->hostname) { ?>
<li><a href="/servers/<?php out($matched_server->hostname, ESC_URL)?>" class="server alert-link"><?php out($matched_server->hostname)?></a></li>
<?php } ?>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if($this->get('server')->rsa_key_fingerprint && count($this->get('matching_servers_by_host_key')) > 1) { ?>
<div class="alert alert-danger">
<p>The server has the same SSH host key as the following:</p>
<ul>
<?php foreach($this->get('matching_servers_by_host_key') as $matched_server) { ?>
<?php if($matched_server->hostname != $this->get('server')->hostname) { ?>
<li><a href="/servers/<?php out($matched_server->hostname, ESC_URL)?>" class="server alert-link"><?php out($matched_server->hostname)?></a></li>
<?php } ?>
@@ -332,6 +344,11 @@
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="edit_server" value="1" class="btn btn-primary">Change settings</button>
</div>
</div>
<?php } else { ?>
<dl>
<dt>Key management</dt>
@@ -374,6 +391,7 @@
</dd>
<?php } ?>
</dl>
<?php if($this->get('server_admin_can_reset_host_key')) { ?>
<div class="form-group">
<label for="rsa_key_fingerprint" class="col-sm-2 control-label">Host key fingerprint</label>
<div class="col-sm-4">
@@ -383,12 +401,13 @@
<button type="button" class="btn btn-default" data-clear="rsa_key_fingerprint">Clear</button>
</div>
</div>
<?php } ?>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="edit_server" value="1" class="btn btn-primary">Change settings</button>
</div>
</div>
<?php } ?>
<?php } ?>
</form>
</div>
<div class="tab-pane fade" id="log">

View File

@@ -23,6 +23,7 @@ if(file_exists('config/keys-sync.pub')) {
}
$content->set('admin_mail', $config['email']['admin_address']);
$content->set('baseurl', $config['web']['baseurl']);
$content->set('security_config', isset($config['security']) ? $config['security'] : array());
$page = new PageSection('base');
$page->set('title', 'Help');

View File

@@ -35,6 +35,7 @@ $all_groups = $group_dir->list_groups();
$all_servers = $active_user->list_admined_servers();
$all_accounts = $server->list_accounts();
$ldap_access_options = $server->list_ldap_access_options();
$server_admin_can_reset_host_key = (isset($config['security']) && isset($config['security']['host_key_reset_restriction']) && $config['security']['host_key_reset_restriction'] == 0);
if(isset($_POST['sync']) && ($server_admin || $active_user->admin)) {
$server->sync_access();
@@ -123,7 +124,7 @@ if(isset($_POST['sync']) && ($server_admin || $active_user->admin)) {
$content->set('exception', $e);
}
}
} elseif(isset($_POST['edit_server']) && $server_admin) {
} elseif(isset($_POST['edit_server']) && $server_admin && $server_admin_can_reset_host_key) {
if($_POST['rsa_key_fingerprint'] == '') $server->rsa_key_fingerprint = null;
$server->update();
redirect('#settings');
@@ -291,7 +292,8 @@ if(isset($_POST['sync']) && ($server_admin || $active_user->admin)) {
$content->set('all_users', $all_users);
$content->set('last_sync', $server->get_last_sync_event());
$content->set('sync_requests', $server->list_sync_requests());
$content->set('matching_servers', $server_dir->list_servers(array(), array('ip_address' => $server->ip_address, 'key_management' => array('keys'))));
$content->set('matching_servers_by_ip', $server_dir->list_servers(array(), array('ip_address' => $server->ip_address, 'key_management' => array('keys'))));
$content->set('matching_servers_by_host_key', $server_dir->list_servers(array(), array('rsa_key_fingerprint' => $server->rsa_key_fingerprint, 'key_management' => array('keys'))));
$content->set('all_groups', $all_groups);
$content->set('all_servers', $all_servers);
$content->set('all_accounts', $all_accounts);
@@ -300,6 +302,7 @@ if(isset($_POST['sync']) && ($server_admin || $active_user->admin)) {
$content->set('email_config', $config['email']);
$content->set('inventory_config', $config['inventory']);
$content->set('default_accounts', isset($config['defaults']['account_groups']) ? $config['defaults']['account_groups'] : array());
$content->set('server_admin_can_reset_host_key', $server_admin_can_reset_host_key);
switch($server->sync_status) {
case 'sync success': $content->set('sync_class', 'success'); break;
case 'sync warning': $content->set('sync_class', 'warning'); break;