From 2bb8817348a6df639ec9959298422b7e7b923823 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 20 Jul 2021 11:25:46 +0200 Subject: pki: openconnect: T3642: Migrate OpenConnect SSL to PKI configuration --- data/templates/ocserv/ocserv_config.tmpl | 14 +-- interface-definitions/vpn_openconnect.xml.in | 9 +- smoketest/configs/pki-misc | 69 +++++++++++++ smoketest/scripts/cli/test_vpn_openconnect.py | 18 +++- src/conf_mode/vpn_openconnect.py | 65 ++++++++++-- src/migration-scripts/openconnect/0-to-1 | 136 ++++++++++++++++++++++++++ 6 files changed, 287 insertions(+), 24 deletions(-) create mode 100644 smoketest/configs/pki-misc create mode 100755 src/migration-scripts/openconnect/0-to-1 diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl index 328af0c0d..0be805235 100644 --- a/data/templates/ocserv/ocserv_config.tmpl +++ b/data/templates/ocserv/ocserv_config.tmpl @@ -12,16 +12,16 @@ auth = "radius [config=/run/ocserv/radiusclient.conf]" auth = "plain[/run/ocserv/ocpasswd]" {% endif %} -{% if ssl.cert_file %} -server-cert = {{ ssl.cert_file }} +{% if ssl.certificate is defined %} +server-cert = /run/ocserv/cert.pem +server-key = /run/ocserv/cert.key +{% if ssl.passphrase is defined %} +key-pin = {{ ssl.passphrase }} {% endif %} - -{% if ssl.key_file %} -server-key = {{ ssl.key_file }} {% endif %} -{% if ssl.ca_cert_file %} -ca-cert = {{ ssl.ca_cert_file }} +{% if ssl.ca_certificate is defined %} +ca-cert = /run/ocserv/ca.pem {% endif %} socket-file = /run/ocserv/ocserv.socket diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in index 1a9d39a12..53c0c22b9 100644 --- a/interface-definitions/vpn_openconnect.xml.in +++ b/interface-definitions/vpn_openconnect.xml.in @@ -75,7 +75,7 @@ - SSL Certificate, SSL Key and CA (/config/auth) + Specify custom ports to use for client connections @@ -108,12 +108,11 @@ - SSL Certificate, SSL Key and CA (/config/auth) + SSL Certificate, SSL Key and CA - #include - #include - #include + #include + #include diff --git a/smoketest/configs/pki-misc b/smoketest/configs/pki-misc new file mode 100644 index 000000000..929552267 --- /dev/null +++ b/smoketest/configs/pki-misc @@ -0,0 +1,69 @@ +interfaces { + ethernet eth0 { + address 192.168.150.1/24 + } +} +system { + config-management { + commit-revisions 100 + } + console { + device ttyS0 { + speed 115200 + } + } + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ + plaintext-password "" + } + } + } + ntp { + server time1.vyos.net { + } + server time2.vyos.net { + } + server time3.vyos.net { + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } +} +vpn { + openconnect { + authentication { + local-users { + username test { + password test + } + } + mode local + } + network-settings { + client-ip-settings { + subnet 192.168.160.0/24 + } + } + ssl { + ca-cert-file /config/auth/ovpn_test_ca.pem + cert-file /config/auth/ovpn_test_server.pem + key-file /config/auth/ovpn_test_server.key + } + } +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "bgp@1:broadcast-relay@1:cluster@1:config-management@1:conntrack@2:conntrack-sync@2:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@6:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:nat66@1:ntp@1:policy@1:pppoe-server@5:pptp@2:qos@1:quagga@9:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrf@2:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.4-rolling-202106290839 diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index bf528c8b7..cad3b1182 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -23,25 +23,33 @@ from vyos.util import process_named_running OCSERV_CONF = '/run/ocserv/ocserv.conf' base_path = ['vpn', 'openconnect'] -cert = '/etc/ssl/certs/ssl-cert-snakeoil.pem' -cert_key = '/etc/ssl/private/ssl-cert-snakeoil.key' + +pki_path = ['pki'] +cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0=' +key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww' class TestVpnOpenconnect(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Delete vpn openconnect configuration + self.cli_delete(pki_path) self.cli_delete(base_path) self.cli_commit() def test_vpn(self): user = 'vyos_user' password = 'vyos_pass' + self.cli_delete(pki_path) self.cli_delete(base_path) + + self.cli_set(pki_path + ['ca', 'openconnect', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'openconnect', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'openconnect', 'private', 'key', key_data]) + self.cli_set(base_path + ["authentication", "local-users", "username", user, "password", password]) self.cli_set(base_path + ["authentication", "mode", "local"]) self.cli_set(base_path + ["network-settings", "client-ip-settings", "subnet", "192.0.2.0/24"]) - self.cli_set(base_path + ["ssl", "ca-cert-file", cert]) - self.cli_set(base_path + ["ssl", "cert-file", cert]) - self.cli_set(base_path + ["ssl", "key-file", cert_key]) + self.cli_set(base_path + ["ssl", "ca-certificate", 'openconnect']) + self.cli_set(base_path + ["ssl", "certificate", 'openconnect']) self.cli_commit() diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 2986c3458..f6db196dc 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -19,9 +19,11 @@ from sys import exit from vyos.config import Config from vyos.configdict import dict_merge -from vyos.xml import defaults +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call +from vyos.xml import defaults from vyos import ConfigError from crypt import crypt, mksalt, METHOD_SHA512 @@ -50,6 +52,10 @@ def get_config(): default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) + if ocserv: + ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + return ocserv def verify(ocserv): @@ -72,14 +78,36 @@ def verify(ocserv): raise ConfigError('openconnect authentication credentials required') # Check ssl - if "ssl" in ocserv: - req_cert = ['cert_file', 'key_file'] - for cert in req_cert: - if not cert in ocserv["ssl"]: - raise ConfigError('openconnect ssl {0} required'.format(cert.replace('_', '-'))) - else: + if 'ssl' not in ocserv: raise ConfigError('openconnect ssl required') + if not ocserv['pki'] or 'certificate' not in ocserv['pki']: + raise ConfigError('PKI not configured') + + ssl = ocserv['ssl'] + if 'certificate' not in ssl: + raise ConfigError('openconnect ssl certificate required') + + cert_name = ssl['certificate'] + + if cert_name not in ocserv['pki']['certificate']: + raise ConfigError('Invalid openconnect ssl certificate') + + cert = ocserv['pki']['certificate'][cert_name] + + if 'certificate' not in cert: + raise ConfigError('Missing certificate in PKI') + + if 'private' not in cert or 'key' not in cert['private']: + raise ConfigError('Missing private key in PKI') + + if 'ca_certificate' in ssl: + if 'ca' not in ocserv['pki']: + raise ConfigError('PKI not configured') + + if ssl['ca_certificate'] not in ocserv['pki']['ca']: + raise ConfigError('Invalid openconnect ssl CA certificate') + # Check network settings if "network_settings" in ocserv: if "push_route" in ocserv["network_settings"]: @@ -109,6 +137,29 @@ def generate(ocserv): # Render local users render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"]) + if "ssl" in ocserv: + cert_file_path = os.path.join(cfg_dir, 'cert.pem') + cert_key_path = os.path.join(cfg_dir, 'cert.key') + ca_cert_file_path = os.path.join(cfg_dir, 'ca.pem') + + if 'certificate' in ocserv['ssl']: + cert_name = ocserv['ssl']['certificate'] + pki_cert = ocserv['pki']['certificate'][cert_name] + + with open(cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_cert['certificate'])) + + if 'private' in pki_cert and 'key' in pki_cert['private']: + with open(cert_key_path, 'w') as f: + f.write(wrap_private_key(pki_cert['private']['key'])) + + if 'ca_certificate' in ocserv['ssl']: + ca_name = ocserv['ssl']['ca_certificate'] + pki_ca_cert = ocserv['pki']['ca'][ca_name] + + with open(ca_cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_ca_cert['certificate'])) + # Render config render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv) diff --git a/src/migration-scripts/openconnect/0-to-1 b/src/migration-scripts/openconnect/0-to-1 new file mode 100755 index 000000000..83cd09143 --- /dev/null +++ b/src/migration-scripts/openconnect/0-to-1 @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 . + +# - Update SSL to use PKI configuration + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key +from vyos.util import run + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'openconnect'] +pki_base = ['pki'] + +if not config.exists(base): + exit(0) + +AUTH_DIR = '/config/auth' + +def wrapped_pem_to_config_value(pem): + return "".join(pem.strip().split("\n")[1:-1]) + +if not config.exists(base + ['ssl']): + exit(0) + +x509_base = base + ['ssl'] +pki_name = 'openconnect' + +if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + +if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + +if config.exists(x509_base + ['ca-cert-file']): + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on openconnect config') + + config.delete(x509_base + ['ca-cert-file']) + +if config.exists(x509_base + ['cert-file']): + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on openconnect config') + + config.delete(x509_base + ['cert-file']) + +if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on openconnect config') + + config.delete(x509_base + ['key-file']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) -- cgit v1.2.3