diff options
| -rw-r--r-- | python/vyos/configverify.py | 103 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpn_openconnect.py | 135 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces_ethernet.py | 20 | ||||
| -rwxr-xr-x | src/conf_mode/load-balancing_reverse-proxy.py | 38 | ||||
| -rwxr-xr-x | src/conf_mode/service_https.py | 39 | ||||
| -rwxr-xr-x | src/conf_mode/vpn_openconnect.py | 52 | ||||
| -rwxr-xr-x | src/conf_mode/vpn_sstp.py | 65 | 
7 files changed, 249 insertions, 203 deletions
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 651036bad..300647d21 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -169,43 +169,6 @@ def verify_tunnel(config):          if 'source_address' in config and is_ipv6(config['source_address']):              raise ConfigError('Can not use local IPv6 address is for mGRE tunnels') -def verify_eapol(config): -    """ -    Common helper function used by interface implementations to perform -    recurring validation of EAPoL configuration. -    """ -    if 'eapol' in config: -        if 'certificate' not in config['eapol']: -            raise ConfigError('Certificate must be specified when using EAPoL!') - -        if 'pki' not in config or 'certificate' not in config['pki']: -            raise ConfigError('Invalid certificate specified for EAPoL') - -        cert_name = config['eapol']['certificate'] -        if cert_name not in config['pki']['certificate']: -            raise ConfigError('Invalid certificate specified for EAPoL') - -        cert = config['pki']['certificate'][cert_name] - -        if 'certificate' not in cert or 'private' not in cert or 'key' not in cert['private']: -            raise ConfigError('Invalid certificate/private key specified for EAPoL') - -        if 'password_protected' in cert['private']: -            raise ConfigError('Encrypted private key cannot be used for EAPoL') - -        if 'ca_certificate' in config['eapol']: -            if 'ca' not in config['pki']: -                raise ConfigError('Invalid CA certificate specified for EAPoL') - -            for ca_cert_name in config['eapol']['ca_certificate']: -                if ca_cert_name not in config['pki']['ca']: -                    raise ConfigError('Invalid CA certificate specified for EAPoL') - -                ca_cert = config['pki']['ca'][ca_cert_name] - -                if 'certificate' not in ca_cert: -                    raise ConfigError('Invalid CA certificate specified for EAPoL') -  def verify_mirror_redirect(config):      """      Common helper function used by interface implementations to perform @@ -495,3 +458,69 @@ def verify_access_list(access_list, config, version=''):      # Check if the specified ACL exists, if not error out      if dict_search(f'policy.access-list{version}.{access_list}', config) == None:          raise ConfigError(f'Specified access-list{version} "{access_list}" does not exist!') + +def verify_pki_certificate(config: dict, cert_name: str, no_password_protected: bool=False): +    """ +    Common helper function user by PKI consumers to perform recurring +    validation functions for PEM based certificates +    """ +    if 'pki' not in config: +        raise ConfigError('PKI is not configured!') + +    if 'certificate' not in config['pki']: +        raise ConfigError('PKI does not contain any certificates!') + +    if cert_name not in config['pki']['certificate']: +        raise ConfigError(f'Certificate "{cert_name}" not found in configuration!') + +    pki_cert = config['pki']['certificate'][cert_name] +    if 'certificate' not in pki_cert: +        raise ConfigError(f'PEM certificate for "{cert_name}" missing in configuration!') + +    if 'private' not in pki_cert or 'key' not in pki_cert['private']: +        raise ConfigError(f'PEM private key for "{cert_name}" missing in configuration!') + +    if no_password_protected and 'password_protected' in pki_cert['private']: +        raise ConfigError('Password protected PEM private key is not supported!') + +def verify_pki_ca_certificate(config: dict, ca_name: str): +    """ +    Common helper function user by PKI consumers to perform recurring +    validation functions for PEM based CA certificates +    """ +    if 'pki' not in config: +        raise ConfigError('PKI is not configured!') + +    if 'ca' not in config['pki']: +        raise ConfigError('PKI does not contain any CA certificates!') + +    if ca_name not in config['pki']['ca']: +        raise ConfigError(f'CA Certificate "{ca_name}" not found in configuration!') + +    pki_cert = config['pki']['ca'][ca_name] +    if 'certificate' not in pki_cert: +        raise ConfigError(f'PEM CA certificate for "{cert_name}" missing in configuration!') + +def verify_pki_dh_parameters(config: dict, dh_name: str, min_key_size: int=0): +    """ +    Common helper function user by PKI consumers to perform recurring +    validation functions on DH parameters +    """ +    from vyos.pki import load_dh_parameters + +    if 'pki' not in config: +        raise ConfigError('PKI is not configured!') + +    if 'dh' not in config['pki']: +        raise ConfigError('PKI does not contain any DH parameters!') + +    if dh_name not in config['pki']['dh']: +        raise ConfigError(f'DH parameter "{dh_name}" not found in configuration!') + +    if min_key_size: +        pki_dh = config['pki']['dh'][dh_name] +        dh_params = load_dh_parameters(pki_dh['parameters']) +        dh_numbers = dh_params.parameter_numbers() +        dh_bits = dh_numbers.p.bit_length() +        if dh_bits < min_key_size: +            raise ConfigError(f'Minimum DH key-size is {min_key_size} bits!') diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index c4502fada..96e858fdb 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -18,6 +18,7 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError  from vyos.template import ip_from_cidr  from vyos.utils.process import process_named_running  from vyos.utils.file import read_file @@ -27,25 +28,110 @@ base_path = ['vpn', 'openconnect']  pki_path = ['pki'] +cert_name = 'OCServ'  cert_data = """ -MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw -WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv -bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx -MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV -BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP -UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3 -QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E -BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu -+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz -ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93 -+dm/LDnp7C0= +MIIDsTCCApmgAwIBAgIURNQMaYmRIP/d+/OPWPWmuwkYHbswDQYJKoZIhvcNAQEL +BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM +CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y +NDA0MDIxNjQxMTRaFw0yNTA0MDIxNjQxMTRaMFcxCzAJBgNVBAYTAkdCMRMwEQYD +VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5 +T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDFeexWVV70fBLOxGofWYlcNxJ9JyLviAZZDXrBIYfQnSrYp51yMKRPTH1e +Sjr7gIxVArAqLoYFgo7frRDkCKg8/izTopxtBTV2XJkLqDGA7DOrtBhgj0zjmF0A +WWIWi83WHc+sTHSvIqNLCDAZgnnzf1ch3W/na10hBTnFX4Yv6CJ4I7doSIyWzaQr +RvUXfaNYnvege+RrG5LzkVGxD2EhHyBqfQ2mxvlgqICqKSZkL56a3c/MHAm+7MKl +2KbSGxwNDs+SpHrCgWVIsl9w0bN2NSAu6GzyfW7V+V1dkiCggLlxXGhGncPMiQ7T +M7GKQULnQl5o/15GkW72Tg6wUdDpAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYD +VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTtil1X +c6dXA6kxZtZCgjx9QPzeLDAfBgNVHSMEGDAWgBTKMZvYAW1thn/uxX1fpcbP5vKq +dzANBgkqhkiG9w0BAQsFAAOCAQEARjS+QYJDz+XTdwK/lMF1GhSdacGnOIWRsbRx +N7odsyBV7Ud5W+Py79n+/PRirw2+jAaGXFmmgdxrcjlM+dZnlO3X0QCIuNdODggD +0J/u1ICPdm9TcJ2lEdbIE2vm2Q9P5RdQ7En7zg8Wu+rcNPlIxd3pHFOMX79vOcgi +RkWWII6tyeeT9COYgXUbg37wf2LkVv4b5PcShrfkWZVFWKDKr1maJ+iMwcIlosOe +Gj3SKe7gKBuPbMRwtocqKAYbW1GH12tA49DNkvxVKxVqnP4nHkwgfOJdpcZAjlyb +gLkzVKInZwg5EvJ7qtSJirDap9jyuLTfr5TmxbcdEhmAqeS41A==  """ -key_data = """ -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx -2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7 -u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww +cert_key_data = """ +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFeexWVV70fBLO +xGofWYlcNxJ9JyLviAZZDXrBIYfQnSrYp51yMKRPTH1eSjr7gIxVArAqLoYFgo7f +rRDkCKg8/izTopxtBTV2XJkLqDGA7DOrtBhgj0zjmF0AWWIWi83WHc+sTHSvIqNL +CDAZgnnzf1ch3W/na10hBTnFX4Yv6CJ4I7doSIyWzaQrRvUXfaNYnvege+RrG5Lz +kVGxD2EhHyBqfQ2mxvlgqICqKSZkL56a3c/MHAm+7MKl2KbSGxwNDs+SpHrCgWVI +sl9w0bN2NSAu6GzyfW7V+V1dkiCggLlxXGhGncPMiQ7TM7GKQULnQl5o/15GkW72 +Tg6wUdDpAgMBAAECggEACbR8bHZv9GT/9EshNLQ3n3a8wQuCLd0fWWi5A90sKbun +pj5/6uOVbP5DL7Xx4HgIrYmJyIZBI5aEg11Oi15vjOZ9o9MF4V0UVmJQ9TU0EEl2 +H/X5uA54MWaaCiaFFGWU3UqEG8wldJFSZCFyt7Y6scBW3b0JFF7+6dyyDPoCWWqh +cNR41Hv0T0eqfXGOXX1JcBlLbqy0QXXeFoLlxV3ouIgWgkKJk7u3vDWCVM/ofP0m +/GyZYWCEA2JljEQZaVgtk1afFoamrjM4doMiirk+Tix4yGno94HLJdDUynqdLNAd +ZdKunFVAJau17b1VVPyfgIvIaPRvSGQVQoXH6TuB2QKBgQD5LRYTxsd8WsOwlB2R +SBYdzDff7c3VuNSAYTp7O2MqWrsoXm2MxLzEJLJUen+jQphL6ti/ObdrSOnKF2So +SizYeJ1Irx4M4BPSdy/Yt3T/+e+Y4K7iQ7Pdvdc/dlZ5XuNHYzuA/F7Ft/9rhUy9 +jSdQYANX+7h8vL7YrEjvhMMMZQKBgQDK4mG4D7XowLlBWv1fK4n/ErWvYSxH/X+A +VVnLv4z4aZHyRS2nTfQnb8PKbHJ/65x9yZs8a+6HqE4CAH+0LfZuOI8qn9OksxPZ +7GuQk/FiVyGXtu18hzlfhzmb0ZTjAalZ5b68DOIhyZIHVketebhljXaB5bfwdIgt +7vTOfotANQKBgQCWiA5WVDgfgBXIjzJtmkcCKWV3+onnG4oFJLfXysDVzYpTkPhN +mm0PcbvqHTcOwiSPeIkIvS15usrCM++zW1xMSlF6n5Bf5t8Svr5BBlPAcJW2ncYJ +Gy2GQDHRPQRwvko/zkscWVpHyCieJCGAQc4GWHqspH2Hnd8Ntsc5K9NJoQKBgFR1 +5/5rM+yghr7pdT9wbbNtg4tuZbPWmYTAg3Bp3vLvaB22pOnYbwMX6SdU/Fm6qVxI +WMLPn+6Dp2337TICTGvYSemRvdb74hC/9ouquzuYUFjLg5Rq6vyU2+u9VUEnyOuu +1DePGXi9ZHh/d7mFSbmlKaesDWYh7StKJknsrmXdAoGBAOm+FnzryKkhIq/ELyT9 +8v4wr0lxCcAP3nNb/P5ocv3m7hRLIkf4S9k/gAL+gE/OtdesomQKjOz7noLO+I2H +rj6ZfC/lhPIRJ4XK5BqgqqH53Zcl/HDoaUjbpmyMvZVoQfUHLut8Y912R6mfm65z +qXl1L7EdHTY+SdoThNJTpmWb +""" + +ca_name = 'VyOS-CA' +ca_data = """ +MIIDnTCCAoWgAwIBAgIUFVRURZXSbQ7F0DiSZYfqY0gQORMwDQYJKoZIhvcNAQEL +BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM +CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y +NDA0MDIxNjQxMDFaFw0yOTA0MDExNjQxMDFaMFcxCzAJBgNVBAYTAkdCMRMwEQYD +VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5 +T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCg7Mjl6+rs8Bdkjqgl2QDuHfrH2mTDCeB7WuNTnIz0BPDtlmwIdqhU7LdC +B/zUSABAa6LBe/Z/bKWCRKyq8fU2/4uWECe975IMXOfFdYT6KA78DROvOi32JZml +n0LAXV+538eb+g19xNtoBhPO8igiNevfkV+nJehRK/41ATj+assTOv87vaSX7Wqy +aP/ZqkIdQD9Kc3cqB4JsYjkWcniHL9yk4oY3cjKK8PJ1pi4FqgFHt2hA+Ic+NvbA +hc47K9otP8FM4jkSii3MZfHA6Czb43BtbR+YEiWPzBhzE2bCuIgeRUumMF1Z+CAT +6U7Cpx3XPh+Ac2RnDa8wKeQ1eqE1AgMBAAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8w +DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAd +BgNVHQ4EFgQUyjGb2AFtbYZ/7sV9X6XGz+byqncwDQYJKoZIhvcNAQELBQADggEB +AArGXCq92vtaUZt528lC34ENPL9bQ7nRAS/ojplAzM9reW3o56sfYWf1M8iwRsJT +LbAwSnVB929RLlDolNpLwpzd1XaMt61Zcx4MFQmQCd+40dfuvMhluZaxt+F9bC1Z +cA7uwe/2HrAIULq3sga9LzSph6dNuyd1rGchr4xHCJ7u4WcF0kqi0Hjcn9S/ppEc +ba2L3rRqZmCbe6Yngx+MS06jonGw0z8F6e8LMkcvJUlNMEC76P+5Byjp4xZGP+y3 +DtIfsfijpb+t1OUe75YmWflTFnHR9GlybNYTxGAl49mFw6LlS1kefXyPtfuReLmv +n+vZdJAWTq76zAPT3n9FClo= +""" + +ca_key_data = """ + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCg7Mjl6+rs8Bd + kjqgl2QDuHfrH2mTDCeB7WuNTnIz0BPDtlmwIdqhU7LdCB/zUSABAa6LBe/Z/bK + WCRKyq8fU2/4uWECe975IMXOfFdYT6KA78DROvOi32JZmln0LAXV+538eb+g19x + NtoBhPO8igiNevfkV+nJehRK/41ATj+assTOv87vaSX7WqyaP/ZqkIdQD9Kc3cq + B4JsYjkWcniHL9yk4oY3cjKK8PJ1pi4FqgFHt2hA+Ic+NvbAhc47K9otP8FM4jk + Sii3MZfHA6Czb43BtbR+YEiWPzBhzE2bCuIgeRUumMF1Z+CAT6U7Cpx3XPh+Ac2 + RnDa8wKeQ1eqE1AgMBAAECggEAEDDaoqVqmMWsONoQiWRMr2h1RZvPxP7OpuKVW + iF3XgrMOb9HZc+Ybpj1dC+NDMekvNaHhMuF2Lqz6UgjDjzzVMH/x4yfDwFWUqeb + SxbglvGmVk4zg48JNkmArLT6GJQccD1XXjZZmqSOhagM4KalCpIdxfvgoZbTCa2 + xMSCLHS+1HCDcmpCoeXM6ZBPTn0NbjRDAqIzCwcq2veG7RSz040obk8h7nrdv7j + hxRGmtPmPFzKgGLNn6GnL7AwYVMiidjj/ntvM4B1OMs9MwUYbtpg98TWcWyu+ZR + akUrnVf9z2aIHCKyuJvke/PNqMgw+L8KV4/478XxWhXfl7K1F3nMQKBgQDRBUDY + NFH0wC4MMWsA+RGwyz7RlzACChDJCMtA/agbW06gUoE9UYf8KtLQQQYljlLJHxH + GD72QnuM+sowGGXnbD4BabA9TQiQUG5c6boznTy1uU1gt8T0Zl0mmC7vIMoMBVd + 5bb0qrZvuR123kDGYn6crug9uvMIYSSlhGmBGTJQKBgQDFGC3vfkCyXzLoYy+RI + s/rXgyBF1PUYQtyDgL0N811L0H7a8JhFnt4FvodUbxv2ob+1kIc9e3yXT6FsGyO + 7IDOnqgeQKy74bYqVPZZuf1FOFb9fuxf00pn1FmhAF4OuSWkhVhrKkyrZwdD8Ar + jLK253J94dogjdKAYfN1csaOA0QKBgD0zUZI8d4a3QoRVb+RACTr/t6v8nZTrR5 + DlX0XvP2qLKJFutuKyXaOrEkDh2R/j9T9oNncMos+WhikUdEVQ7koC1u0i2LXjF + tdAYN4+Akmz+DRmeNoy2VYF4w2YP+pVR+B7OPkCtBVNuPkx3743Fy42mTGPMCKy + jX8Lf59j5Tl1AoGBAI3sk2dZqozHMIlWovIH92CtIKP0gFD2cJ94p3fklvZDSWg + aeKYg4lffc8uZB/AjlAH9ly3ziZx0uIjcOc/RTg96/+SI/dls9xgUhjCmVVJ692 + ki9GMsau/JYaEl+pTvjcOiocDJfNwQHJM3Tx+3FII59DtyXyXo3T/E6kHNSMeBA + oGAR9M48DTspv9OH1S7X6yR6MtMY5ltsBmB3gPhQFxiDKBvARkIkAPqObQ9TG/V + uOz2Purq0Oz7SHsY2jiFDd2KEGo6JfG61NDdIhiQC99ztSgt7NtvSCnX22SfVDW + oFxSK+tek7tvDVXAXCNy4ZESMEUGJ6NDHImb80aF+xZ3wYKw=  """  PROCESS_NAME = 'ocserv-main' @@ -67,9 +153,10 @@ class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase):          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','')]) +        cls.cli_set(cls, pki_path + ['ca', cert_name, 'certificate', ca_data.replace('\n','')]) +        cls.cli_set(cls, pki_path + ['ca', cert_name, 'private', 'key', ca_key_data.replace('\n','')]) +        cls.cli_set(cls, pki_path + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')]) +        cls.cli_set(cls, pki_path + ['certificate', cert_name, 'private', 'key', cert_key_data.replace('\n','')])      @classmethod      def tearDownClass(cls): @@ -108,8 +195,12 @@ class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase):          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']) +        # SSL certificates are mandatory +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() + +        self.cli_set(base_path + ['ssl', 'ca-certificate', cert_name]) +        self.cli_set(base_path + ['ssl', 'certificate', cert_name])          listen_ip_no_cidr = ip_from_cidr(listen_address)          self.cli_set(base_path + ['listen-address', listen_ip_no_cidr]) diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index 2c0f846c3..504d48f89 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -15,7 +15,6 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os -import pprint  from glob import glob  from sys import exit @@ -26,7 +25,6 @@ from vyos.configdict import get_interface_dict  from vyos.configdict import is_node_changed  from vyos.configverify import verify_address  from vyos.configverify import verify_dhcpv6 -from vyos.configverify import verify_eapol  from vyos.configverify import verify_interface_exists  from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_mtu @@ -34,6 +32,8 @@ from vyos.configverify import verify_mtu_ipv6  from vyos.configverify import verify_vlan_config  from vyos.configverify import verify_vrf  from vyos.configverify import verify_bond_bridge_member +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate  from vyos.ethtool import Ethtool  from vyos.ifconfig import EthernetIf  from vyos.ifconfig import BondIf @@ -263,6 +263,22 @@ def verify_allowedbond_changes(ethernet: dict):                                f' on interface "{ethernet["ifname"]}".' \                                f' Interface is a bond member') +def verify_eapol(ethernet: dict): +    """ +    Common helper function used by interface implementations to perform +    recurring validation of EAPoL configuration. +    """ +    if 'eapol' not in ethernet: +        return + +    if 'certificate' not in ethernet['eapol']: +        raise ConfigError('Certificate must be specified when using EAPoL!') + +    verify_pki_certificate(ethernet, ethernet['eapol']['certificate'], no_password_protected=True) + +    if 'ca_certificate' in ethernet['eapol']: +        for ca_cert in ethernet['eapol']['ca_certificate']: +            verify_pki_ca_certificate(ethernet, ca_cert)  def verify(ethernet):      if 'deleted' in ethernet: diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py index 2a0acd84a..694a4e1ea 100755 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ b/src/conf_mode/load-balancing_reverse-proxy.py @@ -20,6 +20,9 @@ from sys import exit  from shutil import rmtree  from vyos.config import Config +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate +from vyos.utils.dict import dict_search  from vyos.utils.process import call  from vyos.utils.network import check_port_availability  from vyos.utils.network import is_listen_port_bind_service @@ -33,8 +36,7 @@ airbag.enable()  load_balancing_dir = '/run/haproxy'  load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'  systemd_service = 'haproxy.service' -systemd_override = r'/run/systemd/system/haproxy.service.d/10-override.conf' - +systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf'  def get_config(config=None):      if config: @@ -54,30 +56,6 @@ def get_config(config=None):      return lb - -def _verify_cert(lb: dict, config: dict) -> None: -    if 'ca_certificate' in config['ssl']: -        ca_name = config['ssl']['ca_certificate'] -        pki_ca = lb['pki'].get('ca') -        if pki_ca is None: -            raise ConfigError(f'CA certificates does not exist in PKI') -        else: -            ca = pki_ca.get(ca_name) -            if ca is None: -                raise ConfigError(f'CA certificate "{ca_name}" does not exist') - -    elif 'certificate' in config['ssl']: -        cert_names = config['ssl']['certificate'] -        pki_certs = lb['pki'].get('certificate') -        if pki_certs is None: -            raise ConfigError(f'Certificates does not exist in PKI') - -        for cert_name in cert_names: -            pki_cert = pki_certs.get(cert_name) -            if pki_cert is None: -                raise ConfigError(f'Certificate "{cert_name}" does not exist') - -  def verify(lb):      if not lb:          return None @@ -107,12 +85,12 @@ def verify(lb):                  raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"')      for front, front_config in lb['service'].items(): -        if 'ssl' in front_config: -            _verify_cert(lb, front_config) +        for cert in dict_search('ssl.certificate', front_config) or []: +            verify_pki_certificate(lb, cert)      for back, back_config in lb['backend'].items(): -        if 'ssl' in back_config: -            _verify_cert(lb, back_config) +        tmp = dict_search('ssl.ca_certificate', front_config) +        if tmp: verify_pki_ca_certificate(lb, tmp)  def generate(lb): diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py index 46efc3c93..9e58b4c72 100755 --- a/src/conf_mode/service_https.py +++ b/src/conf_mode/service_https.py @@ -24,13 +24,14 @@ from time import sleep  from vyos.base import Warning  from vyos.config import Config  from vyos.config import config_dict_merge -from vyos.configdiff import get_config_diff  from vyos.configverify import verify_vrf +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate +from vyos.configverify import verify_pki_dh_parameters  from vyos.defaults import api_config_state  from vyos.pki import wrap_certificate  from vyos.pki import wrap_private_key  from vyos.pki import wrap_dh_parameters -from vyos.pki import load_dh_parameters  from vyos.template import render  from vyos.utils.dict import dict_search  from vyos.utils.process import call @@ -84,33 +85,14 @@ def verify(https):      if https is None:          return None -    if 'certificates' in https and 'certificate' in https['certificates']: -        cert_name = https['certificates']['certificate'] -        if 'pki' not in https: -            raise ConfigError('PKI is not configured!') - -        if cert_name not in https['pki']['certificate']: -            raise ConfigError('Invalid certificate in configuration!') +    if dict_search('certificates.certificate', https) != None: +        verify_pki_certificate(https, https['certificates']['certificate']) -        pki_cert = https['pki']['certificate'][cert_name] - -        if 'certificate' not in pki_cert: -            raise ConfigError('Missing certificate in configuration!') +        tmp = dict_search('certificates.ca_certificate', https) +        if tmp != None: verify_pki_ca_certificate(https, tmp) -        if 'private' not in pki_cert or 'key' not in pki_cert['private']: -            raise ConfigError('Missing certificate private key in configuration!') - -        if 'dh_params' in https['certificates']: -            dh_name = https['certificates']['dh_params'] -            if dh_name not in https['pki']['dh']: -                raise ConfigError('Invalid DH parameter in configuration!') - -            pki_dh = https['pki']['dh'][dh_name] -            dh_params = load_dh_parameters(pki_dh['parameters']) -            dh_numbers = dh_params.parameter_numbers() -            dh_bits = dh_numbers.p.bit_length() -            if dh_bits < 2048: -                raise ConfigError(f'Minimum DH key-size is 2048 bits') +        tmp = dict_search('certificates.dh_params', https) +        if tmp != None: verify_pki_dh_parameters(https, tmp, 2048)      else:          Warning('No certificate specified, using build-in self-signed certificates. '\ @@ -214,7 +196,8 @@ def apply(https):      https_service_name = 'nginx.service'      if https is None: -        call(f'systemctl stop {http_api_service_name}') +        if is_systemd_service_active(http_api_service_name): +            call(f'systemctl stop {http_api_service_name}')          call(f'systemctl stop {https_service_name}')          return diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 08e4fc6db..8159fedea 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-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 @@ -19,6 +19,8 @@ from sys import exit  from vyos.base import Warning  from vyos.config import Config +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate  from vyos.pki import wrap_certificate  from vyos.pki import wrap_private_key  from vyos.template import render @@ -75,7 +77,7 @@ def verify(ocserv):      if "accounting" in ocserv:          if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]:              if not origin["accounting"]['radius']['server']: -                raise ConfigError('Openconnect accounting mode radius requires at least one RADIUS server') +                raise ConfigError('OpenConnect accounting mode radius requires at least one RADIUS server')              if "authentication" not in ocserv or "mode" not in ocserv["authentication"]:                  raise ConfigError('Accounting depends on OpenConnect authentication configuration')              elif "radius" not in ocserv["authentication"]["mode"]: @@ -89,12 +91,12 @@ def verify(ocserv):                      raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration')              if "radius" in ocserv["authentication"]["mode"]:                  if not ocserv["authentication"]['radius']['server']: -                    raise ConfigError('Openconnect authentication mode radius requires at least one RADIUS server') +                    raise ConfigError('OpenConnect authentication mode radius requires at least one RADIUS server')              if "local" in ocserv["authentication"]["mode"]:                  if not ocserv.get("authentication", {}).get("local_users"): -                    raise ConfigError('openconnect mode local required at least one user') +                    raise ConfigError('OpenConnect mode local required at least one user')                  if not ocserv["authentication"]["local_users"]["username"]: -                    raise ConfigError('openconnect mode local required at least one user') +                    raise ConfigError('OpenConnect mode local required at least one user')                  else:                      # For OTP mode: verify that each local user has an OTP key                      if "otp" in ocserv["authentication"]["mode"]["local"]: @@ -127,40 +129,20 @@ def verify(ocserv):                      if 'default_config' not in ocserv["authentication"]["identity_based_config"]:                          raise ConfigError('OpenConnect identity-based-config enabled but default-config not set')          else: -            raise ConfigError('openconnect authentication mode required') +            raise ConfigError('OpenConnect authentication mode required')      else: -        raise ConfigError('openconnect authentication credentials required') +        raise ConfigError('OpenConnect authentication credentials required')      # Check ssl      if 'ssl' not in ocserv: -        raise ConfigError('openconnect ssl required') +        raise ConfigError('SSL missing on OpenConnect config!') -    if not ocserv['pki'] or 'certificate' not in ocserv['pki']: -        raise ConfigError('PKI not configured') +    if 'certificate' not in ocserv['ssl']: +        raise ConfigError('SSL certificate missing on OpenConnect config!') +    verify_pki_certificate(ocserv, ocserv['ssl']['certificate']) -    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') +    if 'ca_certificate' in ocserv['ssl']: +        verify_pki_ca_certificate(ocserv, ocserv['ssl']['ca_certificate'])      # Check network settings      if "network_settings" in ocserv: @@ -172,7 +154,7 @@ def verify(ocserv):          else:              ocserv["network_settings"]["push_route"] = ["default"]      else: -        raise ConfigError('openconnect network settings required') +        raise ConfigError('OpenConnect network settings required!')  def generate(ocserv):      if not ocserv: @@ -276,7 +258,7 @@ def apply(ocserv):                  break              sleep(0.250)              if counter > 5: -                raise ConfigError('openconnect failed to start, check the logs for details') +                raise ConfigError('OpenConnect failed to start, check the logs for details')                  break              counter += 1 diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 8661a8aff..7490fd0e0 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -20,6 +20,8 @@ from sys import exit  from vyos.config import Config  from vyos.configdict import get_accel_dict +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate  from vyos.pki import wrap_certificate  from vyos.pki import wrap_private_key  from vyos.template import render @@ -46,51 +48,6 @@ cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key')  ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem') -def verify_certificate(config): -    # -    # SSL certificate checks -    # -    if not config['pki']: -        raise ConfigError('PKI is not configured') - -    if 'ssl' not in config: -        raise ConfigError('SSL missing on SSTP config') - -    ssl = config['ssl'] - -    # CA -    if 'ca_certificate' not in ssl: -        raise ConfigError('SSL CA certificate missing on SSTP config') - -    ca_name = ssl['ca_certificate'] - -    if ca_name not in config['pki']['ca']: -        raise ConfigError('Invalid CA certificate on SSTP config') - -    if 'certificate' not in config['pki']['ca'][ca_name]: -        raise ConfigError('Missing certificate data for CA certificate on SSTP config') - -    # Certificate -    if 'certificate' not in ssl: -        raise ConfigError('SSL certificate missing on SSTP config') - -    cert_name = ssl['certificate'] - -    if cert_name not in config['pki']['certificate']: -        raise ConfigError('Invalid certificate on SSTP config') - -    pki_cert = config['pki']['certificate'][cert_name] - -    if 'certificate' not in pki_cert: -        raise ConfigError('Missing certificate data for certificate on SSTP config') - -    if 'private' not in pki_cert or 'key' not in pki_cert['private']: -        raise ConfigError('Missing private key for certificate on SSTP config') - -    if 'password_protected' in pki_cert['private']: -        raise ConfigError('Encrypted private key is not supported on SSTP config') - -  def get_config(config=None):      if config:          conf = config @@ -124,7 +81,17 @@ def verify(sstp):      verify_accel_ppp_ip_pool(sstp)      verify_accel_ppp_name_servers(sstp)      verify_accel_ppp_wins_servers(sstp) -    verify_certificate(sstp) + +    if 'ssl' not in sstp: +        raise ConfigError('SSL missing on SSTP config!') + +    if 'certificate' not in sstp['ssl']: +        raise ConfigError('SSL certificate missing on SSTP config!') +    verify_pki_certificate(sstp, sstp['ssl']['certificate']) + +    if 'ca_certificate' not in sstp['ssl']: +        raise ConfigError('SSL CA certificate missing on SSTP config!') +    verify_pki_ca_certificate(sstp, sstp['ssl']['ca_certificate'])  def generate(sstp): @@ -154,15 +121,15 @@ def generate(sstp):  def apply(sstp): +    systemd_service = 'accel-ppp@sstp.service'      if not sstp: -        call('systemctl stop accel-ppp@sstp.service') +        call(f'systemctl stop {systemd_service}')          for file in [sstp_chap_secrets, sstp_conf]:              if os.path.exists(file):                  os.unlink(file) -          return None -    call('systemctl restart accel-ppp@sstp.service') +    call(f'systemctl reload-or-restart {systemd_service}')  if __name__ == '__main__':  | 
