diff --git a/data/templates/ocserv/ocserv_config.j2 b/data/templates/ocserv/ocserv_config.j2 index 80ba357bc..b5e890c32 100644 --- a/data/templates/ocserv/ocserv_config.j2 +++ b/data/templates/ocserv/ocserv_config.j2 @@ -1,138 +1,139 @@ ### generated by vpn_openconnect.py ### {% if listen_address is vyos_defined %} listen-host = {{ listen_address }} {% endif %} tcp-port = {{ listen_ports.tcp }} udp-port = {{ listen_ports.udp }} run-as-user = nobody run-as-group = daemon {% if accounting.mode.radius is vyos_defined %} acct = "radius [config=/run/ocserv/radiusclient.conf]" {% endif %} {% if "radius" in authentication.mode %} auth = "radius [config=/run/ocserv/radiusclient.conf{{ ',groupconfig=true' if authentication.radius.groupconfig is vyos_defined else '' }}]" {% if authentication.identity_based_config.disabled is not vyos_defined %} {% if "group" in authentication.identity_based_config.mode %} config-per-group = {{ authentication.identity_based_config.directory }} default-group-config = {{ authentication.identity_based_config.default_config }} {% endif %} {% endif %} {% elif "local" in authentication.mode %} {% if authentication.mode.local == "password-otp" %} auth = "plain[passwd=/run/ocserv/ocpasswd,otp=/run/ocserv/users.oath]" {% elif authentication.mode.local == "otp" %} auth = "plain[otp=/run/ocserv/users.oath]" {% else %} auth = "plain[/run/ocserv/ocpasswd]" {% endif %} {% else %} auth = "plain[/run/ocserv/ocpasswd]" {% endif %} {% if "identity_based_config" in authentication %} {% if "user" in authentication.identity_based_config.mode %} config-per-user = {{ authentication.identity_based_config.directory }} default-user-config = {{ authentication.identity_based_config.default_config }} {% endif %} {% endif %} {% if ssl.certificate is vyos_defined %} server-cert = /run/ocserv/cert.pem server-key = /run/ocserv/cert.key {% if ssl.passphrase is vyos_defined %} key-pin = {{ ssl.passphrase }} {% endif %} {% endif %} {% if ssl.ca_certificate is vyos_defined %} ca-cert = /run/ocserv/ca.pem {% endif %} socket-file = /run/ocserv/ocserv.socket occtl-socket-file = /run/ocserv/occtl.socket use-occtl = true isolate-workers = true keepalive = 300 dpd = 60 mobile-dpd = 300 switch-to-tcp-timeout = 30 tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA:-VERS-SSL3.0:-ARCFOUR-128" auth-timeout = 240 idle-timeout = 1200 mobile-idle-timeout = 1800 min-reauth-time = 3 cookie-timeout = 300 rekey-method = ssl try-mtu-discovery = true cisco-client-compat = true dtls-legacy = true max-ban-score = 80 ban-reset-time = 300 # The name to use for the tun device device = sslvpn # DNS settings {% if network_settings.name_server is vyos_defined %} {% for dns in network_settings.name_server %} dns = {{ dns }} {% endfor %} {% endif %} {% if network_settings.tunnel_all_dns is vyos_defined %} {% if "yes" in network_settings.tunnel_all_dns %} tunnel-all-dns = true {% else %} tunnel-all-dns = false {% endif %} {% endif %} # IPv4 network pool {% if network_settings.client_ip_settings.subnet is vyos_defined %} ipv4-network = {{ network_settings.client_ip_settings.subnet }} {% endif %} # IPv6 network pool {% if network_settings.client_ipv6_pool.prefix is vyos_defined %} ipv6-network = {{ network_settings.client_ipv6_pool.prefix }} ipv6-subnet-prefix = {{ network_settings.client_ipv6_pool.mask }} {% endif %} {% if network_settings.push_route is vyos_defined %} {% for route in network_settings.push_route %} route = {{ route }} {% endfor %} {% endif %} {% if network_settings.split_dns is vyos_defined %} {% for tmp in network_settings.split_dns %} split-dns = {{ tmp }} {% endfor %} {% endif %} {% if authentication.group is vyos_defined %} # Group settings {% for grp in authentication.group %} select-group = {{ grp }} {% endfor %} {% endif %} - +{% if http_security_headers is vyos_defined %} # HTTP security headers included-http-headers = Strict-Transport-Security: max-age=31536000 ; includeSubDomains included-http-headers = X-Frame-Options: deny included-http-headers = X-Content-Type-Options: nosniff -included-http-headers = Content-Security-Policy: default-src ´none´ +included-http-headers = Content-Security-Policy: default-src "none" included-http-headers = X-Permitted-Cross-Domain-Policies: none included-http-headers = Referrer-Policy: no-referrer included-http-headers = Clear-Site-Data: "cache","cookies","storage" included-http-headers = Cross-Origin-Embedder-Policy: require-corp included-http-headers = Cross-Origin-Opener-Policy: same-origin included-http-headers = Cross-Origin-Resource-Policy: same-origin included-http-headers = X-XSS-Protection: 0 included-http-headers = Pragma: no-cache included-http-headers = Cache-control: no-store, no-cache +{% endif %} diff --git a/interface-definitions/vpn-openconnect.xml.in b/interface-definitions/vpn-openconnect.xml.in index 75c64a99a..736084f8b 100644 --- a/interface-definitions/vpn-openconnect.xml.in +++ b/interface-definitions/vpn-openconnect.xml.in @@ -1,386 +1,392 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="vpn"> <children> <node name="openconnect" owner="${vyos_conf_scripts_dir}/vpn_openconnect.py"> <properties> <help>SSL VPN OpenConnect, AnyConnect compatible server</help> <priority>901</priority> </properties> <children> <node name="accounting"> <properties> <help>Accounting for users OpenConnect VPN Sessions</help> </properties> <children> <node name="mode"> <properties> <help>Accounting mode used by this server</help> </properties> <children> <leafNode name="radius"> <properties> <help>Use RADIUS server for accounting</help> <valueless/> </properties> </leafNode> </children> </node> #include <include/radius-acct-server-ipv4.xml.i> </children> </node> <node name="authentication"> <properties> <help>Authentication for remote access SSL VPN Server</help> </properties> <children> <node name="mode"> <properties> <help>Authentication mode used by this server</help> </properties> <children> <leafNode name="local"> <properties> <help>Use local username/password configuration (OTP supported)</help> <valueHelp> <format>password</format> <description>Password-only local authentication</description> </valueHelp> <valueHelp> <format>otp</format> <description>OTP-only local authentication</description> </valueHelp> <valueHelp> <format>password-otp</format> <description>Password (first) + OTP local authentication</description> </valueHelp> <constraint> <regex>(password|otp|password-otp)</regex> </constraint> <constraintErrorMessage>Invalid authentication mode. Must be one of: password, otp or password-otp </constraintErrorMessage> <completionHelp> <list>otp password password-otp</list> </completionHelp> </properties> </leafNode> <leafNode name="radius"> <properties> <help>Use RADIUS server for user autentication</help> <valueless/> </properties> </leafNode> </children> </node> <node name="identity-based-config"> <properties> <help>Include configuration file by username or RADIUS group attribute</help> </properties> <children> #include <include/generic-disable-node.xml.i> <leafNode name="mode"> <properties> <help>Select per user or per group configuration file - ignored if authentication group is configured</help> <completionHelp> <list>user group</list> </completionHelp> <valueHelp> <format>user</format> <description>Match configuration file on username</description> </valueHelp> <valueHelp> <format>group</format> <description>Match RADIUS response class attribute as file name</description> </valueHelp> <constraint> <regex>(user|group)</regex> </constraint> <constraintErrorMessage>Invalid mode, must be either user or group</constraintErrorMessage> </properties> </leafNode> <leafNode name="directory"> <properties> <help>Directory to containing configuration files</help> <valueHelp> <format>path</format> <description>Path to configuration directory, must be under /config/auth</description> </valueHelp> <constraint> <validator name="file-path" argument="--directory --parent-dir /config/auth --strict"/> </constraint> </properties> </leafNode> <leafNode name="default-config"> <properties> <help>Default configuration if discrete config could not be found</help> <valueHelp> <format>filename</format> <description>Default configuration filename, must be under /config/auth</description> </valueHelp> <constraint> <validator name="file-path" argument="--file --parent-dir /config/auth --strict"/> </constraint> </properties> </leafNode> </children> </node> <leafNode name="group"> <properties> <help>Group that a client is allowed to select (from a list). Maps to RADIUS Class attribute.</help> <valueHelp> <format>txt</format> <description>Group string. The group may be followed by a user-friendly name in brackets: group1[First Group]</description> </valueHelp> <multi/> </properties> </leafNode> #include <include/auth-local-users.xml.i> <node name="local-users"> <children> <tagNode name="username"> <children> <node name="otp"> <properties> <help>2FA OTP authentication parameters</help> </properties> <children> <leafNode name="key"> <properties> <help>Token Key Secret key for the token algorithm (see RFC 4226)</help> <valueHelp> <format>txt</format> <description>OTP key in hex-encoded format</description> </valueHelp> <constraint> <regex>[a-fA-F0-9]{20,10000}</regex> </constraint> <constraintErrorMessage>Key name must only include hex characters and be at least 20 characters long</constraintErrorMessage> </properties> </leafNode> <leafNode name="otp-length"> <properties> <help>Number of digits in OTP code</help> <valueHelp> <format>u32:6-8</format> <description>Number of digits in OTP code</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 6-8"/> </constraint> <constraintErrorMessage>Number of digits in OTP code must be between 6 and 8</constraintErrorMessage> </properties> <defaultValue>6</defaultValue> </leafNode> <leafNode name="interval"> <properties> <help>Time tokens interval in seconds</help> <valueHelp> <format>u32:5-86400</format> <description>Time tokens interval in seconds.</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 5-86400"/> </constraint> <constraintErrorMessage>Time token interval must be between 5 and 86400 seconds</constraintErrorMessage> </properties> <defaultValue>30</defaultValue> </leafNode> <leafNode name="token-type"> <properties> <help>Token type</help> <valueHelp> <format>hotp-time</format> <description>Time-based OTP algorithm</description> </valueHelp> <valueHelp> <format>hotp-event</format> <description>Event-based OTP algorithm</description> </valueHelp> <constraint> <regex>(hotp-time|hotp-event)</regex> </constraint> <completionHelp> <list>hotp-time hotp-event</list> </completionHelp> </properties> <defaultValue>hotp-time</defaultValue> </leafNode> </children> </node> </children> </tagNode> </children> </node> #include <include/radius-auth-server-ipv4.xml.i> <node name="radius"> <children> #include <include/radius-timeout.xml.i> <leafNode name="groupconfig"> <properties> <help>If the groupconfig option is set, then config-per-user will be overriden, and all configuration will be read from RADIUS.</help> </properties> </leafNode> </children> </node> </children> </node> #include <include/listen-address-ipv4-single.xml.i> <leafNode name="listen-address"> <defaultValue>0.0.0.0</defaultValue> </leafNode> <node name="listen-ports"> <properties> <help>Specify custom ports to use for client connections</help> </properties> <children> <leafNode name="tcp"> <properties> <help>tcp port number to accept connections</help> <valueHelp> <format>u32:1-65535</format> <description>Numeric IP port</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> <defaultValue>443</defaultValue> </leafNode> <leafNode name="udp"> <properties> <help>udp port number to accept connections</help> <valueHelp> <format>u32:1-65535</format> <description>Numeric IP port</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> <defaultValue>443</defaultValue> </leafNode> </children> </node> + <leafNode name="http-security-headers"> + <properties> + <help>Enable HTTP security headers</help> + <valueless/> + </properties> + </leafNode> <node name="ssl"> <properties> <help>SSL Certificate, SSL Key and CA</help> </properties> <children> #include <include/pki/ca-certificate.xml.i> #include <include/pki/certificate-key.xml.i> </children> </node> <node name="network-settings"> <properties> <help>Network settings</help> </properties> <children> <leafNode name="push-route"> <properties> <help>Route to be pushed to the client</help> <valueHelp> <format>ipv4net</format> <description>IPv4 network and prefix length</description> </valueHelp> <valueHelp> <format>ipv6net</format> <description>IPv6 network and prefix length</description> </valueHelp> <constraint> <validator name="ip-prefix"/> </constraint> <multi/> </properties> </leafNode> <node name="client-ip-settings"> <properties> <help>Client IP pools settings</help> </properties> <children> <leafNode name="subnet"> <properties> <help>Client IP subnet (CIDR notation)</help> <valueHelp> <format>ipv4net</format> <description>IPv4 address and prefix length</description> </valueHelp> <constraint> <validator name="ipv4-prefix"/> </constraint> <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage> </properties> </leafNode> </children> </node> <node name="client-ipv6-pool"> <properties> <help>Pool of client IPv6 addresses</help> </properties> <children> <leafNode name="prefix"> <properties> <help>Pool of addresses used to assign to clients</help> <valueHelp> <format>ipv6net</format> <description>IPv6 address and prefix length</description> </valueHelp> <constraint> <validator name="ipv6-prefix"/> </constraint> </properties> </leafNode> <leafNode name="mask"> <properties> <help>Prefix length used for individual client</help> <valueHelp> <format>u32:48-128</format> <description>Client prefix length</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 48-128"/> </constraint> </properties> <defaultValue>64</defaultValue> </leafNode> </children> </node> #include <include/name-server-ipv4-ipv6.xml.i> <leafNode name="split-dns"> <properties> <help>Domains over which the provided DNS should be used</help> <valueHelp> <format>txt</format> <description>Client prefix length</description> </valueHelp> <constraint> <validator name="fqdn"/> </constraint> <multi/> </properties> </leafNode> <leafNode name="tunnel-all-dns"> <properties> <help>If the tunnel-all-dns option is set to yes, tunnel all DNS queries via the VPN. This is the default when a default route is set.</help> <completionHelp> <list>yes no</list> </completionHelp> <valueHelp> <format>yes</format> <description>Enable tunneling of all DNS traffic</description> </valueHelp> <valueHelp> <format>no</format> <description>Disable tunneling of all DNS traffic</description> </valueHelp> <constraint> <regex>(yes|no)</regex> </constraint> </properties> <defaultValue>no</defaultValue> </leafNode> </children> </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index 04abeb1aa..c4502fada 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -1,145 +1,166 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2022 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 <http://www.gnu.org/licenses/>. import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.template import ip_from_cidr from vyos.utils.process import process_named_running from vyos.utils.file import read_file OCSERV_CONF = '/run/ocserv/ocserv.conf' base_path = ['vpn', 'openconnect'] pki_path = ['pki'] cert_data = """ MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE 01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3 QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu +JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93 +dm/LDnp7C0= """ key_data = """ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx 2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7 u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww """ PROCESS_NAME = 'ocserv-main' config_file = '/run/ocserv/ocserv.conf' auth_file = '/run/ocserv/ocpasswd' otp_file = '/run/ocserv/users.oath' listen_if = 'dum116' listen_address = '100.64.0.1/32' class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestVPNOpenConnect, 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_set(cls, ['interfaces', 'dummy', listen_if, 'address', listen_address]) cls.cli_set(cls, pki_path + ['ca', 'openconnect', 'certificate', cert_data.replace('\n','')]) cls.cli_set(cls, pki_path + ['certificate', 'openconnect', 'certificate', cert_data.replace('\n','')]) cls.cli_set(cls, pki_path + ['certificate', 'openconnect', 'private', 'key', key_data.replace('\n','')]) @classmethod def tearDownClass(cls): cls.cli_delete(cls, pki_path) cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) super(TestVPNOpenConnect, cls).tearDownClass() def tearDown(self): self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(base_path) self.cli_commit() self.assertFalse(process_named_running(PROCESS_NAME)) def test_ocserv(self): user = 'vyos_user' password = 'vyos_pass' otp = '37500000026900000000200000000000' v4_subnet = '192.0.2.0/24' v6_prefix = '2001:db8:1000::/64' v6_len = '126' name_server = ['1.2.3.4', '1.2.3.5', '2001:db8::1'] split_dns = ['vyos.net', 'vyos.io'] self.cli_set(base_path + ['authentication', 'local-users', 'username', user, 'password', password]) self.cli_set(base_path + ['authentication', 'local-users', 'username', user, 'otp', 'key', otp]) self.cli_set(base_path + ['authentication', 'mode', 'local', 'password-otp']) self.cli_set(base_path + ['network-settings', 'client-ip-settings', 'subnet', v4_subnet]) self.cli_set(base_path + ['network-settings', 'client-ipv6-pool', 'prefix', v6_prefix]) self.cli_set(base_path + ['network-settings', 'client-ipv6-pool', 'mask', v6_len]) for ns in name_server: self.cli_set(base_path + ['network-settings', 'name-server', ns]) for domain in split_dns: self.cli_set(base_path + ['network-settings', 'split-dns', domain]) self.cli_set(base_path + ['ssl', 'ca-certificate', 'openconnect']) self.cli_set(base_path + ['ssl', 'certificate', 'openconnect']) listen_ip_no_cidr = ip_from_cidr(listen_address) self.cli_set(base_path + ['listen-address', listen_ip_no_cidr]) self.cli_commit() # Verify configuration daemon_config = read_file(config_file) # authentication mode local password-otp self.assertIn(f'auth = "plain[passwd=/run/ocserv/ocpasswd,otp=/run/ocserv/users.oath]"', daemon_config) self.assertIn(f'listen-host = {listen_ip_no_cidr}', daemon_config) self.assertIn(f'ipv4-network = {v4_subnet}', daemon_config) self.assertIn(f'ipv6-network = {v6_prefix}', daemon_config) self.assertIn(f'ipv6-subnet-prefix = {v6_len}', daemon_config) # defaults self.assertIn(f'tcp-port = 443', daemon_config) self.assertIn(f'udp-port = 443', daemon_config) for ns in name_server: self.assertIn(f'dns = {ns}', daemon_config) for domain in split_dns: self.assertIn(f'split-dns = {domain}', daemon_config) auth_config = read_file(auth_file) self.assertIn(f'{user}:*:$', auth_config) otp_config = read_file(otp_file) self.assertIn(f'HOTP/T30/6 {user} - {otp}', otp_config) + + # Verify HTTP security headers + self.cli_set(base_path + ['http-security-headers']) + self.cli_commit() + + daemon_config = read_file(config_file) + + self.assertIn('included-http-headers = Strict-Transport-Security: max-age=31536000 ; includeSubDomains', daemon_config) + self.assertIn('included-http-headers = X-Frame-Options: deny', daemon_config) + self.assertIn('included-http-headers = X-Content-Type-Options: nosniff', daemon_config) + self.assertIn('included-http-headers = Content-Security-Policy: default-src "none"', daemon_config) + self.assertIn('included-http-headers = X-Permitted-Cross-Domain-Policies: none', daemon_config) + self.assertIn('included-http-headers = Referrer-Policy: no-referrer', daemon_config) + self.assertIn('included-http-headers = Clear-Site-Data: "cache","cookies","storage"', daemon_config) + self.assertIn('included-http-headers = Cross-Origin-Embedder-Policy: require-corp', daemon_config) + self.assertIn('included-http-headers = Cross-Origin-Opener-Policy: same-origin', daemon_config) + self.assertIn('included-http-headers = Cross-Origin-Resource-Policy: same-origin', daemon_config) + self.assertIn('included-http-headers = X-XSS-Protection: 0', daemon_config) + self.assertIn('included-http-headers = Pragma: no-cache', daemon_config) + self.assertIn('included-http-headers = Cache-control: no-store, no-cache', daemon_config) + if __name__ == '__main__': unittest.main(verbosity=2)