diff --git a/data/templates/ssh/sshd_config.j2 b/data/templates/ssh/sshd_config.j2 index 650fd25e6..2cf0494c4 100644 --- a/data/templates/ssh/sshd_config.j2 +++ b/data/templates/ssh/sshd_config.j2 @@ -1,107 +1,112 @@ ### Autogenerated by service_ssh.py ### # https://linux.die.net/man/5/sshd_config # # Non-configurable defaults # Protocol 2 HostKey /etc/ssh/ssh_host_rsa_key HostKey /etc/ssh/ssh_host_dsa_key HostKey /etc/ssh/ssh_host_ecdsa_key HostKey /etc/ssh/ssh_host_ed25519_key SyslogFacility AUTH LoginGraceTime 120 StrictModes yes PubkeyAuthentication yes IgnoreRhosts yes HostbasedAuthentication no PermitEmptyPasswords no X11Forwarding yes X11DisplayOffset 10 PrintMotd no PrintLastLog yes TCPKeepAlive yes Banner /etc/issue.net Subsystem sftp /usr/lib/openssh/sftp-server UsePAM yes PermitRootLogin no PidFile /run/sshd/sshd.pid AddressFamily any DebianBanner no KbdInteractiveAuthentication no # # User configurable section # # Look up remote host name and check that the resolved host name for the remote IP # address maps back to the very same IP address. UseDNS {{ "no" if disable_host_validation is vyos_defined else "yes" }} # Specifies the port number that sshd(8) listens on {% for value in port %} Port {{ value }} {% endfor %} # Gives the verbosity level that is used when logging messages from sshd LogLevel {{ loglevel | upper }} # Specifies whether password authentication is allowed PasswordAuthentication {{ "no" if disable_password_authentication is vyos_defined else "yes" }} {% if listen_address is vyos_defined %} # Specifies the local addresses sshd should listen on {% for address in listen_address %} ListenAddress {{ address }} {% endfor %} {% endif %} {% if ciphers is vyos_defined %} # Specifies the ciphers allowed for protocol version 2 Ciphers {{ ciphers | join(',') }} {% endif %} {% if hostkey_algorithm is vyos_defined %} # Specifies the available Host Key signature algorithms HostKeyAlgorithms {{ hostkey_algorithm | join(',') }} {% endif %} +{% if pubkey_accepted_algorithm is vyos_defined %} +# Specifies the available PubKey signature algorithms +PubkeyAcceptedAlgorithms {{ pubkey_accepted_algorithm | join(',') }} +{% endif %} + {% if mac is vyos_defined %} # Specifies the available MAC (message authentication code) algorithms MACs {{ mac | join(',') }} {% endif %} {% if key_exchange is vyos_defined %} # Specifies the available Key Exchange algorithms KexAlgorithms {{ key_exchange | join(',') }} {% endif %} {% if access_control is vyos_defined %} {% if access_control.allow.user is vyos_defined %} # If specified, login is allowed only for user names that match AllowUsers {{ access_control.allow.user | join(' ') }} {% endif %} {% if access_control.allow.group is vyos_defined %} # If specified, login is allowed only for users whose primary group or supplementary group list matches AllowGroups {{ access_control.allow.group | join(' ') }} {% endif %} {% if access_control.deny.user is vyos_defined %} # Login is disallowed for user names that match DenyUsers {{ access_control.deny.user | join(' ') }} {% endif %} {% if access_control.deny.group is vyos_defined %} # Login is disallowed for users whose primary group or supplementary group list matches DenyGroups {{ access_control.deny.group | join(' ') }} {% endif %} {% endif %} {% if client_keepalive_interval is vyos_defined %} # Sets a timeout interval in seconds after which if no data has been received from the client, # sshd(8) will send a message through the encrypted channel to request a response from the client ClientAliveInterval {{ client_keepalive_interval }} {% endif %} {% if rekey.data is vyos_defined %} RekeyLimit {{ rekey.data }}M {{ rekey.time + 'M' if rekey.time is vyos_defined }} {% endif %} diff --git a/interface-definitions/service_ssh.xml.in b/interface-definitions/service_ssh.xml.in index d9eee1ab8..221e451d1 100644 --- a/interface-definitions/service_ssh.xml.in +++ b/interface-definitions/service_ssh.xml.in @@ -1,270 +1,283 @@ System services Secure Shell (SSH) 1000 SSH user/group access controls Allow user/group SSH access #include #include Deny user/group SSH access #include #include Allowed ciphers 3des-cbc aes128-cbc aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com (3des-cbc|aes128-cbc|aes192-cbc|aes256-cbc|rijndael-cbc@lysator.liu.se|aes128-ctr|aes192-ctr|aes256-ctr|aes128-gcm@openssh.com|aes256-gcm@openssh.com|chacha20-poly1305@openssh.com) Disable IP Address to Hostname lookup Disable password-based authentication Allow dynamic protection Block source IP in seconds. Subsequent blocks increase by a factor of 1.5 u32:1-65535 Time interval in seconds for blocking 120 Remember source IP in seconds before reset their score u32:1-65535 Time interval in seconds 1800 Block source IP when their cumulative attack score exceeds threshold u32:1-65535 Threshold score 30 Always allow inbound connections from these systems ipv4 Address to match against ipv4net IPv4 address and prefix length ipv6 IPv6 address to match against ipv6net IPv6 address and prefix length Allowed host key signature algorithms ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512 ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 sk-ecdsa-sha2-nistp256@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-rsa-cert-v01@openssh.com rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512-cert-v01@openssh.com ssh-dss-cert-v01@openssh.com ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ssh-rsa|rsa-sha2-256|rsa-sha2-512|ssh-dss|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ecdsa-sha2-nistp256@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512-cert-v01@openssh.com|ssh-dss-cert-v01@openssh.com|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com) + + + Allowed pubkey signature algorithms + + + ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384 ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521 ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-dss ssh-dss-cert-v01@openssh.com ssh-rsa ssh-rsa-cert-v01@openssh.com rsa-sha2-256 rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512 rsa-sha2-512-cert-v01@openssh.com + + + + (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ecdsa-sha2-nistp256|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-dss|ssh-dss-cert-v01@openssh.com|ssh-rsa|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512|rsa-sha2-512-cert-v01@openssh.com) + + + Allowed key exchange (KEX) algorithms diffie-hellman-group1-sha1 diffie-hellman-group14-sha1 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group18-sha512 diffie-hellman-group-exchange-sha1 diffie-hellman-group-exchange-sha256 ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 curve25519-sha256 curve25519-sha256@libssh.org (diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group14-sha256|diffie-hellman-group16-sha512|diffie-hellman-group18-sha512|diffie-hellman-group-exchange-sha1|diffie-hellman-group-exchange-sha256|ecdh-sha2-nistp256|ecdh-sha2-nistp384|ecdh-sha2-nistp521|curve25519-sha256|curve25519-sha256@libssh.org) #include Log level quiet fatal error info verbose quiet stay silent fatal log fatals only error log errors and fatals only info default log level verbose enable logging of failed login attempts (quiet|fatal|error|info|verbose) info Allowed message authentication code (MAC) algorithms hmac-sha1 hmac-sha1-96 hmac-sha2-256 hmac-sha2-512 hmac-md5 hmac-md5-96 umac-64@openssh.com umac-128@openssh.com hmac-sha1-etm@openssh.com hmac-sha1-96-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-md5-etm@openssh.com hmac-md5-96-etm@openssh.com umac-64-etm@openssh.com umac-128-etm@openssh.com (hmac-sha1|hmac-sha1-96|hmac-sha2-256|hmac-sha2-512|hmac-md5|hmac-md5-96|umac-64@openssh.com|umac-128@openssh.com|hmac-sha1-etm@openssh.com|hmac-sha1-96-etm@openssh.com|hmac-sha2-256-etm@openssh.com|hmac-sha2-512-etm@openssh.com|hmac-md5-etm@openssh.com|hmac-md5-96-etm@openssh.com|umac-64-etm@openssh.com|umac-128-etm@openssh.com) Port for SSH service u32:1-65535 Numeric IP port 22 SSH session rekey limit Threshold data in megabytes u32:1-65535 Megabytes Threshold time in minutes u32:1-65535 Minutes Enable transmission of keepalives from server to client u32:1-65535 Time interval in seconds for keepalive message #include diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index b09990c92..d8e325eee 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -1,309 +1,325 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import paramiko import re import unittest from pwd import getpwall from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_running from vyos.utils.process import process_named_running from vyos.utils.file import read_file from vyos.xml_ref import default_value PROCESS_NAME = 'sshd' SSHD_CONF = '/run/sshd/sshd_config' base_path = ['service', 'ssh'] key_rsa = '/etc/ssh/ssh_host_rsa_key' key_dsa = '/etc/ssh/ssh_host_dsa_key' key_ed25519 = '/etc/ssh/ssh_host_ed25519_key' def get_config_value(key): tmp = read_file(SSHD_CONF) tmp = re.findall(f'\n?{key}\s+(.*)', tmp) return tmp class TestServiceSSH(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestServiceSSH, cls).setUpClass() # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) cls.cli_delete(cls, ['vrf']) def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) # delete testing SSH config self.cli_delete(base_path) self.cli_delete(['vrf']) self.cli_commit() self.assertTrue(os.path.isfile(key_rsa)) self.assertTrue(os.path.isfile(key_dsa)) self.assertTrue(os.path.isfile(key_ed25519)) # Established SSH connections remains running after service is stopped. # We can not use process_named_running here - we rather need to check # that the systemd service is no longer running self.assertFalse(is_systemd_service_running(PROCESS_NAME)) def test_ssh_default(self): # Check if SSH service runs with default settings - used for checking # behavior of in XML definition self.cli_set(base_path) # commit changes self.cli_commit() # Check configured port agains CLI default value port = get_config_value('Port') cli_default = default_value(base_path + ['port']) self.assertEqual(port, cli_default) def test_ssh_single_listen_address(self): # Check if SSH service can be configured and runs self.cli_set(base_path + ['port', '1234']) self.cli_set(base_path + ['disable-host-validation']) self.cli_set(base_path + ['disable-password-authentication']) self.cli_set(base_path + ['loglevel', 'verbose']) self.cli_set(base_path + ['client-keepalive-interval', '100']) self.cli_set(base_path + ['listen-address', '127.0.0.1']) # commit changes self.cli_commit() # Check configured port port = get_config_value('Port')[0] self.assertTrue("1234" in port) # Check DNS usage dns = get_config_value('UseDNS')[0] self.assertTrue("no" in dns) # Check PasswordAuthentication pwd = get_config_value('PasswordAuthentication')[0] self.assertTrue("no" in pwd) # Check loglevel loglevel = get_config_value('LogLevel')[0] self.assertTrue("VERBOSE" in loglevel) # Check listen address address = get_config_value('ListenAddress')[0] self.assertTrue("127.0.0.1" in address) # Check keepalive keepalive = get_config_value('ClientAliveInterval')[0] self.assertTrue("100" in keepalive) def test_ssh_multiple_listen_addresses(self): # Check if SSH service can be configured and runs with multiple # listen ports and listen-addresses ports = ['22', '2222', '2223', '2224'] for port in ports: self.cli_set(base_path + ['port', port]) addresses = ['127.0.0.1', '::1'] for address in addresses: self.cli_set(base_path + ['listen-address', address]) # commit changes self.cli_commit() # Check configured port tmp = get_config_value('Port') for port in ports: self.assertIn(port, tmp) # Check listen address tmp = get_config_value('ListenAddress') for address in addresses: self.assertIn(address, tmp) def test_ssh_vrf_single(self): vrf = 'mgmt' # Check if SSH service can be bound to given VRF self.cli_set(base_path + ['vrf', vrf]) # VRF does yet not exist - an error must be thrown with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(['vrf', 'name', vrf, 'table', '1338']) # commit changes self.cli_commit() # Check for process in VRF tmp = cmd(f'ip vrf pids {vrf}') self.assertIn(PROCESS_NAME, tmp) def test_ssh_vrf_multi(self): # Check if SSH service can be bound to multiple VRFs vrfs = ['red', 'blue', 'green'] for vrf in vrfs: self.cli_set(base_path + ['vrf', vrf]) # VRF does yet not exist - an error must be thrown with self.assertRaises(ConfigSessionError): self.cli_commit() table = 12345 for vrf in vrfs: self.cli_set(['vrf', 'name', vrf, 'table', str(table)]) table += 1 # commit changes self.cli_commit() # Check for process in VRF for vrf in vrfs: tmp = cmd(f'ip vrf pids {vrf}') self.assertIn(PROCESS_NAME, tmp) def test_ssh_login(self): # Perform SSH login and command execution with a predefined user. The # result (output of uname -a) must match the output if the command is # run natively. # # We also try to login as an invalid user - this is not allowed to work. test_user = 'ssh_test' test_pass = 'v2i57DZs8idUwMN3VC92' test_command = 'uname -a' self.cli_set(base_path) self.cli_set(['system', 'login', 'user', test_user, 'authentication', 'plaintext-password', test_pass]) # commit changes self.cli_commit() # Login with proper credentials output, error = self.ssh_send_cmd(test_command, test_user, test_pass) # verify login self.assertFalse(error) self.assertEqual(output, cmd(test_command)) # Login with invalid credentials with self.assertRaises(paramiko.ssh_exception.AuthenticationException): output, error = self.ssh_send_cmd(test_command, 'invalid_user', 'invalid_password') self.cli_delete(['system', 'login', 'user', test_user]) self.cli_commit() # After deletion the test user is not allowed to remain in /etc/passwd usernames = [x[0] for x in getpwall()] self.assertNotIn(test_user, usernames) def test_ssh_dynamic_protection(self): # check sshguard service SSHGUARD_CONFIG = '/etc/sshguard/sshguard.conf' SSHGUARD_WHITELIST = '/etc/sshguard/whitelist' SSHGUARD_PROCESS = 'sshguard' block_time = '123' detect_time = '1804' port = '22' threshold = '10' allow_list = ['192.0.2.0/24', '2001:db8::/48'] self.cli_set(base_path + ['dynamic-protection', 'block-time', block_time]) self.cli_set(base_path + ['dynamic-protection', 'detect-time', detect_time]) self.cli_set(base_path + ['dynamic-protection', 'threshold', threshold]) for allow in allow_list: self.cli_set(base_path + ['dynamic-protection', 'allow-from', allow]) # commit changes self.cli_commit() # Check configured port tmp = get_config_value('Port') self.assertIn(port, tmp) # Check sshgurad service self.assertTrue(process_named_running(SSHGUARD_PROCESS)) sshguard_lines = [ f'THRESHOLD={threshold}', f'BLOCK_TIME={block_time}', f'DETECTION_TIME={detect_time}' ] tmp_sshguard_conf = read_file(SSHGUARD_CONFIG) for line in sshguard_lines: self.assertIn(line, tmp_sshguard_conf) tmp_whitelist_conf = read_file(SSHGUARD_WHITELIST) for allow in allow_list: self.assertIn(allow, tmp_whitelist_conf) # Delete service ssh dynamic-protection # but not service ssh itself self.cli_delete(base_path + ['dynamic-protection']) self.cli_commit() self.assertFalse(process_named_running(SSHGUARD_PROCESS)) # Network Device Collaborative Protection Profile def test_ssh_ndcpp(self): ciphers = ['aes128-cbc', 'aes128-ctr', 'aes256-cbc', 'aes256-ctr'] host_key_algs = ['sk-ssh-ed25519@openssh.com', 'ssh-rsa', 'ssh-ed25519'] kexes = ['diffie-hellman-group14-sha1', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521'] macs = ['hmac-sha1', 'hmac-sha2-256', 'hmac-sha2-512'] rekey_time = '60' rekey_data = '1024' for cipher in ciphers: self.cli_set(base_path + ['ciphers', cipher]) for host_key in host_key_algs: self.cli_set(base_path + ['hostkey-algorithm', host_key]) for kex in kexes: self.cli_set(base_path + ['key-exchange', kex]) for mac in macs: self.cli_set(base_path + ['mac', mac]) # Optional rekey parameters self.cli_set(base_path + ['rekey', 'data', rekey_data]) self.cli_set(base_path + ['rekey', 'time', rekey_time]) # commit changes self.cli_commit() ssh_lines = ['Ciphers aes128-cbc,aes128-ctr,aes256-cbc,aes256-ctr', 'HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-rsa,ssh-ed25519', 'MACs hmac-sha1,hmac-sha2-256,hmac-sha2-512', 'KexAlgorithms diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521', 'RekeyLimit 1024M 60M' ] tmp_sshd_conf = read_file(SSHD_CONF) for line in ssh_lines: self.assertIn(line, tmp_sshd_conf) + def test_ssh_pubkey_accepted_algorithm(self): + algs = ['ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', 'ssh-dss', 'ssh-rsa', 'rsa-sha2-256', + 'rsa-sha2-512' + ] + + expected = 'PubkeyAcceptedAlgorithms ' + for alg in algs: + self.cli_set(base_path + ['pubkey-accepted-algorithm', alg]) + expected = f'{expected}{alg},' + expected = expected[:-1] + + self.cli_commit() + tmp_sshd_conf = read_file(SSHD_CONF) + self.assertIn(expected, tmp_sshd_conf) + if __name__ == '__main__': unittest.main(verbosity=2)