This is an automated email from the git hooks/post-receive script. x2go pushed a commit to branch master in repository x2gobroker. commit 0b51f522ebd1d8747a4f401f6cf274bfcb9ddccd Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de> Date: Sun Apr 21 15:21:23 2019 +0200 Make remote agent's SSH HostKey policy configurable globally, backend-wise and per session profile. Fallback to RejectPolicy by default. (See Debian bug #922314). --- etc/x2gobroker.conf | 31 ++++++++++++++++++++++++ x2gobroker/agent.py | 9 ++++++- x2gobroker/brokers/base_broker.py | 42 ++++++++++++++++++++++++++++++++- x2gobroker/defaults.py | 1 + x2gobroker/tests/test_broker_agent.py | 44 +++++++++++++++++++++++++++++++---- 5 files changed, 121 insertions(+), 6 deletions(-) diff --git a/etc/x2gobroker.conf b/etc/x2gobroker.conf index 44e86a4..d88faa6 100644 --- a/etc/x2gobroker.conf +++ b/etc/x2gobroker.conf @@ -242,6 +242,37 @@ # below value is the default. #default-agent-query-mode=NONE +# X2Go Broker's Host Key Policy (if agent query mode is 'SSH') +# +# If X2Go Broker's agent query mode is SSH, the system needs to handle +# X2Go Server side's SSH host keys in a secure and verifyable manner. +# +# The agent-hostkey-policy is the default policy to be used and can be +# either AutoAddPolicy, WarningPolicy, or RejectPolicy. The policy names +# match the corresponding class names in Paramiko SSH. +# +# IMPORTANT: As RejectPolicy is the only safe default, please be aware that +# on fresh X2Go Broker setups, SSH agent queries will always fail, until a +# properly maintained ~x2gobroker/.ssh/known_hosts file is in place. +# +# There are two simple ways to create this known_hosts file: +# +# (a) su - x2gobroker -c "ssh <x2goserver>" +# +# On the command line, you get prompted to confirm the remote +# X2Go server's Follow OpenSSH interactive dialog for accepting +# the remote host's host key. +# +# (b) x2gobroker-testagent --add-to-known-hosts --host <x2goserver> +# +# This command will populate the known_hosts file with the remote +# X2Go server's hostkey while trying to hail its X2Go Broker Agent +# The host key's fingerprint will be shown on stdout, but there will +# be no interactive confirmation. If unsure about this, use approach +# (a) given above. +# +#default-agent-hostkey-policy=RejectPolicy + # Probe SSH port of X2Go Servers (availability check) # # Just before offering an X2Go Server address to a broker client, the diff --git a/x2gobroker/agent.py b/x2gobroker/agent.py index b267277..3530323 100644 --- a/x2gobroker/agent.py +++ b/x2gobroker/agent.py @@ -195,8 +195,15 @@ def _call_remote_broker_agent(username, task, cmdline_args=[], remote_agent=None if remote_agent is None: logger_error.error('With the SSH agent-query-mode a remote agent host (hostname, hostaddr, port) has to be specified!') - elif 'host_key_policy' not in remote_agent: + elif 'host_key_policy' not in remote_agent or remote_agent['host_key_policy'] == 'WarningPolicy': remote_agent['host_key_policy'] = paramiko.WarningPolicy() + elif remote_agent['host_key_policy'] == 'RejectPolicy': + remote_agent['host_key_policy'] = paramiko.RejectPolicy() + elif remote_agent['host_key_policy'] == 'AutoAddPolicy': + remote_agent['host_key_policy'] = paramiko.AutoAddPolicy() + else: + logger_error.error('Invalid SSH HostKey Policy: "{policy}", falling back to "RejectPolicy"!'.format(policy=remote_agent['host_key_policy'])) + remote_agent['host_key_policy'] = paramiko.RejectPolicy() remote_hostaddr = None remote_hostname = None diff --git a/x2gobroker/brokers/base_broker.py b/x2gobroker/brokers/base_broker.py index 5b4303e..3de1426 100644 --- a/x2gobroker/brokers/base_broker.py +++ b/x2gobroker/brokers/base_broker.py @@ -562,6 +562,44 @@ class X2GoBroker(object): else: return _mode + def get_agent_hostkey_policy(self, profile_id): + """\ + Get the agent hostkey policy (either of 'RejectPolicy', + 'AutoAddPolicy' or 'WarningPolicy') that is configured for this + X2Go Session Broker instance. + + The returned policy names match the MissingHostkeyPolicy class + names as found in Python Paramiko. + + :returns: agent hostkey policy + :rtype: ``str`` + + """ + _default_agent_hostkey_policy = "RejectPolicy" + _backend_agent_hostkey_policy = "" + _agent_hostkey_policy = "" + + _profile = self.get_profile_broker(profile_id) + if _profile and 'broker-agent-hostkey-policy' in _profile and _profile['broker-agent-hostkey-policy']: + _agent_hostkey_policy = _profile['broker-agent-hostkey-policy'] + logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found broker-agent-hostkey-policy in session profile with ID {id}: {value}. This one has precendence over the default and the backend value.'.format(id=profile_id, value=_agent_hostkey_policy)) + + elif self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy') and self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy'): + _backend_agent_hostkey_policy = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy') + logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found agent-hostkey-policy in backend config section »{backend}«: {value}. This one has precendence over the default value.'.format(backend=self.backend_name, value=_agent_hostkey_policy)) + + elif self.config.has_value('global', 'default-agent-hostkey-policy') and self.config.get_value('global', 'default-agent-hostkey-policy'): + _default_agent_hostkey_policy = self.config.get_value('global', 'default-agent-hostkey-policy') + logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found default-agent-hostkey-policy in global config section: {value}'.format(value=_default_agent_hostkey_policy)) + + _policy = _agent_hostkey_policy or _backend_agent_hostkey_policy or _default_agent_hostkey_policy + + if _policy not in ('AutoAddPolicy', 'RejectPolicy', 'WarningPolicy'): + logger_broker.warn('base_broker.X2GoBroker.get_agent_hostkey_policy(): given hostkey policy ({policy}) is invalid/unknown, falling back to default hostkey policy ({default_policy}).'.format(policy=_policy, default_policy=_default_agent_hostkey_policy)) + _policy = _default_agent_hostkey_policy + + return _policy + def get_session_autologin(self, profile_id): """\ Detect if the given profile is configured to try automatic session @@ -1096,7 +1134,9 @@ class X2GoBroker(object): remote_agent = { 'hostname': remote_agent_hostname, 'hostaddr': remote_agent_hostaddr, - 'port': remote_agent_port, } + 'port': remote_agent_port, + 'host_key_policy': self.get_agent_hostkey_policy(profile_id), + } try: if x2gobroker.agent.ping(remote_agent=remote_agent): diff --git a/x2gobroker/defaults.py b/x2gobroker/defaults.py index e10ccb0..8989688 100644 --- a/x2gobroker/defaults.py +++ b/x2gobroker/defaults.py @@ -243,6 +243,7 @@ X2GOBROKER_CONFIG_DEFAULTS = { 'default-authorized-keys': '%h/.x2go/authorized_keys', 'default-sshproxy-authorized-keys': '%h/.x2go/authorized_keys', 'default-agent-query-mode': 'NONE', + 'default-agent-hostkey-policy': 'RejectPolicy', 'default-portscan-x2goservers': True, 'default-use-load-checker': False, 'load-checker-intervals': 300, diff --git a/x2gobroker/tests/test_broker_agent.py b/x2gobroker/tests/test_broker_agent.py index 8d9e489..a7e4ee8 100644 --- a/x2gobroker/tests/test_broker_agent.py +++ b/x2gobroker/tests/test_broker_agent.py @@ -110,23 +110,39 @@ host = host1.mydomain, host2.yourdomain name = testprofile5 host = host1.mydomain (10.0.2.4), host2.mydomain (10.0.2.5) broker-agent-query-mode = SSH +broker-agent-hostkey-policy = WarningPolicy [testprofile6] name = testprofile6 host = host1.mydomain (10.0.2.4), host2.mydomain (10.0.2.5) sshport = 23467 broker-agent-query-mode = SSH +broker-agent-hostkey-policy = WarningPolicy [testprofile7] name = testprofile7 host = docker-vm-1 (docker-server:22001), docker-vm-2 (docker-server:22002) broker-agent-query-mode = SSH +broker-agent-hostkey-policy = WarningPolicy [testprofile8] name = testprofile8 host = docker-vm-0 (docker-server), docker-vm-1 (docker-server:22001), docker-vm-2 (docker-server:22002) sshport = 22000 broker-agent-query-mode = SSH +broker-agent-hostkey-policy = WarningPolicy + +[testprofile9] +name = testprofile9 +host = host1.mydomain (10.0.2.4) +broker-agent-query-mode = SSH +broker-agent-hostkey-policy = AutoAddPolicy + +[testprofile10] +name = testprofile10 +host = host1.mydomain (10.0.2.4) +broker-agent-query-mode = SSH +broker-agent-hostkey-policy = SomeUnkownPolicy """ tf = tempfile.NamedTemporaryFile(mode='w') @@ -207,7 +223,7 @@ broker-agent-query-mode = SSH i = 0 while i < 10: _remoteagent5 = inifile_backend.get_remote_agent('testprofile5') - self.assertTrue( _remoteagent5 == {'hostname': 'host1.mydomain', 'hostaddr': '10.0.2.4', 'port': 22, 'load_factors': {}, } or _remoteagent5 == {'hostname': 'host2.mydomain', 'hostaddr': '10.0.2.5', 'port': 22, 'load_factors': {}, } ) + self.assertTrue( _remoteagent5 == {'hostname': 'host1.mydomain', 'hostaddr': '10.0.2.4', 'port': 22, 'load_factors': {}, 'host_key_policy': 'WarningPolicy'} or _remoteagent5 == {'hostname': 'host2.mydomain', 'hostaddr': '10.0.2.5', 'port': 22, 'load_factors': {}, 'host_key_policy': 'WarningPolicy', } ) _session5 = inifile_backend.select_session('testprofile5', 'foo5N') self.assertTrue( _session5 == {'port': 22, 'server': '10.0.2.4', } or _session5 == {'port': 22, 'server': '10.0.2.5', } ) i += 1 @@ -221,7 +237,7 @@ broker-agent-query-mode = SSH self.assertTrue( _profile6['host'][0] in ('host1.mydomain', 'host2.mydomain') ) self.assertTrue( 'status' not in _profile6 ) _remoteagent6 = inifile_backend.get_remote_agent('testprofile6') - self.assertTrue( _remoteagent6 == {'hostname': 'host1.mydomain', 'hostaddr': '10.0.2.4', 'port': 23467, 'load_factors': {}, } or _remoteagent6 == {'hostname': 'host2.mydomain', 'hostaddr': '10.0.2.5', 'port': 23467, 'load_factors': {}, } ) + self.assertTrue( _remoteagent6 == {'hostname': 'host1.mydomain', 'hostaddr': '10.0.2.4', 'port': 23467, 'load_factors': {}, 'host_key_policy': 'WarningPolicy', } or _remoteagent6 == {'hostname': 'host2.mydomain', 'hostaddr': '10.0.2.5', 'port': 23467, 'load_factors': {}, 'host_key_policy': 'WarningPolicy', } ) _session6 = inifile_backend.select_session('testprofile6', 'foo6N') self.assertTrue( _session6 == {'port': 23467, 'server': '10.0.2.4', } or _session6 == {'port': 23467, 'server': '10.0.2.5', } ) @@ -233,7 +249,7 @@ broker-agent-query-mode = SSH i = 0 while i < 10: _remoteagent7 = inifile_backend.get_remote_agent('testprofile7') - self.assertTrue( _remoteagent7 == {'hostname': 'docker-vm-1', 'hostaddr': 'docker-server', 'port': 22001, 'load_factors': {}, } or _remoteagent7 == {'hostname': 'docker-vm-2', 'hostaddr': 'docker-server', 'port': 22002, 'load_factors': {}, } ) + self.assertTrue( _remoteagent7 == {'hostname': 'docker-vm-1', 'hostaddr': 'docker-server', 'port': 22001, 'load_factors': {}, 'host_key_policy': 'WarningPolicy', } or _remoteagent7 == {'hostname': 'docker-vm-2', 'hostaddr': 'docker-server', 'port': 22002, 'load_factors': {}, 'host_key_policy': 'WarningPolicy', } ) _session7 = inifile_backend.select_session('testprofile7', 'foo7N') self.assertTrue( _session7 == {'port': 22001, 'server': 'docker-server', } or _session7 == {'port': 22001, 'server': 'docker-server', } ) i += 1 @@ -246,11 +262,31 @@ broker-agent-query-mode = SSH i = 0 while i < 10: _remoteagent8 = inifile_backend.get_remote_agent('testprofile8') - self.assertTrue( _remoteagent8 == {'hostname': 'docker-vm-0', 'hostaddr': 'docker-server', 'port': 22000, 'load_factors': {}, } or _remoteagent8 == {'hostname': 'docker-vm-1', 'hostaddr': 'docker-server', 'port': 22001, 'load_factors': {}, } or _remoteagent8 == {'hostname': 'docker-vm-2', 'hostaddr': 'docker-server', 'port': 22002, 'load_factors': {}, } ) + self.assertTrue( _remoteagent8 == {'hostname': 'docker-vm-0', 'hostaddr': 'docker-server', 'port': 22000, 'load_factors': {}, 'host_key_policy': 'WarningPolicy', } or _remoteagent8 == {'hostname': 'docker-vm-1', 'hostaddr': 'docker-server', 'port': 22001, 'load_factors': {}, 'host_key_policy': 'WarningPolicy', } or _remoteagent8 == {'hostname': 'docker-vm-2', 'hostaddr': 'docker-server', 'port': 22002, 'load_factors': {}, 'host_key_policy': 'WarningPolicy', } ) _session8 = inifile_backend.select_session('testprofile8', 'foo8N') self.assertTrue( _session8 == {'port': 22000, 'server': 'docker-server', } or _session8 == {'port': 22001, 'server': 'docker-server', } or _session8 == {'port': 22001, 'server': 'docker-server', } ) i += 1 + # test "testprofile9", test if hostkey policy is propagated from session profile config to remote agent settings + + _list9 = inifile_backend.list_profiles(username='foo9N') + _profile9 = _list9['testprofile9'] + _profile9['host'].sort() + self.assertTrue( _profile9['host'][0] in ('host1.mydomain') ) + self.assertTrue( 'status' not in _profile9 ) + _remoteagent9 = inifile_backend.get_remote_agent('testprofile9') + self.assertTrue( _remoteagent9 == {'hostname': 'host1.mydomain', 'hostaddr': '10.0.2.4', 'port': 22, 'load_factors': {}, 'host_key_policy': 'AutoAddPolicy'}) + + # test "testprofile10", test if an invalid hostkey policy is propagated from session profile config to remote agent settings and ignored with RejectPolicy as fallback + + _list10 = inifile_backend.list_profiles(username='foo10N') + _profile10 = _list10['testprofile10'] + _profile10['host'].sort() + self.assertTrue( _profile10['host'][0] in ('host1.mydomain') ) + self.assertTrue( 'status' not in _profile10 ) + _remoteagent10 = inifile_backend.get_remote_agent('testprofile10') + self.assertTrue( _remoteagent10 == {'hostname': 'host1.mydomain', 'hostaddr': '10.0.2.4', 'port': 22, 'load_factors': {}, 'host_key_policy': 'RejectPolicy'}) + x2gobroker.agent._call_local_broker_agent = _save_local_broker_agent_call x2gobroker.agent._call_remote_broker_agent = _save_remote_broker_agent_call x2gobroker.utils.portscan = _save_portscan -- Alioth's /home/x2go-admin/maintenancescripts/git/hooks/post-receive-email on /srv/git/code.x2go.org/x2gobroker.git