diff options
author | sarthurdev <965089+sarthurdev@users.noreply.github.com> | 2021-06-29 11:06:44 +0200 |
---|---|---|
committer | sarthurdev <965089+sarthurdev@users.noreply.github.com> | 2021-06-29 21:26:44 +0200 |
commit | f5a8a9cdfe52c331177c8bc7b8fb84fc08d4f60a (patch) | |
tree | 932113197018d823ca83e092139c8a06f89e3b99 | |
parent | 6f66e71e4622c54058b8689d4be730905d69fe22 (diff) | |
download | vyos-1x-f5a8a9cdfe52c331177c8bc7b8fb84fc08d4f60a.tar.gz vyos-1x-f5a8a9cdfe52c331177c8bc7b8fb84fc08d4f60a.zip |
pki: ipsec: T3642: Migrate IPSec to use PKI configuration
-rw-r--r-- | data/templates/ipsec/swanctl.conf.tmpl | 6 | ||||
-rw-r--r-- | data/templates/ipsec/swanctl/peer.tmpl | 2 | ||||
-rw-r--r-- | interface-definitions/include/pki/certificate-key.xml.i | 15 | ||||
-rw-r--r-- | interface-definitions/include/pki/dh-parameters.xml.i | 14 | ||||
-rw-r--r-- | interface-definitions/include/pki/openvpn_tls-auth.xml.i | 14 | ||||
-rw-r--r-- | interface-definitions/pki.xml.in | 8 | ||||
-rw-r--r-- | interface-definitions/vpn_ipsec.xml.in | 38 | ||||
-rw-r--r-- | op-mode-definitions/pki.xml.in | 6 | ||||
-rw-r--r-- | smoketest/configs/pki-ipsec | 95 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 89 | ||||
-rwxr-xr-x | src/migration-scripts/ipsec/6-to-7 | 137 | ||||
-rwxr-xr-x | src/op_mode/pki.py | 4 |
12 files changed, 310 insertions, 118 deletions
diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl index ea6d85743..9e629b176 100644 --- a/data/templates/ipsec/swanctl.conf.tmpl +++ b/data/templates/ipsec/swanctl.conf.tmpl @@ -55,9 +55,9 @@ secrets { } {% elif peer_conf.authentication.mode == 'x509' %} private_{{ peer_conn_name }} { - file = {{ peer_conf.authentication.x509.key.file }} -{% if "password" in peer_conf.authentication.x509.key and peer_conf.authentication.x509.key.password %} - secret = "{{ peer_conf.authentication.x509.key.password}}" + file = {{ peer_conf.authentication.x509.certificate }}.pem +{% if peer_conf.authentication.x509.passphrase is defined %} + secret = "{{ peer_conf.authentication.x509.passphrase }}" {% endif %} } {% elif peer_conf.authentication.mode == 'rsa' and not ns.local_key_set %} diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl index 0d01cd546..36cb1abfb 100644 --- a/data/templates/ipsec/swanctl/peer.tmpl +++ b/data/templates/ipsec/swanctl/peer.tmpl @@ -35,7 +35,7 @@ auth = {{ auth_type }} {% endif %} {% if peer_conf.authentication.mode == 'x509' %} - certs = {{ peer_conf.authentication.x509.cert_file }} + certs = {{ peer_conf.authentication.x509.certificate }}.pem {% elif peer_conf.authentication.mode == 'rsa' %} pubkeys = localhost.pub {% endif %} diff --git a/interface-definitions/include/pki/certificate-key.xml.i b/interface-definitions/include/pki/certificate-key.xml.i index b68f38442..7f26d25c1 100644 --- a/interface-definitions/include/pki/certificate-key.xml.i +++ b/interface-definitions/include/pki/certificate-key.xml.i @@ -1,17 +1,6 @@ <!-- include start from pki/certificate-key.xml.i --> -<leafNode name="certificate"> - <properties> - <help>Certificate and private key in PKI configuration</help> - <valueHelp> - <format>cert name</format> - <description>Name of certificate in PKI configuration</description> - </valueHelp> - <completionHelp> - <path>pki certificate</path> - </completionHelp> - </properties> -</leafNode> -<leafNode name="private-key-passphrase"> +#include <include/pki/certificate.xml.i> +<leafNode name="passphrase"> <properties> <help>Private key passphrase</help> <valueHelp> diff --git a/interface-definitions/include/pki/dh-parameters.xml.i b/interface-definitions/include/pki/dh-parameters.xml.i deleted file mode 100644 index 6e69528e7..000000000 --- a/interface-definitions/include/pki/dh-parameters.xml.i +++ /dev/null @@ -1,14 +0,0 @@ -<!-- include start from pki/dh-parameters.xml.i --> -<leafNode name="dh-parameters"> - <properties> - <help>Diffie-Hellman parameters in PKI configuration</help> - <valueHelp> - <format>DH name</format> - <description>Name of DH params in PKI configuration</description> - </valueHelp> - <completionHelp> - <path>pki dh</path> - </completionHelp> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/pki/openvpn_tls-auth.xml.i b/interface-definitions/include/pki/openvpn_tls-auth.xml.i deleted file mode 100644 index 2b9a69653..000000000 --- a/interface-definitions/include/pki/openvpn_tls-auth.xml.i +++ /dev/null @@ -1,14 +0,0 @@ -<!-- include start from pki/openvpn_tls-auth.xml.i --> -<leafNode name="auth-key"> - <properties> - <help>Static key for tls-auth in PKI configuration</help> - <valueHelp> - <format>key name</format> - <description>Name of static key in PKI configuration</description> - </valueHelp> - <completionHelp> - <path>pki openvpn tls-auth</path> - </completionHelp> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in index e818ae438..4b082cbc4 100644 --- a/interface-definitions/pki.xml.in +++ b/interface-definitions/pki.xml.in @@ -141,19 +141,19 @@ <help>OpenVPN keys</help> </properties> <children> - <tagNode name="tls-auth"> + <tagNode name="shared-secret"> <properties> - <help>OpenVPN TLS auth key</help> + <help>OpenVPN shared secret key</help> </properties> <children> <leafNode name="key"> <properties> - <help>OpenVPN TLS auth key data</help> + <help>OpenVPN shared secret key data</help> </properties> </leafNode> <leafNode name="version"> <properties> - <help>OpenVPN TLS auth key version</help> + <help>OpenVPN shared secret key version</help> </properties> </leafNode> </children> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 2031217ba..7b1b3a595 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -804,42 +804,8 @@ <help>X.509 certificate</help> </properties> <children> - #include <include/certificate.xml.i> - #include <include/certificate-ca.xml.i> - <leafNode name="crl-file"> - <properties> - <help>File containing the X.509 Certificate Revocation List (CRL)</help> - <valueHelp> - <format>txt</format> - <description>File in /config/auth</description> - </valueHelp> - </properties> - </leafNode> - <node name="key"> - <properties> - <help>Key file and password to open it</help> - </properties> - <children> - <leafNode name="file"> - <properties> - <help>File containing the private key for the X.509 certificate for this host</help> - <valueHelp> - <format>txt</format> - <description>File in /config/auth</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="password"> - <properties> - <help>Password that protects the private key</help> - <valueHelp> - <format>txt</format> - <description>Password that protects the private key</description> - </valueHelp> - </properties> - </leafNode> - </children> - </node> + #include <include/pki/certificate-key.xml.i> + #include <include/pki/ca-certificate.xml.i> </children> </node> </children> diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in index 0cea3db08..06b15eed4 100644 --- a/op-mode-definitions/pki.xml.in +++ b/op-mode-definitions/pki.xml.in @@ -134,14 +134,14 @@ <help>Generate OpenVPN keys</help> </properties> <children> - <node name="tls-auth"> + <node name="shared-secret"> <properties> - <help>Generate OpenVPN TLS key</help> + <help>Generate OpenVPN shared secret key</help> </properties> <children> <tagNode name="install"> <properties> - <help>Commands for installing generated OpenVPN TLS key into running configuration</help> + <help>Commands for installing generated OpenVPN shared secret key into running configuration</help> <completionHelp> <list><key name></list> </completionHelp> diff --git a/smoketest/configs/pki-ipsec b/smoketest/configs/pki-ipsec new file mode 100644 index 000000000..7708a3cdd --- /dev/null +++ b/smoketest/configs/pki-ipsec @@ -0,0 +1,95 @@ +interfaces { + dummy dum0 { + address 172.20.0.1/30 + } + 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 { + ipsec { + esp-group MyESPGroup { + proposal 1 { + encryption aes128 + hash sha1 + } + } + ike-group MyIKEGroup { + proposal 1 { + dh-group 2 + encryption aes128 + hash sha1 + } + } + ipsec-interfaces { + interface eth0 + } + site-to-site { + peer 192.168.150.2 { + authentication { + mode x509 + x509 { + ca-cert-file ovpn_test_ca.pem + cert-file ovpn_test_server.pem + key { + file ovpn_test_server.key + } + } + } + default-esp-group MyESPGroup + ike-group MyIKEGroup + local-address 192.168.150.1 + tunnel 0 { + local { + prefix 172.20.0.0/24 + } + remote { + prefix 172.21.0.0/24 + } + } + } + } + } +} + + +// 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/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index d598ff6da..e8e8b453a 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -23,6 +23,10 @@ from vyos.config import Config from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists from vyos.ifconfig import Interface +from vyos.pki import wrap_certificate +from vyos.pki import wrap_crl +from vyos.pki import wrap_public_key +from vyos.pki import wrap_private_key from vyos.template import ip_from_cidr from vyos.template import render from vyos.validate import is_ipv6_link_local @@ -115,6 +119,8 @@ def get_config(config=None): ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', 'interface']) ipsec['l2tp_exists'] = conf.exists(['vpn', 'l2tp', 'remote-access', 'ipsec-settings']) ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel']) + ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) ipsec['rsa_keys'] = conf.get_config_dict(['vpn', 'rsa-keys'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) @@ -187,6 +193,24 @@ def get_dhcp_address(iface): return ip_from_cidr(address) return None +def verify_pki(pki, x509_conf): + if not pki or 'ca' not in pki or 'certificate' not in pki: + raise ConfigError(f'PKI is not configured') + + ca_cert_name = x509_conf['ca_certificate'] + cert_name = x509_conf['certificate'] + + if not dict_search(f'ca.{ca_cert_name}.certificate', ipsec['pki']): + raise ConfigError(f'Missing CA certificate on specified PKI CA certificate "{ca_cert_name}"') + + if not dict_search(f'certificate.{cert_name}.certificate', ipsec['pki']): + raise ConfigError(f'Missing certificate on specified PKI certificate "{cert_name}"') + + if not dict_search(f'certificate.{cert_name}.private.key', ipsec['pki']): + raise ConfigError(f'Missing private key on specified PKI certificate "{cert_name}"') + + return True + def verify(ipsec): if not ipsec: return None @@ -237,24 +261,12 @@ def verify(ipsec): if 'x509' not in peer_conf['authentication']: raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}") - if 'key' not in peer_conf['authentication']['x509']: - raise ConfigError(f"Missing x509 key on site-to-site peer {peer}") - - if 'ca_cert_file' not in peer_conf['authentication']['x509'] or 'cert_file' not in peer_conf['authentication']['x509']: - raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}") + x509 = peer_conf['authentication']['x509'] - if 'file' not in peer_conf['authentication']['x509']['key']: - raise ConfigError(f"Missing x509 key file on site-to-site peer {peer}") + if 'ca_certificate' not in x509 or 'certificate' not in x509: + raise ConfigError(f"Missing x509 certificates on site-to-site peer {peer}") - for key in ['ca_cert_file', 'cert_file', 'crl_file']: - if key in peer_conf['authentication']['x509']: - path = os.path.join(X509_PATH, peer_conf['authentication']['x509'][key]) - if not os.path.exists(path): - raise ConfigError(f"File not found for {key} on site-to-site peer {peer}") - - key_path = os.path.join(X509_PATH, peer_conf['authentication']['x509']['key']['file']) - if not os.path.exists(key_path): - raise ConfigError(f"Private key not found on site-to-site peer {peer}") + verify_pki(ipsec['pki'], x509) if peer_conf['authentication']['mode'] == 'rsa': if not verify_rsa_local_key(ipsec): @@ -320,6 +332,31 @@ def verify(ipsec): if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']): raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") +def generate_pki_files(pki, x509_conf): + ca_cert_name = x509_conf['ca_certificate'] + ca_cert_data = dict_search(f'ca.{ca_cert_name}.certificate', pki) + ca_cert_crls = dict_search(f'ca.{ca_cert_name}.crl', pki) or [] + crl_index = 1 + + cert_name = x509_conf['certificate'] + cert_data = dict_search(f'certificate.{cert_name}.certificate', pki) + key_data = dict_search(f'certificate.{cert_name}.private.key', pki) + protected = 'passphrase' in x509_conf + + with open(os.path.join(CA_PATH, f'{ca_cert_name}.pem'), 'w') as f: + f.write(wrap_certificate(ca_cert_data)) + + for crl in ca_cert_crls: + with open(os.path.join(CRL_PATH, f'{ca_cert_name}_{crl_index}.pem'), 'w') as f: + f.write(wrap_crl(crl)) + crl_index += 1 + + with open(os.path.join(CERT_PATH, f'{cert_name}.pem'), 'w') as f: + f.write(wrap_certificate(cert_data)) + + with open(os.path.join(KEY_PATH, f'{cert_name}.pem'), 'w') as f: + f.write(wrap_private_key(key_data, protected)) + def generate(ipsec): data = {} @@ -334,24 +371,20 @@ def generate(ipsec): data['marks'] = {} data['rsa_local_key'] = verify_rsa_local_key(ipsec) + for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH]: + if not os.path.exists(path): + os.mkdir(path, mode=0o755) + + if not os.path.exists(KEY_PATH): + os.mkdir(KEY_PATH, mode=0o700) + if 'site_to_site' in data and 'peer' in data['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): if peer in ipsec['dhcp_no_address']: continue if peer_conf['authentication']['mode'] == 'x509': - cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['cert_file']) - copy_file(cert_file, CERT_PATH, True) - - key_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['key']['file']) - copy_file(key_file, X509_PATH, True) - - ca_cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['ca_cert_file']) - copy_file(ca_cert_file, CA_PATH, True) - - if 'crl_file' in peer_conf['authentication']['x509']: - crl_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['crl_file']) - copy_file(crl_file, CRL_PATH, True) + generate_pki_files(ipsec['pki'], peer_conf['authentication']['x509']) local_ip = '' if 'local_address' in peer_conf: diff --git a/src/migration-scripts/ipsec/6-to-7 b/src/migration-scripts/ipsec/6-to-7 new file mode 100755 index 000000000..6655fba93 --- /dev/null +++ b/src/migration-scripts/ipsec/6-to-7 @@ -0,0 +1,137 @@ +#!/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 <http://www.gnu.org/licenses/>. + +# Migrate /config/auth certificates and keys into 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 + +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() + +pki_base = ['pki'] +ipsec_site_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] + +config = ConfigTree(config_file) +changes_made = False + +AUTH_DIR = '/config/auth' + +def wrapped_pem_to_config_value(pem): + return "".join(pem.strip().split("\n")[1:-1]) + +if config.exists(ipsec_site_base): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + + for peer in config.list_nodes(ipsec_site_base): + if not config.exists(ipsec_site_base + [peer, 'authentication', 'x509']): + continue + + changes_made = True + + peer_x509_base = ipsec_site_base + [peer, 'authentication', 'x509'] + pki_name = 'peer_' + peer.replace(".", "-") + + if config.exists(peer_x509_base + ['cert-file']): + cert_file = config.return_value(peer_x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(peer_x509_base + ['certificate'], value=pki_name) + config.delete(peer_x509_base + ['cert-file']) + + if config.exists(peer_x509_base + ['ca-cert-file']): + ca_cert_file = config.return_value(peer_x509_base + ['ca-cert-file']) + ca_cert_path = os.path.join(AUTH_DIR, ca_cert_file) + ca_cert = None + + with open(ca_cert_path, 'r') as f: + ca_cert_data = f.read() + ca_cert = load_certificate(ca_cert_data, wrap_tags=False) + + ca_cert_pem = encode_certificate(ca_cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(ca_cert_pem)) + config.set(peer_x509_base + ['ca-certificate'], value=pki_name) + config.delete(peer_x509_base + ['ca-cert-file']) + + if config.exists(peer_x509_base + ['crl-file']): + crl_file = config.return_value(peer_x509_base + ['crl-file']) + crl_path = os.path.join(AUTH_DIR, crl_file) + crl = None + + with open(crl_path, 'r') as f: + crl_data = f.read() + crl = load_crl(crl_data, wrap_tags=False) + + crl_pem = encode_certificate(crl) + config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + config.delete(peer_x509_base + ['crl-file']) + + if config.exists(peer_x509_base + ['key', 'file']): + key_file = config.return_value(peer_x509_base + ['key', 'file']) + key_passphrase = None + + if config.exists(peer_x509_base + ['key', 'password']): + key_passphrase = config.return_value(peer_x509_base + ['key', 'password']) + + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=key_passphrase, wrap_tags=False) + + key_pem = encode_private_key(key, passphrase=key_passphrase) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + + if key_passphrase: + config.set(pki_base + ['certificate', pki_name, 'private', 'password-protected']) + config.set(peer_x509_base + ['private-key-passphrase'], value=key_passphrase) + + config.delete(peer_x509_base + ['key']) + +if changes_made: + 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)) + sys.exit(1) diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 321a5e60d..d99a432aa 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -473,8 +473,8 @@ def generate_openvpn_key(name, install=False): key_version = version_search[1] print("Configure mode commands to install OpenVPN key:") - print("set pki openvpn tls-auth %s key '%s'" % (name, key_data)) - print("set pki openvpn tls-auth %s version '%s'" % (name, key_version)) + print("set pki openvpn shared-secret %s key '%s'" % (name, key_data)) + print("set pki openvpn shared-secret %s version '%s'" % (name, key_version)) def generate_wireguard_key(name, install=False): private_key = cmd('wg genkey') |