diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2 index 80239ea56..a530c14ba 100644 --- a/data/templates/https/nginx.default.j2 +++ b/data/templates/https/nginx.default.j2 @@ -1,60 +1,60 @@ ### Autogenerated by service_https.py ### # Default server configuration {% for server in server_block_list %} server { # SSL configuration # {% if server.address == '*' %} listen {{ server.port }} ssl; listen [::]:{{ server.port }} ssl; {% else %} listen {{ server.address | bracketize_ipv6 }}:{{ server.port }} ssl; {% endif %} {% for name in server.name %} server_name {{ name }}; {% endfor %} root /srv/localui; -{% if server.certbot %} - ssl_certificate {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/fullchain.pem; - ssl_certificate_key {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/privkey.pem; - include {{ server.certbot_dir }}/options-ssl-nginx.conf; - ssl_dhparam {{ server.certbot_dir }}/ssl-dhparams.pem; -{% elif server.vyos_cert %} +{% if server.vyos_cert %} ssl_certificate {{ server.vyos_cert.crt }}; ssl_certificate_key {{ server.vyos_cert.key }}; {% else %} # # Self signed certs generated by the ssl-cert package # Don't use them in a production server! # include snippets/snakeoil.conf; {% endif %} + ssl_session_cache shared:le_nginx_SSL:10m; + ssl_session_timeout 1440m; + ssl_session_tickets off; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; # proxy settings for HTTP API, if enabled; 503, if not location ~ ^/(retrieve|configure|config-file|image|container-image|generate|show|reboot|reset|poweroff|docs|openapi.json|redoc|graphql) { {% if server.api %} proxy_pass http://unix:/run/api.sock; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 600; proxy_buffering off; {% else %} return 503; {% endif %} {% if server.allow_client %} {% for client in server.allow_client %} allow {{ client }}; {% endfor %} deny all; {% endif %} } error_page 497 =301 https://$host:{{ server.port }}$request_uri; } {% endfor %} diff --git a/debian/control b/debian/control index 3f1ba1a63..871a1f0f7 100644 --- a/debian/control +++ b/debian/control @@ -1,212 +1,212 @@ Source: vyos-1x Section: contrib/net Priority: extra Maintainer: VyOS Package Maintainers <maintainers@vyos.net> Build-Depends: debhelper (>= 9), dh-python, fakeroot, gcc, iproute2, libvyosconfig0 (>= 0.0.7), libzmq3-dev, python3 (>= 3.10), python3-coverage, python3-lxml, python3-netifaces, python3-nose, python3-jinja2, python3-psutil, python3-setuptools, python3-sphinx, python3-xmltodict, quilt, whois Standards-Version: 3.9.6 Package: vyos-1x Architecture: amd64 arm64 Pre-Depends: libnss-tacplus [amd64], libpam-tacplus [amd64], libpam-radius-auth [amd64] Depends: ${python3:Depends} (>= 3.10), aardvark-dns, accel-ppp, auditd, avahi-daemon, beep, bmon, bsdmainutils, + certbot, charon-systemd, conntrack, conntrackd, conserver-client, conserver-server, console-data, cron, curl, dbus, ddclient (>= 3.11.1), dmidecode, dropbear, easy-rsa, etherwake, ethtool, fdisk, fastnetmon [amd64], file, frr (>= 7.5), frr-pythontools, frr-rpki-rtrlib, frr-snmp, fuse-overlayfs, libpam-google-authenticator, git, grc, haproxy, hostapd, hsflowd, hvinfo, igmpproxy, ipaddrcheck, iperf, iperf3, iproute2 (>= 6.0.0), iptables, iputils-arping, isc-dhcp-client, isc-dhcp-relay, isc-dhcp-server, iw, jool, keepalived (>=2.0.5), lcdproc, lcdproc-extra-drivers, libatomic1, libauparse0, libcharon-extra-plugins (>=5.9), libcharon-extauth-plugins (>=5.9), libndp-tools, libnetfilter-conntrack3, libnfnetlink0, libqmi-utils, libstrongswan-extra-plugins (>=5.9), libstrongswan-standard-plugins (>=5.9), libvyosconfig0, linux-cpupower, lldpd, lm-sensors, lsscsi, minisign, modemmanager, mtr-tiny, ndisc6, ndppd, netavark, netplug, nfct, nftables (>= 0.9.3), nginx-light, chrony, nvme-cli, ocserv, opennhrp, openssh-server, openssl, openvpn, openvpn-auth-ldap, openvpn-auth-radius, openvpn-otp, owamp-client, owamp-server, pciutils, pdns-recursor, pmacct (>= 1.6.0), podman, pppoe, procps, python3, - python3-certbot-nginx, python3-cryptography, python3-hurry.filesize, python3-inotify, python3-isc-dhcp-leases, python3-jinja2, python3-jmespath, python3-netaddr, python3-netifaces, python3-paramiko, python3-passlib, python3-psutil, python3-pyhumps, python3-pystache, python3-pyudev, python3-six, python3-tabulate, python3-vici (>= 5.7.2), python3-voluptuous, python3-xmltodict, python3-zmq, qrencode, radvd, salt-minion, sed, smartmontools, snmp, snmpd, squashfs-tools, squid, squidclient, squidguard, sshguard, ssl-cert, sstp-client, strongswan (>= 5.9), strongswan-swanctl (>= 5.9), stunnel4, sudo, systemd, telegraf (>= 1.20), tcpdump, tcptraceroute, telnet, tftpd-hpa, traceroute, tuned, twamp-client, twamp-server, udp-broadcast-relay, uidmap, usb-modeswitch, usbutils, vyatta-bash, vyatta-cfg, vyos-http-api-tools, vyos-utils, wide-dhcpv6-client, wireguard-tools, wireless-regdb, wpasupplicant (>= 0.6.7), zabbix-agent2, ndppd, miniupnpd-nftables Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything Package: vyos-1x-vmware Architecture: amd64 Depends: vyos-1x, open-vm-tools Description: VyOS configuration scripts and data for VMware Adds configuration files required for VyOS running on VMware hosts. Package: vyos-1x-smoketest Architecture: all Depends: skopeo, snmp, vyos-1x Description: VyOS build sanity checking toolkit diff --git a/interface-definitions/include/constraint/email.xml.i b/interface-definitions/include/constraint/email.xml.i new file mode 100644 index 000000000..b19a88d64 --- /dev/null +++ b/interface-definitions/include/constraint/email.xml.i @@ -0,0 +1,3 @@ +<!-- include start from constraint/email.xml.i --> +<regex>[^\s@]+@([^\s@.,]+\.)+[^\s@.,]{2,}</regex> +<!-- include end --> diff --git a/interface-definitions/include/version/https-version.xml.i b/interface-definitions/include/version/https-version.xml.i index fa18278f3..525314dbd 100644 --- a/interface-definitions/include/version/https-version.xml.i +++ b/interface-definitions/include/version/https-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/https-version.xml.i --> -<syntaxVersion component='https' version='5'></syntaxVersion> +<syntaxVersion component='https' version='6'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in index 097c541ac..0ed199539 100644 --- a/interface-definitions/pki.xml.in +++ b/interface-definitions/pki.xml.in @@ -1,243 +1,297 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="pki" owner="${vyos_conf_scripts_dir}/pki.py"> <properties> <help>VyOS PKI configuration</help> <priority>300</priority> </properties> <children> <tagNode name="ca"> <properties> <help>Certificate Authority</help> <constraint> #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> </constraint> </properties> <children> <leafNode name="certificate"> <properties> <help>CA certificate in PEM format</help> <constraint> <validator name="base64"/> </constraint> <constraintErrorMessage>CA certificate is not base64-encoded</constraintErrorMessage> </properties> </leafNode> #include <include/generic-description.xml.i> <node name="private"> <properties> <help>CA private key in PEM format</help> </properties> <children> <leafNode name="key"> <properties> <help>CA private key in PEM format</help> <constraint> <validator name="base64"/> </constraint> <constraintErrorMessage>CA private key is not base64-encoded</constraintErrorMessage> </properties> </leafNode> <leafNode name="password-protected"> <properties> <help>CA private key is password protected</help> <valueless/> </properties> </leafNode> </children> </node> <leafNode name="crl"> <properties> <help>Certificate revocation list in PEM format</help> <constraint> <validator name="base64"/> </constraint> <constraintErrorMessage>CRL is not base64-encoded</constraintErrorMessage> <multi/> </properties> </leafNode> <leafNode name="revoke"> <properties> <help>If parent CA is present, this CA certificate will be included in generated CRLs</help> <valueless/> </properties> </leafNode> </children> </tagNode> <tagNode name="certificate"> <properties> <help>Certificate</help> <constraint> #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> </constraint> </properties> <children> <leafNode name="certificate"> <properties> <help>Certificate in PEM format</help> <constraint> <validator name="base64"/> </constraint> <constraintErrorMessage>Certificate is not base64-encoded</constraintErrorMessage> </properties> </leafNode> + <node name="acme"> + <properties> + <help>Automatic Certificate Management Environment (ACME) request</help> + </properties> + <children> + #include <include/url-http-https.xml.i> + <leafNode name="url"> + <defaultValue>https://acme-v02.api.letsencrypt.org/directory</defaultValue> + </leafNode> + <leafNode name="domain-name"> + <properties> + <help>Domain Name</help> + <constraint> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Invalid domain name (RFC 1123 section 2).\nMay only contain letters, numbers and .-_</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="email"> + <properties> + <help>Email address to associate with certificate</help> + <constraint> + #include <include/constraint/email.xml.i> + </constraint> + </properties> + </leafNode> + #include <include/listen-address-ipv4-single.xml.i> + <leafNode name="rsa-key-size"> + <properties> + <help>Size of the RSA key</help> + <completionHelp> + <list>2048 3072 4096</list> + </completionHelp> + <valueHelp> + <format>2048</format> + <description>RSA key length 2048 bit</description> + </valueHelp> + <valueHelp> + <format>3072</format> + <description>RSA key length 3072 bit</description> + </valueHelp> + <valueHelp> + <format>4096</format> + <description>RSA key length 4096 bit</description> + </valueHelp> + <constraint> + <regex>(2048|3072|4096)</regex> + </constraint> + </properties> + <defaultValue>2048</defaultValue> + </leafNode> + </children> + </node> #include <include/generic-description.xml.i> <node name="private"> <properties> <help>Certificate private key</help> </properties> <children> <leafNode name="key"> <properties> <help>Certificate private key in PEM format</help> <constraint> <validator name="base64"/> </constraint> <constraintErrorMessage>Certificate private key is not base64-encoded</constraintErrorMessage> </properties> </leafNode> <leafNode name="password-protected"> <properties> <help>Certificate private key is password protected</help> <valueless/> </properties> </leafNode> </children> </node> <leafNode name="revoke"> <properties> <help>If CA is present, this certificate will be included in generated CRLs</help> <valueless/> </properties> </leafNode> </children> </tagNode> <tagNode name="dh"> <properties> <help>Diffie-Hellman parameters</help> <constraint> #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> </constraint> </properties> <children> <leafNode name="parameters"> <properties> <help>DH parameters in PEM format</help> <constraint> <validator name="base64"/> </constraint> <constraintErrorMessage>DH parameters are not base64-encoded</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> <tagNode name="key-pair"> <properties> <help>Public and private keys</help> </properties> <children> <node name="public"> <properties> <help>Public key</help> </properties> <children> <leafNode name="key"> <properties> <help>Public key in PEM format</help> <constraint> <validator name="base64"/> </constraint> <constraintErrorMessage>Public key is not base64-encoded</constraintErrorMessage> </properties> </leafNode> </children> </node> <node name="private"> <properties> <help>Private key</help> </properties> <children> <leafNode name="key"> <properties> <help>Private key in PEM format</help> <constraint> <validator name="base64"/> </constraint> <constraintErrorMessage>Private key is not base64-encoded</constraintErrorMessage> </properties> </leafNode> <leafNode name="password-protected"> <properties> <help>Private key is password protected</help> <valueless/> </properties> </leafNode> </children> </node> </children> </tagNode> <node name="openvpn"> <properties> <help>OpenVPN keys</help> </properties> <children> <tagNode name="shared-secret"> <properties> <help>OpenVPN shared secret key</help> </properties> <children> <leafNode name="key"> <properties> <help>OpenVPN shared secret key data</help> </properties> </leafNode> <leafNode name="version"> <properties> <help>OpenVPN shared secret key version</help> </properties> </leafNode> </children> </tagNode> </children> </node> <node name="x509"> <properties> <help>X509 Settings</help> </properties> <children> <node name="default"> <properties> <help>X509 Default Values</help> </properties> <children> <leafNode name="country"> <properties> <help>Default country</help> </properties> <defaultValue>GB</defaultValue> </leafNode> <leafNode name="state"> <properties> <help>Default state</help> </properties> <defaultValue>Some-State</defaultValue> </leafNode> <leafNode name="locality"> <properties> <help>Default locality</help> </properties> <defaultValue>Some-City</defaultValue> </leafNode> <leafNode name="organization"> <properties> <help>Default organization</help> </properties> <defaultValue>VyOS</defaultValue> </leafNode> </children> </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/interface-definitions/service_https.xml.in b/interface-definitions/service_https.xml.in index 223f10962..57f36a982 100644 --- a/interface-definitions/service_https.xml.in +++ b/interface-definitions/service_https.xml.in @@ -1,220 +1,202 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="service"> <children> <node name="https" owner="${vyos_conf_scripts_dir}/service_https.py"> <properties> <help>HTTPS configuration</help> <priority>1001</priority> </properties> <children> <tagNode name="virtual-host"> <properties> <help>Identifier for virtual host</help> <constraint> <regex>[a-zA-Z0-9-_.:]{1,255}</regex> </constraint> <constraintErrorMessage>illegal characters in identifier or identifier longer than 255 characters</constraintErrorMessage> </properties> <children> <leafNode name="listen-address"> <properties> <help>Address to listen for HTTPS requests</help> <completionHelp> <script>${vyos_completion_dir}/list_local_ips.sh --both</script> </completionHelp> <valueHelp> <format>ipv4</format> <description>HTTPS IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>HTTPS IPv6 address</description> </valueHelp> <valueHelp> <format>'*'</format> <description>any</description> </valueHelp> <constraint> <validator name="ip-address"/> <regex>\*</regex> </constraint> </properties> </leafNode> #include <include/port-number.xml.i> <leafNode name='port'> <defaultValue>443</defaultValue> </leafNode> <leafNode name="server-name"> <properties> <help>Server names: exact, wildcard, or regex</help> <multi/> </properties> </leafNode> #include <include/allow-client.xml.i> </children> </tagNode> <node name="api"> <properties> <help>VyOS HTTP API configuration</help> </properties> <children> <node name="keys"> <properties> <help>HTTP API keys</help> </properties> <children> <tagNode name="id"> <properties> <help>HTTP API id</help> </properties> <children> <leafNode name="key"> <properties> <help>HTTP API plaintext key</help> </properties> </leafNode> </children> </tagNode> </children> </node> <leafNode name="strict"> <properties> <help>Enforce strict path checking</help> <valueless/> </properties> </leafNode> <leafNode name="debug"> <properties> <help>Debug</help> <valueless/> <hidden/> </properties> </leafNode> <node name="graphql"> <properties> <help>GraphQL support</help> </properties> <children> <leafNode name="introspection"> <properties> <help>Schema introspection</help> <valueless/> </properties> </leafNode> <node name="authentication"> <properties> <help>GraphQL authentication</help> </properties> <children> <leafNode name="type"> <properties> <help>Authentication type</help> <completionHelp> <list>key token</list> </completionHelp> <valueHelp> <format>key</format> <description>Use API keys</description> </valueHelp> <valueHelp> <format>token</format> <description>Use JWT token</description> </valueHelp> <constraint> <regex>(key|token)</regex> </constraint> </properties> <defaultValue>key</defaultValue> </leafNode> <leafNode name="expiration"> <properties> <help>Token time to expire in seconds</help> <valueHelp> <format>u32:60-31536000</format> <description>Token lifetime in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 60-31536000"/> </constraint> </properties> <defaultValue>3600</defaultValue> </leafNode> <leafNode name="secret-length"> <properties> <help>Length of shared secret in bytes</help> <valueHelp> <format>u32:16-65535</format> <description>Byte length of generated shared secret</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 16-65535"/> </constraint> </properties> <defaultValue>32</defaultValue> </leafNode> </children> </node> </children> </node> <node name="cors"> <properties> <help>Set CORS options</help> </properties> <children> <leafNode name="allow-origin"> <properties> <help>Allow resource request from origin</help> <multi/> </properties> </leafNode> </children> </node> </children> </node> <node name="api-restrict"> <properties> <help>Restrict api proxy to subset of virtual hosts</help> </properties> <children> <leafNode name="virtual-host"> <properties> <help>Restrict proxy to virtual host(s)</help> <multi/> </properties> </leafNode> </children> </node> <node name="certificates"> <properties> <help>TLS certificates</help> </properties> <children> #include <include/pki/ca-certificate.xml.i> #include <include/pki/certificate.xml.i> - <node name="certbot" owner="${vyos_conf_scripts_dir}/service_https_certificates_certbot.py"> - <properties> - <help>Request or apply a letsencrypt certificate for domain-name</help> - </properties> - <children> - <leafNode name="domain-name"> - <properties> - <help>Domain name(s) for which to obtain certificate</help> - <multi/> - </properties> - </leafNode> - <leafNode name="email"> - <properties> - <help>Email address to associate with certificate</help> - </properties> - </leafNode> - </children> - </node> </children> </node> #include <include/interface/vrf.xml.i> </children> </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index df17371cc..f01c715cb 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -1,360 +1,366 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="monitor"> <properties> <help>Monitor system information</help> </properties> <children> <node name="log"> <properties> <help>Monitor last lines of messages file</help> </properties> <command>SYSTEMD_LOG_COLOR=false journalctl --no-hostname --follow --boot</command> <children> <node name="color"> <properties> <help>Output log in a colored fashion</help> </properties> <command>SYSTEMD_LOG_COLOR=false grc journalctl --no-hostname --follow --boot</command> </node> <node name="ids"> <properties> <help>Monitor Intrusion Detection System log</help> </properties> <children> <leafNode name="ddos-protection"> <properties> <help>Monitor last lines of DDOS protection</help> </properties> <command>journalctl --no-hostname --follow --boot --unit fastnetmon.service</command> </leafNode> </children> </node> + <leafNode name="certbot"> + <properties> + <help>Monitor last lines of certbot log</help> + </properties> + <command>if sudo test -f /var/log/letsencrypt/letsencrypt.log; then sudo tail --follow=name /var/log/letsencrypt/letsencrypt.log; else echo "Cerbot log does not exist"; fi</command> + </leafNode> <leafNode name="conntrack-sync"> <properties> <help>Monitor last lines of conntrack-sync log</help> </properties> <command>journalctl --no-hostname --follow --boot --unit conntrackd.service</command> </leafNode> <leafNode name="console-server"> <properties> <help>Monitor last lines of console server log</help> </properties> <command>journalctl --no-hostname --follow --boot --unit conserver-server.service</command> </leafNode> <node name="dhcp"> <properties> <help>Monitor last lines of Dynamic Host Control Protocol log</help> </properties> <children> <node name="server"> <properties> <help>Monitor last lines of DHCP server log</help> </properties> <command>journalctl --no-hostname --follow --boot --unit isc-dhcp-server.service</command> </node> <node name="client"> <properties> <help>Monitor last lines of DHCP client log</help> </properties> <command>journalctl --no-hostname --follow --boot --unit "dhclient@*.service"</command> <children> <tagNode name="interface"> <properties> <help>Show DHCP client log on specific interface</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces --broadcast</script> </completionHelp> </properties> <command>journalctl --no-hostname --follow --boot --unit "dhclient@$6.service"</command> </tagNode> </children> </node> </children> </node> <node name="dhcpv6"> <properties> <help>Monitor last lines of Dynamic Host Control Protocol IPv6 log</help> </properties> <children> <node name="server"> <properties> <help>Monitor last lines of DHCPv6 server log</help> </properties> <command>journalctl --no-hostname --follow --boot --unit isc-dhcp-server6.service</command> </node> <node name="client"> <properties> <help>Monitor last lines of DHCPv6 client log</help> </properties> <command>journalctl --no-hostname --follow --boot --unit "dhcp6c@*.service"</command> <children> <tagNode name="interface"> <properties> <help>Show DHCPv6 client log on specific interface</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> <command>journalctl --no-hostname --follow --boot --unit "dhcp6c@$6.service"</command> </tagNode> </children> </node> </children> </node> <leafNode name="flow-accounting"> <properties> <help>Monitor last lines of flow-accounting log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit uacctd.service</command> </leafNode> <leafNode name="ipoe-server"> <properties> <help>Monitor last lines of IP over Ethernet server log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit accel-ppp@ipoe.service</command> </leafNode> <leafNode name="kernel"> <properties> <help>Monitor last lines of Linux Kernel log</help> </properties> <command>journalctl --no-hostname --boot --follow --dmesg</command> </leafNode> <leafNode name="ndp-proxy"> <properties> <help>Monitor last lines of Neighbor Discovery Protocol (NDP) Proxy</help> </properties> <command>journalctl --no-hostname --boot --follow --unit ndppd.service</command> </leafNode> <leafNode name="nhrp"> <properties> <help>Monitor last lines of Next Hop Resolution Protocol log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit opennhrp.service</command> </leafNode> <leafNode name="ntp"> <properties> <help>Monitor last lines of Network Time Protocol log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit chrony.service</command> </leafNode> <node name="openvpn"> <properties> <help>Monitor last lines of OpenVPN log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit openvpn@*.service</command> <children> <tagNode name="interface"> <properties> <help>Monitor last lines of specific OpenVPN interface log</help> <completionHelp> <path>interfaces openvpn</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --unit openvpn@$5.service</command> </tagNode> </children> </node> <node name="pppoe"> <properties> <help>Monitor last lines of PPPoE interface log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit "ppp@pppoe*.service"</command> <children> <tagNode name="interface"> <properties> <help>Monitor last lines of PPPoE log for specific interface</help> <completionHelp> <path>interfaces pppoe</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command> </tagNode> </children> </node> <leafNode name="pppoe-server"> <properties> <help>Monitor last lines of PPPoE server log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit accel-ppp@pppoe.service</command> </leafNode> <node name="protocol"> <properties> <help>Monitor routing protocol logs</help> </properties> <children> <leafNode name="ospf"> <properties> <help>Monitor log for OSPF</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ospfd</command> </leafNode> <leafNode name="ospfv3"> <properties> <help>Monitor log for OSPF for IPv6</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ospf6d</command> </leafNode> <leafNode name="bgp"> <properties> <help>Monitor log for BGP</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/bgpd</command> </leafNode> <leafNode name="rip"> <properties> <help>Monitor log for RIP</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ripd</command> </leafNode> <leafNode name="ripng"> <properties> <help>Monitor log for RIPng</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ripngd</command> </leafNode> <leafNode name="static"> <properties> <help>Monitor log for static route</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/staticd</command> </leafNode> <leafNode name="multicast"> <properties> <help>Monitor log for Multicast protocol</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/pimd</command> </leafNode> <leafNode name="isis"> <properties> <help>Monitor log for ISIS</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/isisd</command> </leafNode> <leafNode name="nhrp"> <properties> <help>Monitor log for NHRP</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/nhrpd</command> </leafNode> <leafNode name="bfd"> <properties> <help>Monitor log for BFD</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/bfdd</command> </leafNode> <leafNode name="mpls"> <properties> <help>Monitor log for MPLS</help> </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ldpd</command> </leafNode> </children> </node> <node name="macsec"> <properties> <help>Monitor last lines of MACsec</help> </properties> <command>journalctl --no-hostname --boot --follow --unit "wpa_supplicant-macsec@*.service"</command> <children> <tagNode name="interface"> <properties> <help>Monitor last lines of specific MACsec interface</help> <completionHelp> <path>interfaces macsec</path> </completionHelp> </properties> <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --follow --unit "wpa_supplicant-macsec@$SRC.service"</command> </tagNode> </children> </node> <leafNode name="router-advert"> <properties> <help>Monitor last lines of Router Advertisement Daemon log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit radvd.service</command> </leafNode> <leafNode name="snmp"> <properties> <help>Monitor last lines of Simple Network Monitoring Protocol log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit snmpd.service</command> </leafNode> <node name="ssh"> <properties> <help>Monitor last lines of Secure Shell log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit ssh.service</command> <children> <node name="dynamic-protection"> <properties> <help>Monitor last lines of SSH guard log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit sshguard.service</command> </node> </children> </node> <leafNode name="vpn"> <properties> <help>Monitor last lines of ALL Virtual Private Network services</help> </properties> <command>journalctl --no-hostname --boot --follow --unit strongswan.service --unit accel-ppp@*.service --unit ocserv.service</command> </leafNode> <leafNode name="ipsec"> <properties> <help>Monitor last lines of IPsec log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit strongswan.service</command> </leafNode> <leafNode name="l2tp"> <properties> <help>Monitor last lines of L2TP log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit accel-ppp@l2tp.service</command> </leafNode> <leafNode name="openconnect"> <properties> <help>Monitor last lines of OpenConnect log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit ocserv.service</command> </leafNode> <leafNode name="pptp"> <properties> <help>Monitor last lines of PPTP log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit accel-ppp@pptp.service</command> </leafNode> <leafNode name="sstp"> <properties> <help>Monitor last lines of Secure Socket Tunneling Protocol server</help> </properties> <command>journalctl --no-hostname --boot --follow --unit accel-ppp@sstp.service</command> </leafNode> <node name="sstpc"> <properties> <help>Monitor last lines of Secure Socket Tunneling Protocol client</help> </properties> <command>journalctl --no-hostname --boot --follow --unit "ppp@sstpc*.service"</command> <children> <tagNode name="interface"> <properties> <help>Monitor last lines of SSTP client log for specific interface</help> <completionHelp> <path>interfaces sstpc</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command> </tagNode> </children> </node> <leafNode name="vrrp"> <properties> <help>Monitor last lines of Virtual Router Redundancy Protocol log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit keepalived.service</command> </leafNode> </children> </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in index ca0eb3687..4b8d9c47a 100644 --- a/op-mode-definitions/pki.xml.in +++ b/op-mode-definitions/pki.xml.in @@ -1,577 +1,587 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="generate"> <children> <node name="pki"> <properties> <help>Generate PKI certificates and keys</help> </properties> <children> <node name="ca"> <properties> <help>Generate CA certificate</help> </properties> <children> <tagNode name="sign"> <properties> <help>Sign generated CA certificate with another specified CA certificate</help> <completionHelp> <path>pki ca</path> </completionHelp> </properties> <children> <tagNode name="file"> <properties> <help>Write generated CA certificate into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$7" --sign "$5" --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated CA certificate into running configuration</help> <completionHelp> <list><certificate name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$7" --sign "$5" --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "noname" --sign "$5"</command> </tagNode> <tagNode name="file"> <properties> <help>Write generated CA certificate into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$5" --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated CA certificate into running configuration</help> <completionHelp> <list><CA name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$5" --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "noname"</command> </node> <node name="certificate"> <properties> <help>Generate certificate request</help> </properties> <children> <node name="self-signed"> <properties> <help>Generate self-signed certificate</help> </properties> <children> <tagNode name="file"> <properties> <help>Write generated self-signed certificate into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$6" --self-sign --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated self-signed certificate into running configuration</help> <completionHelp> <list><certificate name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$6" --self-sign --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" --self-sign</command> </node> <tagNode name="sign"> <properties> <help>Sign generated certificate with specified CA certificate</help> <completionHelp> <path>pki ca</path> </completionHelp> </properties> <children> <tagNode name="file"> <properties> <help>Write generated signed certificate into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$7" --sign "$5" --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated signed certificate into running configuration</help> <completionHelp> <list><certificate name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$7" --sign "$5" --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" --sign "$5"</command> </tagNode> <tagNode name="file"> <properties> <help>Write generated certificate request and key into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$5" --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated certificate private key into running configuration</help> <completionHelp> <list><certificate name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$5" --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname"</command> </node> <tagNode name="crl"> <properties> <help>Generate CRL for specified CA certificate</help> <completionHelp> <path>pki ca</path> </completionHelp> </properties> <children> <tagNode name="file"> <properties> <help>Write generated CRL into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4" --file</command> </tagNode> <leafNode name="install"> <properties> <help>Commands for installing generated CRL into running configuration</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4" --install</command> </leafNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4"</command> </tagNode> <node name="dh"> <properties> <help>Generate DH parameters</help> </properties> <children> <tagNode name="file"> <properties> <help>Write generated DH parameters into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --dh "$5" --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated DH parameters into running configuration</help> <completionHelp> <list><DH name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --dh "$5" --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --dh "noname"</command> </node> <node name="key-pair"> <properties> <help>Generate a key pair</help> </properties> <children> <tagNode name="file"> <properties> <help>Write generated key pair into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --keypair "$5" --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated key pair into running configuration</help> <completionHelp> <list><key name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --keypair "$5" --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --keypair "noname"</command> </node> <node name="openvpn"> <properties> <help>Generate OpenVPN keys</help> </properties> <children> <node name="shared-secret"> <properties> <help>Generate OpenVPN shared secret key</help> </properties> <children> <tagNode name="file"> <properties> <help>Write generated OpenVPN shared secret key into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "$6" --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated OpenVPN shared secret key into running configuration</help> <completionHelp> <list><key name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "$6" --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "noname"</command> </node> </children> </node> <node name="ssh-key"> <properties> <help>Generate SSH key</help> </properties> <children> <tagNode name="file"> <properties> <help>Write generated SSH keys into the specified filename</help> <completionHelp> <list><filename></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ssh "$5" --file</command> </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated SSH key into running configuration</help> <completionHelp> <list><key name></list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ssh "$5" --install</command> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ssh "noname"</command> </node> <node name="wireguard"> <properties> <help>Generate WireGuard keys</help> </properties> <children> <node name="key-pair"> <properties> <help>Generate WireGuard public/private key-pair</help> </properties> <children> <node name="install"> <properties> <help>Generate CLI commands to install WireGuard key to configuration</help> </properties> <children> <tagNode name="interface"> <properties> <help>WireGuard Interface used in install command</help> <completionHelp> <path>interfaces wireguard</path> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key --interface "$7" --install</command> </tagNode> </children> </node> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key</command> </node> <node name="preshared-key"> <properties> <help>Generate WireGuard pre-shared key</help> </properties> <children> <node name="install"> <properties> <help>Generate CLI commands to install WireGuard key to configuration</help> </properties> <children> <tagNode name="interface"> <properties> <help>WireGuard Interface used in install command</help> <completionHelp> <path>interfaces wireguard</path> </completionHelp> </properties> <children> <tagNode name="peer"> <properties> <help>Interface used for install command</help> <completionHelp> <path>interfaces wireguard ${COMP_WORDS[COMP_CWORD-2]} peer</path> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --psk --interface "$7" --peer "$9" --install</command> </tagNode> </children> </tagNode> </children> </node> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --psk</command> </node> </children> </node> </children> </node> </children> </node> <node name="import"> <properties> <help>Import an object</help> </properties> <children> <node name="pki"> <properties> <help>Import file into PKI configuration</help> </properties> <children> <tagNode name="ca"> <properties> <help>Import CA certificate into PKI</help> <completionHelp> <list><name></list> </completionHelp> </properties> <children> <tagNode name="file"> <properties> <help>Path to CA certificate file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --ca "$4" --filename "$6"</command> </tagNode> <tagNode name="key-file"> <properties> <help>Path to private key file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --ca "$4" --key-filename "$6"</command> </tagNode> </children> </tagNode> <tagNode name="certificate"> <properties> <help>Import certificate into PKI</help> <completionHelp> <list><name></list> </completionHelp> </properties> <children> <tagNode name="file"> <properties> <help>Path to certificate file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --certificate "$4" --filename "$6"</command> </tagNode> <tagNode name="key-file"> <properties> <help>Path to private key file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --certificate "$4" --key-filename "$6"</command> </tagNode> </children> </tagNode> <tagNode name="crl"> <properties> <help>Import certificate revocation list into PKI</help> <completionHelp> <list><CA name></list> </completionHelp> </properties> <children> <tagNode name="file"> <properties> <help>Path to CRL file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --crl "$4" --filename "$6"</command> </tagNode> </children> </tagNode> <tagNode name="dh"> <properties> <help>Import DH parameters into PKI</help> <completionHelp> <list><name></list> </completionHelp> </properties> <children> <tagNode name="file"> <properties> <help>Path to DH parameters file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --dh "$4" --filename "$6"</command> </tagNode> </children> </tagNode> <tagNode name="key-pair"> <properties> <help>Import key pair into PKI</help> <completionHelp> <list><name></list> </completionHelp> </properties> <children> <tagNode name="public-file"> <properties> <help>Path to public key file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --keypair "$4" --filename "$6"</command> </tagNode> <tagNode name="private-file"> <properties> <help>Path to private key file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --keypair "$4" --key-filename "$6"</command> </tagNode> </children> </tagNode> <node name="openvpn"> <properties> <help>Import OpenVPN keys into PKI</help> </properties> <children> <tagNode name="shared-secret"> <properties> <help>Import OpenVPN shared secret key into PKI</help> <completionHelp> <list><name></list> </completionHelp> </properties> <children> <tagNode name="file"> <properties> <help>Path to shared secret key file</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --openvpn "$5" --filename "$7"</command> </tagNode> </children> </tagNode> </children> </node> </children> </node> </children> </node> <node name="show"> <children> <node name="pki"> <properties> <help>Show PKI x509 certificates</help> </properties> <children> <leafNode name="ca"> <properties> <help>Show x509 CA certificates</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --ca "all"</command> </leafNode> <tagNode name="ca"> <properties> <help>Show x509 CA certificate by name</help> <completionHelp> <path>pki ca</path> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --ca "$4"</command> <children> <leafNode name="pem"> <properties> <help>Show x509 CA certificate in PEM format</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --ca "$4" --pem</command> </leafNode> </children> </tagNode> <leafNode name="certificate"> <properties> <help>Show x509 certificates</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "all"</command> </leafNode> <tagNode name="certificate"> <properties> <help>Show x509 certificate by name</help> <completionHelp> <path>pki certificate</path> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "$4"</command> <children> <leafNode name="pem"> <properties> <help>Show x509 certificate in PEM format</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "$4" --pem</command> </leafNode> <tagNode name="fingerprint"> <properties> <help>Show x509 certificate fingerprint</help> <completionHelp> <list>sha256 sha384 sha512</list> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "$4" --fingerprint "$6"</command> </tagNode> </children> </tagNode> <leafNode name="crl"> <properties> <help>Show x509 certificate revocation lists</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --crl "all"</command> </leafNode> <tagNode name="crl"> <properties> <help>Show x509 certificate revocation lists by CA name</help> <completionHelp> <path>pki ca</path> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --crl "$4"</command> <children> <leafNode name="pem"> <properties> <help>Show x509 certificate revocation lists by CA name in PEM format</help> </properties> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --crl "$4" --pem</command> </leafNode> </children> </tagNode> </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action show</command> </node> </children> </node> + <node name="renew"> + <children> + <leafNode name="certbot"> + <properties> + <help>Start manual certbot renewal</help> + </properties> + <command>sudo systemctl start certbot.service</command> + </leafNode> + </children> + </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 6cd53882d..432a21b59 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -1,505 +1,511 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="show"> <properties> <help>Show system information</help> </properties> <children> <tagNode name="log"> <properties> <help>Show last number of messages in master logging buffer</help> <completionHelp> <list><1-9999></list> </completionHelp> </properties> <command>if ${vyos_validators_dir}/numeric --range 1-9999 "$3"; then journalctl --no-hostname --boot --lines "$3"; fi</command> </tagNode> <node name="log"> <properties> <help>Show contents of current master logging buffer</help> </properties> <command>journalctl --no-hostname --boot</command> <children> <leafNode name="audit"> <properties> <help>Show audit logs</help> </properties> <command>cat /var/log/audit/audit.log</command> </leafNode> <leafNode name="all"> <properties> <help>Show contents of all master log files</help> </properties> <command>sudo bash -c 'eval $(lesspipe); less $_vyatta_less_options --prompt=".logm, file %i of %m., page %dt of %D" -- `printf "%s\n" /var/log/messages* | sort -nr`'</command> </leafNode> <leafNode name="authorization"> <properties> <help>Show listing of authorization attempts</help> </properties> <command>journalctl --no-hostname --boot --quiet SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command> </leafNode> + <leafNode name="certbot"> + <properties> + <help>Show log for certbot</help> + </properties> + <command>if sudo test -f /var/log/letsencrypt/letsencrypt.log; then sudo cat /var/log/letsencrypt/letsencrypt.log; else echo "Cerbot log does not exist"; fi</command> + </leafNode> <leafNode name="cluster"> <properties> <help>Show log for Cluster</help> </properties> <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e heartbeat -e cl_status -e mach_down -e ha_log</command> </leafNode> <leafNode name="conntrack-sync"> <properties> <help>Show log for Conntrack-sync</help> </properties> <command>journalctl --no-hostname --boot --unit conntrackd.service</command> </leafNode> <leafNode name="console-server"> <properties> <help>Show log for console server</help> </properties> <command>journalctl --no-hostname --boot --unit conserver-server.service</command> </leafNode> <node name="ids"> <properties> <help>Show log for for Intrusion Detection System</help> </properties> <children> <leafNode name="ddos-protection"> <properties> <help>Show log for DDOS protection</help> </properties> <command>journalctl --no-hostname --boot --unit fastnetmon.service</command> </leafNode> </children> </node> <node name="dhcp"> <properties> <help>Show log for Dynamic Host Control Protocol (DHCP)</help> </properties> <children> <node name="server"> <properties> <help>Show log for DHCP server</help> </properties> <command>journalctl --no-hostname --boot --unit isc-dhcp-server.service</command> </node> <node name="client"> <properties> <help>Show DHCP client logs</help> </properties> <command>journalctl --no-hostname --boot --unit "dhclient@*.service"</command> <children> <tagNode name="interface"> <properties> <help>Show DHCP client log on specific interface</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces --broadcast</script> </completionHelp> </properties> <command>journalctl --no-hostname --boot --unit "dhclient@$6.service"</command> </tagNode> </children> </node> </children> </node> <node name="dhcpv6"> <properties> <help>Show log for Dynamic Host Control Protocol IPv6 (DHCPv6)</help> </properties> <children> <node name="server"> <properties> <help>Show log for DHCPv6 server</help> </properties> <command>journalctl --no-hostname --boot --unit isc-dhcp-server6.service</command> </node> <node name="client"> <properties> <help>Show DHCPv6 client logs</help> </properties> <command>journalctl --no-hostname --boot --unit "dhcp6c@*.service"</command> <children> <tagNode name="interface"> <properties> <help>Show DHCPv6 client log on specific interface</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> <command>journalctl --no-hostname --boot --unit "dhcp6c@$6.service"</command> </tagNode> </children> </node> </children> </node> <node name="firewall"> <properties> <help>Show log for Firewall</help> </properties> <children> <tagNode name="ipv6-name"> <properties> <help>Show log for a specified firewall (IPv6)</help> <completionHelp> <path>firewall ipv6-name</path> </completionHelp> </properties> <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | egrep "\[$5-([0-9]+|default)-[ADR]\]"</command> <children> <tagNode name="rule"> <properties> <help>Show log for a rule in the specified firewall</help> <completionHelp> <path>firewall ipv6-name ${COMP_WORDS[4]} rule</path> </completionHelp> </properties> <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "\[$5-$7-[ADR]\]"</command> </tagNode> </children> </tagNode> <tagNode name="name"> <properties> <help>Show log for a specified firewall (IPv4)</help> <completionHelp> <path>firewall name</path> </completionHelp> </properties> <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | egrep "\[$5-([0-9]+|default)-[ADR]\]"</command> <children> <tagNode name="rule"> <properties> <help>Show log for a rule in the specified firewall</help> <completionHelp> <path>firewall name ${COMP_WORDS[4]} rule</path> </completionHelp> </properties> <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | egrep "\[$5-$7-[ADR]\]"</command> </tagNode> </children> </tagNode> </children> </node> <leafNode name="flow-accounting"> <properties> <help>Show log for flow-accounting</help> </properties> <command>journalctl --no-hostname --boot --unit uacctd.service</command> </leafNode> <leafNode name="https"> <properties> <help>Show log for HTTPs</help> </properties> <command>journalctl --no-hostname --boot --unit nginx.service</command> </leafNode> <tagNode name="image"> <properties> <help>Show contents of master log file for image</help> <completionHelp> <script>compgen -f /lib/live/mount/persistence/boot/ | grep -v grub | sed -e s@/lib/live/mount/persistence/boot/@@</script> </completionHelp> </properties> <command>less $_vyatta_less_options --prompt=".log, page %dt of %D" -- /lib/live/mount/persistence/boot/$4/rw/var/log/messages</command> <children> <leafNode name="all"> <properties> <help>Show contents of all master log files for image</help> </properties> <command>eval $(lesspipe); less $_vyatta_less_options --prompt=".log?m, file %i of %m., page %dt of %D" -- `printf "%s\n" /lib/live/mount/persistence/boot/$4/rw/var/log/messages* | sort -nr`</command> </leafNode> <leafNode name="authorization"> <properties> <help>Show listing of authorization attempts for image</help> </properties> <command>less $_vyatta_less_options --prompt=".log, page %dt of %D" -- /lib/live/mount/persistence/boot/$4/rw/var/log/auth.log</command> </leafNode> <tagNode name="tail"> <properties> <help>Show last changes to messages</help> <completionHelp> <list><NUMBER></list> </completionHelp> </properties> <command>tail -n "$6" /lib/live/mount/persistence/boot/$4/rw/var/log/messages | ${VYATTA_PAGER:-cat}</command> </tagNode> </children> </tagNode> <leafNode name="ipoe-server"> <properties> <help>Show log for IPoE server</help> </properties> <command>journalctl --no-hostname --boot --unit accel-ppp@ipoe.service</command> </leafNode> <leafNode name="kernel"> <properties> <help>Show log for Linux Kernel</help> </properties> <command>journalctl --no-hostname --boot --dmesg</command> </leafNode> <leafNode name="lldp"> <properties> <help>Show log for Link Layer Discovery Protocol (LLDP)</help> </properties> <command>journalctl --no-hostname --boot --unit lldpd.service</command> </leafNode> <leafNode name="nat"> <properties> <help>Show log for Network Address Translation (NAT)</help> </properties> <command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command> </leafNode> <leafNode name="ndp-proxy"> <properties> <help>Show log for Neighbor Discovery Protocol (NDP) Proxy</help> </properties> <command>journalctl --no-hostname --boot --unit ndppd.service</command> </leafNode> <leafNode name="nhrp"> <properties> <help>Show log for Next Hop Resolution Protocol (NHRP)</help> </properties> <command>journalctl --no-hostname --boot --unit opennhrp.service</command> </leafNode> <leafNode name="ntp"> <properties> <help>Show log for Network Time Protocol (NTP)</help> </properties> <command>journalctl --no-hostname --boot --unit chrony.service</command> </leafNode> <node name="macsec"> <properties> <help>Show log for MACsec</help> </properties> <command>journalctl --no-hostname --boot --unit "wpa_supplicant-macsec@*.service"</command> <children> <tagNode name="interface"> <properties> <help>Show MACsec log on specific interface</help> <completionHelp> <path>interfaces macsec</path> </completionHelp> </properties> <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --unit "wpa_supplicant-macsec@$SRC.service"</command> </tagNode> </children> </node> <node name="openvpn"> <properties> <help>Show log for OpenVPN</help> </properties> <command>journalctl --no-hostname --boot --unit openvpn@*.service</command> <children> <tagNode name="interface"> <properties> <help>Show OpenVPN log on specific interface</help> <completionHelp> <path>interfaces openvpn</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --unit openvpn@$5.service</command> </tagNode> </children> </node> <node name="pppoe"> <properties> <help>Show log for PPPoE interface</help> </properties> <command>journalctl --no-hostname --boot --unit "ppp@pppoe*.service"</command> <children> <tagNode name="interface"> <properties> <help>Show PPPoE log on specific interface</help> <completionHelp> <path>interfaces pppoe</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command> </tagNode> </children> </node> <leafNode name="pppoe-server"> <properties> <help>Show log for PPPoE server</help> </properties> <command>journalctl --no-hostname --boot --unit accel-ppp@pppoe.service</command> </leafNode> <node name="protocol"> <properties> <help>Show log for Routing Protocol</help> </properties> <children> <leafNode name="ospf"> <properties> <help>Show log for OSPF</help> </properties> <command>journalctl --boot /usr/lib/frr/ospfd</command> </leafNode> <leafNode name="ospfv3"> <properties> <help>Show log for OSPF for IPv6</help> </properties> <command>journalctl --boot /usr/lib/frr/ospf6d</command> </leafNode> <leafNode name="bgp"> <properties> <help>Show log for BGP</help> </properties> <command>journalctl --boot /usr/lib/frr/bgpd</command> </leafNode> <leafNode name="rip"> <properties> <help>Show log for RIP</help> </properties> <command>journalctl --boot /usr/lib/frr/ripd</command> </leafNode> <leafNode name="ripng"> <properties> <help>Show log for RIPng</help> </properties> <command>journalctl --boot /usr/lib/frr/ripngd</command> </leafNode> <leafNode name="static"> <properties> <help>Show log for static route</help> </properties> <command>journalctl --boot /usr/lib/frr/staticd</command> </leafNode> <leafNode name="multicast"> <properties> <help>Show log for Multicast protocol</help> </properties> <command>journalctl --boot /usr/lib/frr/pimd</command> </leafNode> <leafNode name="isis"> <properties> <help>Show log for ISIS</help> </properties> <command>journalctl --boot /usr/lib/frr/isisd</command> </leafNode> <leafNode name="nhrp"> <properties> <help>Show log for NHRP</help> </properties> <command>journalctl --boot /usr/lib/frr/nhrpd</command> </leafNode> <leafNode name="bfd"> <properties> <help>Show log for BFD</help> </properties> <command>journalctl --boot /usr/lib/frr/bfdd</command> </leafNode> <leafNode name="mpls"> <properties> <help>Show log for MPLS</help> </properties> <command>journalctl --boot /usr/lib/frr/ldpd</command> </leafNode> </children> </node> <leafNode name="router-advert"> <properties> <help>Show log for Router Advertisement Daemon (radvd)</help> </properties> <command>journalctl --no-hostname --boot --unit radvd.service</command> </leafNode> <leafNode name="snmp"> <properties> <help>Show log for Simple Network Monitoring Protocol (SNMP)</help> </properties> <command>journalctl --no-hostname --boot --unit snmpd.service</command> </leafNode> <node name="ssh"> <properties> <help>Show log for Secure Shell (SSH)</help> </properties> <command>journalctl --no-hostname --boot --unit ssh.service</command> <children> <node name="dynamic-protection"> <properties> <help>Show SSH guard log</help> </properties> <command>journalctl --no-hostname --boot --unit sshguard.service</command> </node> </children> </node> <tagNode name="tail"> <properties> <help>Show last n changes to messages</help> <completionHelp> <list><NUMBER></list> </completionHelp> </properties> <command>tail -n "$4" /var/log/messages | ${VYATTA_PAGER:-cat}</command> </tagNode> <node name="tail"> <properties> <help>Show last 10 lines of /var/log/messages file</help> </properties> <command>tail -n 10 /var/log/messages</command> </node> <leafNode name="vpn"> <properties> <help>Monitor last lines of ALL Virtual Private Network services</help> </properties> <command>journalctl --no-hostname --boot --unit strongswan.service --unit accel-ppp@*.service --unit ocserv.service</command> </leafNode> <leafNode name="ipsec"> <properties> <help>Show log for IPsec</help> </properties> <command>journalctl --no-hostname --boot --unit strongswan.service</command> </leafNode> <leafNode name="l2tp"> <properties> <help>Show log for L2TP</help> </properties> <command>journalctl --no-hostname --boot --unit accel-ppp@l2tp.service</command> </leafNode> <leafNode name="openconnect"> <properties> <help>Show log for OpenConnect</help> </properties> <command>journalctl --no-hostname --boot --unit ocserv.service</command> </leafNode> <leafNode name="pptp"> <properties> <help>Show log for PPTP</help> </properties> <command>journalctl --no-hostname --boot --unit accel-ppp@pptp.service</command> </leafNode> <leafNode name="sstp"> <properties> <help>Show log for Secure Socket Tunneling Protocol (SSTP) server</help> </properties> <command>journalctl --no-hostname --boot --unit accel-ppp@sstp.service</command> </leafNode> <node name="sstpc"> <properties> <help>Show log for Secure Socket Tunneling Protocol (SSTP) client</help> </properties> <command>journalctl --no-hostname --boot --unit "ppp@sstpc*.service"</command> <children> <tagNode name="interface"> <properties> <help>Show SSTP client log on specific interface</help> <completionHelp> <path>interfaces sstpc</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command> </tagNode> </children> </node> <leafNode name="vrrp"> <properties> <help>Show log for Virtual Router Redundancy Protocol (VRRP)</help> </properties> <command>journalctl --no-hostname --boot --unit keepalived.service</command> </leafNode> <leafNode name="webproxy"> <properties> <help>Show log for Webproxy</help> </properties> <command>journalctl --no-hostname --boot --unit squid.service</command> </leafNode> </children> </node> </children> </node> </interfaceDefinition> diff --git a/python/vyos/config.py b/python/vyos/config.py index ca7b035e5..bee85315d 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -1,583 +1,620 @@ # Copyright 2017, 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. """ A library for reading VyOS running config data. This library is used internally by all config scripts of VyOS, but its API should be considered stable and safe to use in user scripts. Note that this module will not work outside VyOS. Node taxonomy ############# There are multiple types of config tree nodes in VyOS, each requires its own set of operations. *Leaf nodes* (such as "address" in interfaces) can have values, but cannot have children. Leaf nodes can have one value, multiple values, or no values at all. For example, "system host-name" is a single-value leaf node, "system name-server" is a multi-value leaf node (commonly abbreviated "multi node"), and "system ip disable-forwarding" is a valueless leaf node. Non-leaf nodes cannot have values, but they can have child nodes. They are divided into two classes depending on whether the names of their children are fixed or not. For example, under "system", the names of all valid child nodes are predefined ("login", "name-server" etc.). To the contrary, children of the "system task-scheduler task" node can have arbitrary names. Such nodes are called *tag nodes*. This terminology is confusing but we keep using it for lack of a better word. No one remembers if the "tag" in "task Foo" is "task" or "Foo", but the distinction is irrelevant in practice. Configuration modes ################### VyOS has two distinct modes: operational mode and configuration mode. When a user logins, the CLI is in the operational mode. In this mode, only the running (effective) config is accessible for reading. When a user enters the "configure" command, a configuration session is setup. Every config session has its *proposed* (or *session*) config built on top of the current running config. When changes are commited, if commit succeeds, the proposed config is merged into the running config. In configuration mode, "base" functions like `exists`, `return_value` return values from the session config, while functions prefixed "effective" return values from the running config. In operational mode, all functions return values from the running config. """ import re import json from copy import deepcopy from typing import Union import vyos.configtree from vyos.xml_ref import multi_to_list from vyos.xml_ref import from_source from vyos.xml_ref import ext_dict_merge from vyos.xml_ref import relative_defaults from vyos.utils.dict import get_sub_dict from vyos.utils.dict import mangle_dict_keys from vyos.configsource import ConfigSource from vyos.configsource import ConfigSourceSession class ConfigDict(dict): _from_defaults = {} _dict_kwargs = {} def from_defaults(self, path: list[str]) -> bool: return from_source(self._from_defaults, path) @property def kwargs(self) -> dict: return self._dict_kwargs def config_dict_merge(src: dict, dest: Union[dict, ConfigDict]) -> ConfigDict: if not isinstance(dest, ConfigDict): dest = ConfigDict(dest) return ext_dict_merge(src, dest) +def config_dict_mangle_acme(name, cli_dict): + """ + Load CLI PKI dictionary and if an ACME certificate is used, load it's content + and place it into the CLI dictionary as it would be a "regular" CLI PKI based + certificate with private key + """ + from vyos.base import ConfigError + from vyos.defaults import directories + from vyos.utils.file import read_file + from vyos.pki import encode_certificate + from vyos.pki import encode_private_key + from vyos.pki import load_certificate + from vyos.pki import load_private_key + + try: + vyos_certbot_dir = directories['certbot'] + + if 'acme' in cli_dict: + tmp = read_file(f'{vyos_certbot_dir}/live/{name}/cert.pem') + tmp = load_certificate(tmp, wrap_tags=False) + cert_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1]) + + tmp = read_file(f'{vyos_certbot_dir}/live/{name}/privkey.pem') + tmp = load_private_key(tmp, wrap_tags=False) + key_base64 = "".join(encode_private_key(tmp).strip().split("\n")[1:-1]) + # install ACME based PEM keys into "regular" CLI config keys + cli_dict.update({'certificate' : cert_base64, 'private' : {'key' : key_base64}}) + except: + raise ConfigError(f'Unable to load ACME certificates for "{name}"!') + + return cli_dict + class Config(object): """ The class of config access objects. Internally, in the current implementation, this object is *almost* stateless, the only state it keeps is relative *config path* for convenient access to config subtrees. """ def __init__(self, session_env=None, config_source=None): if config_source is None: self._config_source = ConfigSourceSession(session_env) else: if not isinstance(config_source, ConfigSource): raise TypeError("config_source not of type ConfigSource") self._config_source = config_source self._level = [] self._dict_cache = {} (self._running_config, self._session_config) = self._config_source.get_configtree_tuple() def get_config_tree(self, effective=False): if effective: return self._running_config return self._session_config def _make_path(self, path): # Backwards-compatibility stuff: original implementation used string paths # libvyosconfig paths are lists, but since node names cannot contain whitespace, # splitting at whitespace is reasonably safe. # It may cause problems with exists() when it's used for checking values, # since values may contain whitespace. if isinstance(path, str): path = re.split(r'\s+', path) elif isinstance(path, list): pass else: raise TypeError("Path must be a whitespace-separated string or a list") return (self._level + path) def set_level(self, path): """ Set the *edit level*, that is, a relative config tree path. Once set, all operations will be relative to this path, for example, after ``set_level("system")``, calling ``exists("name-server")`` is equivalent to calling ``exists("system name-server"`` without ``set_level``. Args: path (str|list): relative config path """ # Make sure there's always a space between default path (level) # and path supplied as method argument # XXX: for small strings in-place concatenation is not a problem if isinstance(path, str): if path: self._level = re.split(r'\s+', path) else: self._level = [] elif isinstance(path, list): self._level = path.copy() else: raise TypeError("Level path must be either a whitespace-separated string or a list") def get_level(self): """ Gets the current edit level. Returns: str: current edit level """ return(self._level.copy()) def exists(self, path): """ Checks if a node or value with given path exists in the proposed config. Args: path (str): Configuration tree path Returns: True if node or value exists in the proposed config, False otherwise Note: This function should not be used outside of configuration sessions. In operational mode scripts, use ``exists_effective``. """ if self._session_config is None: return False # Assume the path is a node path first if self._session_config.exists(self._make_path(path)): return True else: # If that check fails, it may mean the path has a value at the end. # libvyosconfig exists() works only for _nodes_, not _values_ # libvyattacfg also worked for values, so we emulate that case here if isinstance(path, str): path = re.split(r'\s+', path) path_without_value = path[:-1] try: # return_values() is safe to use with single-value nodes, # it simply returns a single-item list in that case. values = self._session_config.return_values(self._make_path(path_without_value)) # If we got this far, the node does exist and has values, # so we need to check if it has the value in question among its values. return (path[-1] in values) except vyos.configtree.ConfigTreeError: # Even the parent node doesn't exist at all return False def session_changed(self): """ Returns: True if the config session has uncommited changes, False otherwise. """ return self._config_source.session_changed() def in_session(self): """ Returns: True if called from a configuration session, False otherwise. """ return self._config_source.in_session() def show_config(self, path=[], default=None, effective=False): """ Args: path (str list): Configuration tree path, or empty default (str): Default value to return Returns: str: working configuration """ return self._config_source.show_config(path, default, effective) def get_cached_root_dict(self, effective=False): cached = self._dict_cache.get(effective, {}) if cached: return cached if effective: config = self._running_config else: config = self._session_config if config: config_dict = json.loads(config.to_json()) else: config_dict = {} self._dict_cache[effective] = config_dict return config_dict def verify_mangling(self, key_mangling): if not (isinstance(key_mangling, tuple) and \ (len(key_mangling) == 2) and \ isinstance(key_mangling[0], str) and \ isinstance(key_mangling[1], str)): raise ValueError("key_mangling must be a tuple of two strings") def get_config_dict(self, path=[], effective=False, key_mangling=None, get_first_key=False, no_multi_convert=False, no_tag_node_value_mangle=False, with_defaults=False, with_recursive_defaults=False, with_pki=False): """ Args: path (str list): Configuration tree path, can be empty effective=False: effective or session config key_mangling=None: mangle dict keys according to regex and replacement get_first_key=False: if k = path[:-1], return sub-dict d[k] instead of {k: d[k]} no_multi_convert=False: if convert, return single value of multi node as list Returns: a dict representation of the config under path """ kwargs = locals().copy() del kwargs['self'] del kwargs['no_multi_convert'] del kwargs['with_defaults'] del kwargs['with_recursive_defaults'] del kwargs['with_pki'] lpath = self._make_path(path) root_dict = self.get_cached_root_dict(effective) conf_dict = get_sub_dict(root_dict, lpath, get_first_key=get_first_key) rpath = lpath if get_first_key else lpath[:-1] if not no_multi_convert: conf_dict = multi_to_list(rpath, conf_dict) if key_mangling is not None: self.verify_mangling(key_mangling) conf_dict = mangle_dict_keys(conf_dict, key_mangling[0], key_mangling[1], abs_path=rpath, no_tag_node_value_mangle=no_tag_node_value_mangle) if with_defaults or with_recursive_defaults: defaults = self.get_config_defaults(**kwargs, recursive=with_recursive_defaults) conf_dict = config_dict_merge(defaults, conf_dict) else: conf_dict = ConfigDict(conf_dict) if with_pki and conf_dict: pki_dict = self.get_config_dict(['pki'], key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True) if pki_dict: + if 'certificate' in pki_dict: + for certificate in pki_dict['certificate']: + pki_dict['certificate'][certificate] = config_dict_mangle_acme( + certificate, pki_dict['certificate'][certificate]) + conf_dict['pki'] = pki_dict # save optional args for a call to get_config_defaults setattr(conf_dict, '_dict_kwargs', kwargs) return conf_dict def get_config_defaults(self, path=[], effective=False, key_mangling=None, no_tag_node_value_mangle=False, get_first_key=False, recursive=False) -> dict: lpath = self._make_path(path) root_dict = self.get_cached_root_dict(effective) conf_dict = get_sub_dict(root_dict, lpath, get_first_key) defaults = relative_defaults(lpath, conf_dict, get_first_key=get_first_key, recursive=recursive) rpath = lpath if get_first_key else lpath[:-1] if key_mangling is not None: self.verify_mangling(key_mangling) defaults = mangle_dict_keys(defaults, key_mangling[0], key_mangling[1], abs_path=rpath, no_tag_node_value_mangle=no_tag_node_value_mangle) return defaults def merge_defaults(self, config_dict: ConfigDict, recursive=False): if not isinstance(config_dict, ConfigDict): raise TypeError('argument is not of type ConfigDict') if not config_dict.kwargs: raise ValueError('argument missing metadata') args = config_dict.kwargs d = self.get_config_defaults(**args, recursive=recursive) config_dict = config_dict_merge(d, config_dict) return config_dict def is_multi(self, path): """ Args: path (str): Configuration tree path Returns: True if a node can have multiple values, False otherwise. Note: It also returns False if node doesn't exist. """ self._config_source.set_level(self.get_level) return self._config_source.is_multi(path) def is_tag(self, path): """ Args: path (str): Configuration tree path Returns: True if a node is a tag node, False otherwise. Note: It also returns False if node doesn't exist. """ self._config_source.set_level(self.get_level) return self._config_source.is_tag(path) def is_leaf(self, path): """ Args: path (str): Configuration tree path Returns: True if a node is a leaf node, False otherwise. Note: It also returns False if node doesn't exist. """ self._config_source.set_level(self.get_level) return self._config_source.is_leaf(path) def return_value(self, path, default=None): """ Retrieve a value of single-value leaf node in the running or proposed config Args: path (str): Configuration tree path default (str): Default value to return if node does not exist Returns: str: Node value, if it has any None: if node is valueless *or* if it doesn't exist Note: Due to the issue with treatment of valueless nodes by this function, valueless nodes should be checked with ``exists`` instead. This function cannot be used outside a configuration session. In operational mode scripts, use ``return_effective_value``. """ if self._session_config: try: value = self._session_config.return_value(self._make_path(path)) except vyos.configtree.ConfigTreeError: value = None else: value = None if not value: return(default) else: return(value) def return_values(self, path, default=[]): """ Retrieve all values of a multi-value leaf node in the running or proposed config Args: path (str): Configuration tree path Returns: str list: Node values, if it has any []: if node does not exist Note: This function cannot be used outside a configuration session. In operational mode scripts, use ``return_effective_values``. """ if self._session_config: try: values = self._session_config.return_values(self._make_path(path)) except vyos.configtree.ConfigTreeError: values = [] else: values = [] if not values: return(default.copy()) else: return(values) def list_nodes(self, path, default=[]): """ Retrieve names of all children of a tag node in the running or proposed config Args: path (str): Configuration tree path Returns: string list: child node names """ if self._session_config: try: nodes = self._session_config.list_nodes(self._make_path(path)) except vyos.configtree.ConfigTreeError: nodes = [] else: nodes = [] if not nodes: return(default.copy()) else: return(nodes) def exists_effective(self, path): """ Checks if a node or value exists in the running (effective) config. Args: path (str): Configuration tree path Returns: True if node exists in the running config, False otherwise Note: This function is safe to use in operational mode. In configuration mode, it ignores uncommited changes. """ if self._running_config is None: return False # Assume the path is a node path first if self._running_config.exists(self._make_path(path)): return True else: # If that check fails, it may mean the path has a value at the end. # libvyosconfig exists() works only for _nodes_, not _values_ # libvyattacfg also worked for values, so we emulate that case here if isinstance(path, str): path = re.split(r'\s+', path) path_without_value = path[:-1] try: # return_values() is safe to use with single-value nodes, # it simply returns a single-item list in that case. values = self._running_config.return_values(self._make_path(path_without_value)) # If we got this far, the node does exist and has values, # so we need to check if it has the value in question among its values. return (path[-1] in values) except vyos.configtree.ConfigTreeError: # Even the parent node doesn't exist at all return False def return_effective_value(self, path, default=None): """ Retrieve a values of a single-value leaf node in a running (effective) config Args: path (str): Configuration tree path default (str): Default value to return if node does not exist Returns: str: Node value """ if self._running_config: try: value = self._running_config.return_value(self._make_path(path)) except vyos.configtree.ConfigTreeError: value = None else: value = None if not value: return(default) else: return(value) def return_effective_values(self, path, default=[]): """ Retrieve all values of a multi-value node in a running (effective) config Args: path (str): Configuration tree path Returns: str list: A list of values """ if self._running_config: try: values = self._running_config.return_values(self._make_path(path)) except vyos.configtree.ConfigTreeError: values = [] else: values = [] if not values: return(default.copy()) else: return(values) def list_effective_nodes(self, path, default=[]): """ Retrieve names of all children of a tag node in the running config Args: path (str): Configuration tree path Returns: str list: child node names """ if self._running_config: try: nodes = self._running_config.list_nodes(self._make_path(path)) except vyos.configtree.ConfigTreeError: nodes = [] else: nodes = [] if not nodes: return(default.copy()) else: return(nodes) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 17e12bcaf..f5369ee7a 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -1,898 +1,899 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2023 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 os import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import call from vyos.utils.process import process_named_running from vyos.utils.file import read_file ethernet_path = ['interfaces', 'ethernet'] tunnel_path = ['interfaces', 'tunnel'] vti_path = ['interfaces', 'vti'] nhrp_path = ['protocols', 'nhrp'] base_path = ['vpn', 'ipsec'] charon_file = '/etc/strongswan.d/charon.conf' dhcp_waiting_file = '/tmp/ipsec_dhcp_waiting' swanctl_file = '/etc/swanctl/swanctl.conf' peer_ip = '203.0.113.45' connection_name = 'main-branch' local_id = 'left' remote_id = 'right' interface = 'eth1' vif = '100' esp_group = 'MyESPGroup' ike_group = 'MyIKEGroup' secret = 'MYSECRETKEY' PROCESS_NAME = 'charon-systemd' regex_uuid4 = '[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}' ca_name = 'MyVyOS-CA' ca_pem = """ MIICMDCCAdegAwIBAgIUBCzIjYvD7SPbx5oU18IYg7NVxQ0wCgYIKoZIzj0EAwIw ZzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEgMB4GA1UEAwwXSVBTZWMgU21va2V0ZXN0 IFJvb3QgQ0EwHhcNMjMwOTI0MTIwMzQxWhcNMzMwOTIxMTIwMzQxWjBnMQswCQYD VQQGEwJHQjETMBEGA1UECAwKU29tZS1TdGF0ZTESMBAGA1UEBwwJU29tZS1DaXR5 MQ0wCwYDVQQKDARWeU9TMSAwHgYDVQQDDBdJUFNlYyBTbW9rZXRlc3QgUm9vdCBD QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEh8/yU572B3zmFxrGgHk+H7grYt EHUJodY3gXNWMHz0gySrbGhsGtECDfP/G+T4Suk7cuVzB1wnLocSafD8TcqjYTBf MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsG AQUFBwMCBggrBgEFBQcDATAdBgNVHQ4EFgQUTYoQJNlk7X87/gRegHnCnPef39Aw CgYIKoZIzj0EAwIDRwAwRAIgX1spXjrUc10r3g/Zm4O31LU5O08J2vVqFo94zHE5 0VgCIG4JK9Zg5O/yn4mYksZux7efiHRUzL2y2TXQ9IqrqM8W """ int_ca_name = 'MyVyOS-IntCA' int_ca_pem = """ MIICYDCCAgWgAwIBAgIUcFx2BVYErHI+SneyPYHijxXt1cgwCgYIKoZIzj0EAwIw ZzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEgMB4GA1UEAwwXSVBTZWMgU21va2V0ZXN0 IFJvb3QgQ0EwHhcNMjMwOTI0MTIwNTE5WhcNMzMwOTIwMTIwNTE5WjBvMQswCQYD VQQGEwJHQjETMBEGA1UECAwKU29tZS1TdGF0ZTESMBAGA1UEBwwJU29tZS1DaXR5 MQ0wCwYDVQQKDARWeU9TMSgwJgYDVQQDDB9JUFNlYyBTbW9rZXRlc3QgSW50ZXJt ZWRpYXRlIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIHw2G5dq3c715AcA tzR++dYu1fLRFmHzRGTZOT7hLrh2Fg4hnKFPLOeUA5Qi50xCvjJ9JnonTyy2RfRH axYizKOBhjCBgzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAd BgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwHQYDVR0OBBYEFC9KrFYtA+hO l7vdMbWxTMAyLB7BMB8GA1UdIwQYMBaAFE2KECTZZO1/O/4EXoB5wpz3n9/QMAoG CCqGSM49BAMCA0kAMEYCIQCnqWbElgOL9dGO3iLxasFNq/hM7vM/DzaiHi4BowxW 0gIhAMohefNj+QgLfPhvyODHIPE9LMyfp7lJEaCC2K8PCSFD """ peer_name = 'peer1' peer_cert = """ MIICSTCCAfCgAwIBAgIUPxYleUgCo/glVVePze3QmAFgi6MwCgYIKoZIzj0EAwIw bzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEoMCYGA1UEAwwfSVBTZWMgU21va2V0ZXN0 IEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MjQxMjA2NDJaFw0yODA5MjIxMjA2NDJa MGQxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlT b21lLUNpdHkxDTALBgNVBAoMBFZ5T1MxHTAbBgNVBAMMFElQU2VjIFNtb2tldGVz dCBQZWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZJtuTDu84uy++GMwRNLl 10JAXZxXQSDl+CdTWwjbQZURcdY+ia7BoaoYX/0VKPel3Se64rIUQQLQoY/9MJb9 UKN1MHMwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI KwYBBQUHAwEwHQYDVR0OBBYEFNJCdnkm3cAmf04UwOKL7IqMJ6OXMB8GA1UdIwQY MBaAFC9KrFYtA+hOl7vdMbWxTMAyLB7BMAoGCCqGSM49BAMCA0cAMEQCIGVnDRUy UJ0U/deDvrBo1+AakZndkNAMN/XNo5a5GzhEAiBCY7E/3b0BIO8FiIbVB3iDcaxg g7ET2RgWxvhEoN3ZRw== """ peer_key = """ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVDEZDK7q/T+tiJUV WLKS3ZYDfZ4lZv0C1gJpYq0gWP2hRANCAARkm25MO7zi7L74YzBE0uXXQkBdnFdB IOX4J1NbCNtBlRFx1j6JrsGhqhhf/RUo96XdJ7rishRBAtChj/0wlv1Q """ swanctl_dir = '/etc/swanctl' CERT_PATH = f'{swanctl_dir}/x509/' CA_PATH = f'{swanctl_dir}/x509ca/' class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): skip_process_check = False @classmethod def setUpClass(cls): super(TestVPNIPsec, 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, ['pki']) cls.cli_set(cls, base_path + ['interface', f'{interface}.{vif}']) @classmethod def tearDownClass(cls): super(TestVPNIPsec, cls).tearDownClass() cls.cli_delete(cls, base_path + ['interface', f'{interface}.{vif}']) def setUp(self): # Set IKE/ESP Groups self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes128']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha1']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '2']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes128']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha1']) def tearDown(self): # Check for running process if not self.skip_process_check: self.assertTrue(process_named_running(PROCESS_NAME)) else: self.skip_process_check = False # Reset self.cli_delete(base_path) self.cli_delete(tunnel_path) self.cli_commit() # Check for no longer running process self.assertFalse(process_named_running(PROCESS_NAME)) def setupPKI(self): self.cli_set(['pki', 'ca', ca_name, 'certificate', ca_pem.replace('\n','')]) self.cli_set(['pki', 'ca', int_ca_name, 'certificate', int_ca_pem.replace('\n','')]) self.cli_set(['pki', 'certificate', peer_name, 'certificate', peer_cert.replace('\n','')]) self.cli_set(['pki', 'certificate', peer_name, 'private', 'key', peer_key.replace('\n','')]) def tearDownPKI(self): self.cli_delete(['pki']) def test_01_dhcp_fail_handling(self): # Skip process check - connection is not created for this test self.skip_process_check = True # Interface for dhcp-interface self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp']) # Use VLAN to avoid getting IP from qemu dhcp server # vpn ipsec auth psk <tag> id <x.x.x.x> self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) # Site to site peer_base_path = base_path + ['site-to-site', 'peer', connection_name] self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) self.cli_set(peer_base_path + ['ike-group', ike_group]) self.cli_set(peer_base_path + ['default-esp-group', esp_group]) self.cli_set(peer_base_path + ['dhcp-interface', f'{interface}.{vif}']) self.cli_set(peer_base_path + ['tunnel', '1', 'protocol', 'gre']) self.cli_commit() self.assertTrue(os.path.exists(dhcp_waiting_file)) dhcp_waiting = read_file(dhcp_waiting_file) self.assertIn(f'{interface}.{vif}', dhcp_waiting) # Ensure dhcp-failed interface was added for dhclient hook self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address']) def test_02_site_to_site(self): self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) local_address = '192.0.2.10' priority = '20' life_bytes = '100000' life_packets = '2000000' # vpn ipsec auth psk <tag> id <x.x.x.x> self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) # Site to site peer_base_path = base_path + ['site-to-site', 'peer', connection_name] self.cli_set(base_path + ['esp-group', esp_group, 'life-bytes', life_bytes]) self.cli_set(base_path + ['esp-group', esp_group, 'life-packets', life_packets]) self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) self.cli_set(peer_base_path + ['ike-group', ike_group]) self.cli_set(peer_base_path + ['default-esp-group', esp_group]) self.cli_set(peer_base_path + ['local-address', local_address]) self.cli_set(peer_base_path + ['remote-address', peer_ip]) self.cli_set(peer_base_path + ['tunnel', '1', 'protocol', 'tcp']) self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.10.0/24']) self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.11.0/24']) self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'port', '443']) self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.10.0/24']) self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.11.0/24']) self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'port', '443']) self.cli_set(peer_base_path + ['tunnel', '2', 'local', 'prefix', '10.1.0.0/16']) self.cli_set(peer_base_path + ['tunnel', '2', 'remote', 'prefix', '10.2.0.0/16']) self.cli_set(peer_base_path + ['tunnel', '2', 'priority', priority]) self.cli_commit() # Verify strongSwan configuration swanctl_conf = read_file(swanctl_file) swanctl_conf_lines = [ f'version = 2', f'auth = psk', f'life_bytes = {life_bytes}', f'life_packets = {life_packets}', f'rekey_time = 28800s', # default value f'proposals = aes128-sha1-modp1024', f'esp_proposals = aes128-sha1-modp1024', f'life_time = 3600s', # default value f'local_addrs = {local_address} # dhcp:no', f'remote_addrs = {peer_ip}', f'mode = tunnel', f'{connection_name}-tunnel-1', f'local_ts = 172.16.10.0/24[tcp/443],172.16.11.0/24[tcp/443]', f'remote_ts = 172.17.10.0/24[tcp/443],172.17.11.0/24[tcp/443]', f'mode = tunnel', f'{connection_name}-tunnel-2', f'local_ts = 10.1.0.0/16', f'remote_ts = 10.2.0.0/16', f'priority = {priority}', f'mode = tunnel', ] for line in swanctl_conf_lines: self.assertIn(line, swanctl_conf) swanctl_secrets_lines = [ f'id-{regex_uuid4} = "{local_id}"', f'id-{regex_uuid4} = "{remote_id}"', f'id-{regex_uuid4} = "{local_address}"', f'id-{regex_uuid4} = "{peer_ip}"', f'secret = "{secret}"' ] for line in swanctl_secrets_lines: self.assertRegex(swanctl_conf, fr'{line}') def test_03_site_to_site_vti(self): local_address = '192.0.2.10' vti = 'vti10' # IKE self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) self.cli_set(base_path + ['ike-group', ike_group, 'disable-mobike']) # ESP self.cli_set(base_path + ['esp-group', esp_group, 'compression']) # VTI interface self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24']) # vpn ipsec auth psk <tag> id <x.x.x.x> self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) # Site to site peer_base_path = base_path + ['site-to-site', 'peer', connection_name] self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) self.cli_set(peer_base_path + ['connection-type', 'none']) self.cli_set(peer_base_path + ['force-udp-encapsulation']) self.cli_set(peer_base_path + ['ike-group', ike_group]) self.cli_set(peer_base_path + ['default-esp-group', esp_group]) self.cli_set(peer_base_path + ['local-address', local_address]) self.cli_set(peer_base_path + ['remote-address', peer_ip]) self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.10.0/24']) self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.11.0/24']) self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.10.0/24']) self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.11.0/24']) self.cli_set(peer_base_path + ['vti', 'bind', vti]) self.cli_set(peer_base_path + ['vti', 'esp-group', esp_group]) self.cli_commit() swanctl_conf = read_file(swanctl_file) if_id = vti.lstrip('vti') # The key defaults to 0 and will match any policies which similarly do # not have a lookup key configuration - thus we shift the key by one # to also support a vti0 interface if_id = str(int(if_id) +1) swanctl_conf_lines = [ f'version = 2', f'auth = psk', f'proposals = aes128-sha1-modp1024', f'esp_proposals = aes128-sha1-modp1024', f'local_addrs = {local_address} # dhcp:no', f'mobike = no', f'remote_addrs = {peer_ip}', f'mode = tunnel', f'local_ts = 172.16.10.0/24,172.16.11.0/24', f'remote_ts = 172.17.10.0/24,172.17.11.0/24', f'ipcomp = yes', f'start_action = none', f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one f'if_id_out = {if_id}', f'updown = "/etc/ipsec.d/vti-up-down {vti}"' ] for line in swanctl_conf_lines: self.assertIn(line, swanctl_conf) swanctl_secrets_lines = [ f'id-{regex_uuid4} = "{local_id}"', f'id-{regex_uuid4} = "{remote_id}"', f'secret = "{secret}"' ] for line in swanctl_secrets_lines: self.assertRegex(swanctl_conf, fr'{line}') def test_04_dmvpn(self): tunnel_if = 'tun100' nhrp_secret = 'secret' ike_lifetime = '3600' esp_lifetime = '1800' # Tunnel self.cli_set(tunnel_path + [tunnel_if, 'address', '172.16.253.134/29']) self.cli_set(tunnel_path + [tunnel_if, 'encapsulation', 'gre']) self.cli_set(tunnel_path + [tunnel_if, 'source-address', '192.0.2.1']) self.cli_set(tunnel_path + [tunnel_if, 'enable-multicast']) self.cli_set(tunnel_path + [tunnel_if, 'parameters', 'ip', 'key', '1']) # NHRP self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'cisco-authentication', nhrp_secret]) self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'holding-time', '300']) self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'multicast', 'dynamic']) self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'redirect']) self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'shortcut']) # IKE/ESP Groups self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', esp_lifetime]) self.cli_set(base_path + ['esp-group', esp_group, 'mode', 'transport']) self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'dh-group2']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha1']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', '3des']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'md5']) self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev1']) self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime]) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '2']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha1']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'prf', 'prfsha1']) # Profile self.cli_set(base_path + ['profile', 'NHRPVPN', 'authentication', 'mode', 'pre-shared-secret']) self.cli_set(base_path + ['profile', 'NHRPVPN', 'authentication', 'pre-shared-secret', nhrp_secret]) self.cli_set(base_path + ['profile', 'NHRPVPN', 'bind', 'tunnel', tunnel_if]) self.cli_set(base_path + ['profile', 'NHRPVPN', 'esp-group', esp_group]) self.cli_set(base_path + ['profile', 'NHRPVPN', 'ike-group', ike_group]) self.cli_commit() swanctl_conf = read_file(swanctl_file) swanctl_lines = [ f'proposals = aes128-sha1-modp1024,aes256-sha1-prfsha1-modp1024', f'version = 1', f'rekey_time = {ike_lifetime}s', f'rekey_time = {esp_lifetime}s', f'esp_proposals = aes128-sha1-modp1024,aes256-sha1-modp1024,3des-md5-modp1024', f'local_ts = dynamic[gre]', f'remote_ts = dynamic[gre]', f'mode = transport', f'secret = {nhrp_secret}' ] for line in swanctl_lines: self.assertIn(line, swanctl_conf) # There is only one NHRP test so no need to delete this globally in tearDown() self.cli_delete(nhrp_path) def test_05_x509_site2site(self): # Enable PKI self.setupPKI() vti = 'vti20' self.cli_set(vti_path + [vti, 'address', '192.168.0.1/31']) peer_ip = '172.18.254.202' connection_name = 'office' local_address = '172.18.254.201' peer_base_path = base_path + ['site-to-site', 'peer', connection_name] self.cli_set(peer_base_path + ['authentication', 'local-id', peer_name]) self.cli_set(peer_base_path + ['authentication', 'mode', 'x509']) self.cli_set(peer_base_path + ['authentication', 'remote-id', 'peer2']) self.cli_set(peer_base_path + ['authentication', 'x509', 'ca-certificate', int_ca_name]) self.cli_set(peer_base_path + ['authentication', 'x509', 'certificate', peer_name]) self.cli_set(peer_base_path + ['connection-type', 'initiate']) self.cli_set(peer_base_path + ['ike-group', ike_group]) self.cli_set(peer_base_path + ['ikev2-reauth', 'inherit']) self.cli_set(peer_base_path + ['local-address', local_address]) self.cli_set(peer_base_path + ['remote-address', peer_ip]) self.cli_set(peer_base_path + ['vti', 'bind', vti]) self.cli_set(peer_base_path + ['vti', 'esp-group', esp_group]) self.cli_commit() swanctl_conf = read_file(swanctl_file) tmp = peer_ip.replace('.', '-') if_id = vti.lstrip('vti') # The key defaults to 0 and will match any policies which similarly do # not have a lookup key configuration - thus we shift the key by one # to also support a vti0 interface if_id = str(int(if_id) +1) swanctl_lines = [ f'{connection_name}', f'version = 0', # key-exchange not set - defaulting to 0 for ikev1 and ikev2 f'send_cert = always', f'mobike = yes', f'keyingtries = 0', f'id = "{peer_name}"', f'auth = pubkey', f'certs = {peer_name}.pem', f'proposals = aes128-sha1-modp1024', f'esp_proposals = aes128-sha1-modp1024', f'local_addrs = {local_address} # dhcp:no', f'remote_addrs = {peer_ip}', f'local_ts = 0.0.0.0/0,::/0', f'remote_ts = 0.0.0.0/0,::/0', f'updown = "/etc/ipsec.d/vti-up-down {vti}"', f'if_id_in = {if_id}', # will be 11 for vti10 f'if_id_out = {if_id}', f'ipcomp = no', f'mode = tunnel', f'start_action = start', ] for line in swanctl_lines: self.assertIn(line, swanctl_conf) swanctl_secrets_lines = [ f'{connection_name}', f'file = {peer_name}.pem', ] for line in swanctl_secrets_lines: self.assertIn(line, swanctl_conf) # Check Root CA, Intermediate CA and Peer cert/key pair is present self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}_1.pem'))) self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}_2.pem'))) self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem'))) # There is only one VTI test so no need to delete this globally in tearDown() self.cli_delete(vti_path) # Disable PKI self.tearDownPKI() def test_06_flex_vpn_vips(self): local_address = '192.0.2.5' local_id = 'vyos-r1' remote_id = 'vyos-r2' peer_base_path = base_path + ['site-to-site', 'peer', connection_name] self.cli_set(tunnel_path + ['tun1', 'encapsulation', 'gre']) self.cli_set(tunnel_path + ['tun1', 'source-address', local_address]) self.cli_set(base_path + ['interface', interface]) self.cli_set(base_path + ['options', 'flexvpn']) self.cli_set(base_path + ['options', 'interface', 'tun1']) self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) # vpn ipsec auth psk <tag> id <x.x.x.x> self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) self.cli_set(peer_base_path + ['authentication', 'local-id', local_id]) self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) self.cli_set(peer_base_path + ['authentication', 'remote-id', remote_id]) self.cli_set(peer_base_path + ['connection-type', 'initiate']) self.cli_set(peer_base_path + ['ike-group', ike_group]) self.cli_set(peer_base_path + ['default-esp-group', esp_group]) self.cli_set(peer_base_path + ['local-address', local_address]) self.cli_set(peer_base_path + ['remote-address', peer_ip]) self.cli_set(peer_base_path + ['tunnel', '1', 'protocol', 'gre']) self.cli_set(peer_base_path + ['virtual-address', '203.0.113.55']) self.cli_set(peer_base_path + ['virtual-address', '203.0.113.56']) self.cli_commit() # Verify strongSwan configuration swanctl_conf = read_file(swanctl_file) swanctl_conf_lines = [ f'version = 2', f'vips = 203.0.113.55, 203.0.113.56', f'life_time = 3600s', # default value f'local_addrs = {local_address} # dhcp:no', f'remote_addrs = {peer_ip}', f'{connection_name}-tunnel-1', f'mode = tunnel', ] for line in swanctl_conf_lines: self.assertIn(line, swanctl_conf) swanctl_secrets_lines = [ f'id-{regex_uuid4} = "{local_id}"', f'id-{regex_uuid4} = "{remote_id}"', f'id-{regex_uuid4} = "{peer_ip}"', f'id-{regex_uuid4} = "{local_address}"', f'secret = "{secret}"', ] for line in swanctl_secrets_lines: self.assertRegex(swanctl_conf, fr'{line}') # Verify charon configuration charon_conf = read_file(charon_file) charon_conf_lines = [ f'# Cisco FlexVPN', f'cisco_flexvpn = yes', f'install_virtual_ip = yes', f'install_virtual_ip_on = tun1', ] for line in charon_conf_lines: self.assertIn(line, charon_conf) def test_07_ikev2_road_warrior(self): # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17 self.setupPKI() ike_group = 'IKE-RW' esp_group = 'ESP-RW' conn_name = 'vyos-rw' local_address = '192.0.2.1' ip_pool_name = 'ra-rw-ipv4' username = 'vyos' password = 'secret' ike_lifetime = '7200' eap_lifetime = '3600' local_id = 'ipsec.vyos.net' name_servers = ['172.16.254.100', '172.16.254.101'] prefix = '172.16.250.0/28' # IKE self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime]) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256']) # ESP self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime]) self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256']) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509']) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name]) # verify() - CA cert required for x509 auth with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name]) for ns in name_servers: self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns]) self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix]) self.cli_commit() # verify applied configuration swanctl_conf = read_file(swanctl_file) swanctl_lines = [ f'{conn_name}', f'remote_addrs = %any', f'local_addrs = {local_address}', f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048', f'version = 2', f'send_certreq = no', f'rekey_time = {ike_lifetime}s', f'keyingtries = 0', f'pools = {ip_pool_name}', f'id = "{local_id}"', f'auth = pubkey', f'certs = peer1.pem', f'auth = eap-mschapv2', f'eap_id = %any', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', f'rekey_time = {eap_lifetime}s', f'rand_time = 540s', f'dpd_action = clear', f'inactivity = 28800', f'local_ts = 0.0.0.0/0,::/0', ] for line in swanctl_lines: self.assertIn(line, swanctl_conf) swanctl_secrets_lines = [ f'eap-{conn_name}-{username}', f'secret = "{password}"', f'id-{conn_name}-{username} = "{username}"', ] for line in swanctl_secrets_lines: self.assertIn(line, swanctl_conf) swanctl_pool_lines = [ f'{ip_pool_name}', f'addrs = {prefix}', f'dns = {",".join(name_servers)}', ] for line in swanctl_pool_lines: self.assertIn(line, swanctl_conf) # Check Root CA, Intermediate CA and Peer cert/key pair is present self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}_1.pem'))) self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem'))) self.tearDownPKI() def test_08_ikev2_road_warrior_client_auth_eap_tls(self): # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17 self.setupPKI() ike_group = 'IKE-RW' esp_group = 'ESP-RW' conn_name = 'vyos-rw' local_address = '192.0.2.1' ip_pool_name = 'ra-rw-ipv4' username = 'vyos' password = 'secret' ike_lifetime = '7200' eap_lifetime = '3600' local_id = 'ipsec.vyos.net' name_servers = ['172.16.254.100', '172.16.254.101'] prefix = '172.16.250.0/28' # IKE self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime]) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256']) # ESP self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime]) self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256']) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id]) # Use EAP-TLS auth instead of default EAP-MSCHAPv2 self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'eap-tls']) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509']) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name]) # verify() - CA cert required for x509 auth with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name]) for ns in name_servers: self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns]) self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix]) self.cli_commit() # verify applied configuration swanctl_conf = read_file(swanctl_file) swanctl_lines = [ f'{conn_name}', f'remote_addrs = %any', f'local_addrs = {local_address}', f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048', f'version = 2', f'send_certreq = no', f'rekey_time = {ike_lifetime}s', f'keyingtries = 0', f'pools = {ip_pool_name}', f'id = "{local_id}"', f'auth = pubkey', f'certs = peer1.pem', f'auth = eap-tls', f'eap_id = %any', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', f'rekey_time = {eap_lifetime}s', f'rand_time = 540s', f'dpd_action = clear', f'inactivity = 28800', f'local_ts = 0.0.0.0/0,::/0', ] for line in swanctl_lines: self.assertIn(line, swanctl_conf) swanctl_pool_lines = [ f'{ip_pool_name}', f'addrs = {prefix}', f'dns = {",".join(name_servers)}', ] for line in swanctl_pool_lines: self.assertIn(line, swanctl_conf) # Check Root CA, Intermediate CA and Peer cert/key pair is present self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}_1.pem'))) self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem'))) self.tearDownPKI() def test_09_ikev2_road_warrior_client_auth_x509(self): # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17 self.setupPKI() ike_group = 'IKE-RW' esp_group = 'ESP-RW' conn_name = 'vyos-rw' local_address = '192.0.2.1' ip_pool_name = 'ra-rw-ipv4' ike_lifetime = '7200' eap_lifetime = '3600' local_id = 'ipsec.vyos.net' name_servers = ['172.16.254.100', '172.16.254.101'] prefix = '172.16.250.0/28' # IKE self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime]) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256']) # ESP self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime]) self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256']) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id]) # Use client-mode x509 instead of default EAP-MSCHAPv2 self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'x509']) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509']) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name]) # verify() - CA cert required for x509 auth with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address]) self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name]) for ns in name_servers: self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns]) self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix]) self.cli_commit() # verify applied configuration swanctl_conf = read_file(swanctl_file) swanctl_lines = [ f'{conn_name}', f'remote_addrs = %any', f'local_addrs = {local_address}', f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048', f'version = 2', f'send_certreq = no', f'rekey_time = {ike_lifetime}s', f'keyingtries = 0', f'pools = {ip_pool_name}', f'id = "{local_id}"', f'auth = pubkey', f'certs = peer1.pem', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', f'rekey_time = {eap_lifetime}s', f'rand_time = 540s', f'dpd_action = clear', f'inactivity = 28800', f'local_ts = 0.0.0.0/0,::/0', ] for line in swanctl_lines: self.assertIn(line, swanctl_conf) swanctl_unexpected_lines = [ f'auth = eap-', f'eap_id' ] for unexpected_line in swanctl_unexpected_lines: self.assertNotIn(unexpected_line, swanctl_conf) swanctl_pool_lines = [ f'{ip_pool_name}', f'addrs = {prefix}', f'dns = {",".join(name_servers)}', ] for line in swanctl_pool_lines: self.assertIn(line, swanctl_conf) # Check Root CA, Intermediate CA and Peer cert/key pair is present self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}_1.pem'))) self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem'))) self.tearDownPKI() if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index f7e14aa16..239e44c3b 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -1,305 +1,433 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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 <http://www.gnu.org/licenses/>. +import os + +from sys import argv from sys import exit from vyos.config import Config -from vyos.configdep import set_dependents, call_dependents +from vyos.config import config_dict_merge +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.configdict import node_changed +from vyos.configdiff import Diff +from vyos.defaults import directories from vyos.pki import is_ca_certificate from vyos.pki import load_certificate from vyos.pki import load_public_key from vyos.pki import load_private_key from vyos.pki import load_crl from vyos.pki import load_dh_parameters +from vyos.utils.boot import boot_configuration_complete +from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive +from vyos.utils.process import call +from vyos.utils.process import cmd +from vyos.utils.process import is_systemd_service_active from vyos import ConfigError from vyos import airbag airbag.enable() -# keys to recursively search for under specified path, script to call if update required +vyos_certbot_dir = directories['certbot'] + +# keys to recursively search for under specified path sync_search = [ { 'keys': ['certificate'], 'path': ['service', 'https'], - 'script': '/usr/libexec/vyos/conf_mode/service_https.py' }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['interfaces', 'ethernet'], - 'script': '/usr/libexec/vyos/conf_mode/interfaces_ethernet.py' }, { 'keys': ['certificate', 'ca_certificate', 'dh_params', 'shared_secret_key', 'auth_key', 'crypt_key'], 'path': ['interfaces', 'openvpn'], - 'script': '/usr/libexec/vyos/conf_mode/interfaces_openvpn.py' }, { 'keys': ['ca_certificate'], 'path': ['interfaces', 'sstpc'], - 'script': '/usr/libexec/vyos/conf_mode/interfaces_sstpc.py' }, { 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'], 'path': ['vpn', 'ipsec'], - 'script': '/usr/libexec/vyos/conf_mode/vpn_ipsec.py' }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['vpn', 'openconnect'], - 'script': '/usr/libexec/vyos/conf_mode/vpn_openconnect.py' }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['vpn', 'sstp'], - 'script': '/usr/libexec/vyos/conf_mode/vpn_sstp.py' } ] # key from other config nodes -> key in pki['changed'] and pki sync_translate = { 'certificate': 'certificate', 'ca_certificate': 'ca', 'dh_params': 'dh', 'local_key': 'key_pair', 'remote_key': 'key_pair', 'shared_secret_key': 'openvpn', 'auth_key': 'openvpn', 'crypt_key': 'openvpn' } +def certbot_delete(certificate): + if not boot_configuration_complete(): + return + if os.path.exists(f'{vyos_certbot_dir}/renewal/{certificate}.conf'): + cmd(f'certbot delete --non-interactive --config-dir {vyos_certbot_dir} --cert-name {certificate}') + +def certbot_request(name: str, config: dict, dry_run: bool=True): + # We do not call certbot when booting the system - there is no need to do so and + # request new certificates during boot/image upgrade as the certbot configuration + # is stored persistent under /config - thus we do not open the door to transient + # errors + if not boot_configuration_complete(): + return + + domains = '--domains ' + ' --domains '.join(config['domain_name']) + tmp = f'certbot certonly --non-interactive --config-dir {vyos_certbot_dir} --cert-name {name} '\ + f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\ + f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\ + f'{domains}' + if 'listen_address' in config: + tmp += f' --http-01-address {config["listen_address"]}' + # verify() does not need to actually request a cert but only test for plausability + if dry_run: + tmp += ' --dry-run' + + cmd(tmp, raising=ConfigError, message=f'ACME certbot request failed for "{name}"!') + def get_config(config=None): if config: conf = config else: conf = Config() base = ['pki'] pki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - pki['changed'] = {} + if len(argv) > 1 and argv[1] == 'certbot_renew': + pki['certbot_renew'] = {} + tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'ca' : tmp}) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'ca' : tmp}) tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'certificate' : tmp}) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'certificate' : tmp}) tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'dh' : tmp}) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'dh' : tmp}) tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'key_pair' : tmp}) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'key_pair' : tmp}) - tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'openvpn' : tmp}) + tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), + recursive=True) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'openvpn' : tmp}) # We only merge on the defaults of there is a configuration at all if conf.exists(base): - pki = conf.merge_defaults(pki, recursive=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(**pki.kwargs, recursive=True) + # remove ACME default configuration if unused by CLI + if 'certificate' in pki: + for name, cert_config in pki['certificate'].items(): + if 'acme' not in cert_config: + # Remove ACME default values + del default_values['certificate'][name]['acme'] + + # merge CLI and default dictionary + pki = config_dict_merge(default_values, pki) + + # Certbot triggered an external renew of the certificates. + # Mark all ACME based certificates as "changed" to trigger + # update of dependent services + if 'certificate' in pki and 'certbot_renew' in pki: + renew = [] + for name, cert_config in pki['certificate'].items(): + if 'acme' in cert_config: + renew.append(name) + # If triggered externally by certbot, certificate key is not present in changed + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'certificate' : renew}) # We need to get the entire system configuration to verify that we are not # deleting a certificate that is still referenced somewhere! pki['system'] = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - if 'changed' in pki: - for search in sync_search: - for key in search['keys']: - changed_key = sync_translate[key] - - if changed_key not in pki['changed']: - continue - - for item_name in pki['changed'][changed_key]: - node_present = False - if changed_key == 'openvpn': - node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) - else: - node_present = dict_search_args(pki, changed_key, item_name) - - if node_present: - search_dict = dict_search_args(pki['system'], *search['path']) - - if not search_dict: - continue - - for found_name, found_path in dict_search_recursive(search_dict, key): - if found_name == item_name: - path = search['path'] - path_str = ' '.join(path + found_path) - print(f'pki: Updating config: {path_str} {found_name}') - - if path[0] == 'interfaces': - ifname = found_path[0] - set_dependents(path[1], conf, ifname) - else: - set_dependents(path[1], conf) + for search in sync_search: + for key in search['keys']: + changed_key = sync_translate[key] + if 'changed' not in pki or changed_key not in pki['changed']: + continue + + for item_name in pki['changed'][changed_key]: + node_present = False + if changed_key == 'openvpn': + node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) + else: + node_present = dict_search_args(pki, changed_key, item_name) + + if node_present: + search_dict = dict_search_args(pki['system'], *search['path']) + if not search_dict: + continue + for found_name, found_path in dict_search_recursive(search_dict, key): + if found_name == item_name: + path = search['path'] + path_str = ' '.join(path + found_path) + print(f'PKI: Updating config: {path_str} {found_name}') + + if path[0] == 'interfaces': + ifname = found_path[0] + set_dependents(path[1], conf, ifname) + else: + set_dependents(path[1], conf) return pki def is_valid_certificate(raw_data): # If it loads correctly we're good, or return False return load_certificate(raw_data, wrap_tags=True) def is_valid_ca_certificate(raw_data): # Check if this is a valid certificate with CA attributes cert = load_certificate(raw_data, wrap_tags=True) if not cert: return False return is_ca_certificate(cert) def is_valid_public_key(raw_data): # If it loads correctly we're good, or return False return load_public_key(raw_data, wrap_tags=True) def is_valid_private_key(raw_data, protected=False): # If it loads correctly we're good, or return False # With encrypted private keys, we always return true as we cannot ask for password to verify if protected: return True return load_private_key(raw_data, passphrase=None, wrap_tags=True) def is_valid_crl(raw_data): # If it loads correctly we're good, or return False return load_crl(raw_data, wrap_tags=True) def is_valid_dh_parameters(raw_data): # If it loads correctly we're good, or return False return load_dh_parameters(raw_data, wrap_tags=True) def verify(pki): if not pki: return None if 'ca' in pki: for name, ca_conf in pki['ca'].items(): if 'certificate' in ca_conf: if not is_valid_ca_certificate(ca_conf['certificate']): raise ConfigError(f'Invalid certificate on CA certificate "{name}"') if 'private' in ca_conf and 'key' in ca_conf['private']: private = ca_conf['private'] protected = 'password_protected' in private if not is_valid_private_key(private['key'], protected): raise ConfigError(f'Invalid private key on CA certificate "{name}"') if 'crl' in ca_conf: ca_crls = ca_conf['crl'] if isinstance(ca_crls, str): ca_crls = [ca_crls] for crl in ca_crls: if not is_valid_crl(crl): raise ConfigError(f'Invalid CRL on CA certificate "{name}"') if 'certificate' in pki: for name, cert_conf in pki['certificate'].items(): if 'certificate' in cert_conf: if not is_valid_certificate(cert_conf['certificate']): raise ConfigError(f'Invalid certificate on certificate "{name}"') if 'private' in cert_conf and 'key' in cert_conf['private']: private = cert_conf['private'] protected = 'password_protected' in private if not is_valid_private_key(private['key'], protected): raise ConfigError(f'Invalid private key on certificate "{name}"') + if 'acme' in cert_conf: + if 'domain_name' not in cert_conf['acme']: + raise ConfigError(f'At least one domain-name is required to request '\ + f'certificate for "{name}" via ACME!') + + if 'email' not in cert_conf['acme']: + raise ConfigError(f'An email address is required to request '\ + f'certificate for "{name}" via ACME!') + + if 'certbot_renew' not in pki: + # Only run the ACME command if something on this entity changed, + # as this is time intensive + tmp = dict_search('changed.certificate', pki) + if tmp != None and name in tmp: + certbot_request(name, cert_conf['acme']) + if 'dh' in pki: for name, dh_conf in pki['dh'].items(): if 'parameters' in dh_conf: if not is_valid_dh_parameters(dh_conf['parameters']): raise ConfigError(f'Invalid DH parameters on "{name}"') if 'key_pair' in pki: for name, key_conf in pki['key_pair'].items(): if 'public' in key_conf and 'key' in key_conf['public']: if not is_valid_public_key(key_conf['public']['key']): raise ConfigError(f'Invalid public key on key-pair "{name}"') if 'private' in key_conf and 'key' in key_conf['private']: private = key_conf['private'] protected = 'password_protected' in private if not is_valid_private_key(private['key'], protected): raise ConfigError(f'Invalid private key on key-pair "{name}"') if 'x509' in pki: if 'default' in pki['x509']: default_values = pki['x509']['default'] if 'country' in default_values: country = default_values['country'] if len(country) != 2 or not country.isalpha(): raise ConfigError(f'Invalid default country value. Value must be 2 alpha characters.') if 'changed' in pki: # if the list is getting longer, we can move to a dict() and also embed the # search key as value from line 173 or 176 for search in sync_search: for key in search['keys']: changed_key = sync_translate[key] if changed_key not in pki['changed']: continue for item_name in pki['changed'][changed_key]: node_present = False if changed_key == 'openvpn': node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) else: node_present = dict_search_args(pki, changed_key, item_name) if not node_present: search_dict = dict_search_args(pki['system'], *search['path']) if not search_dict: continue for found_name, found_path in dict_search_recursive(search_dict, key): if found_name == item_name: path_str = " ".join(search['path'] + found_path) raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"') return None def generate(pki): if not pki: return None + # Certbot renewal only needs to re-trigger the services to load up the + # new PEM file + if 'certbot_renew' in pki: + return None + + certbot_list = [] + certbot_list_on_disk = [] + if os.path.exists(f'{vyos_certbot_dir}/live'): + certbot_list_on_disk = [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()] + + if 'certificate' in pki: + changed_certificates = dict_search('changed.certificate', pki) + for name, cert_conf in pki['certificate'].items(): + if 'acme' in cert_conf: + certbot_list.append(name) + # generate certificate if not found on disk + if name not in certbot_list_on_disk: + certbot_request(name, cert_conf['acme'], dry_run=False) + elif changed_certificates != None and name in changed_certificates: + # when something for the certificate changed, we should delete it + if name in certbot_list_on_disk: + certbot_delete(name) + certbot_request(name, cert_conf['acme'], dry_run=False) + + # Cleanup certbot configuration and certificates if no longer in use by CLI + # Get foldernames under vyos_certbot_dir which each represent a certbot cert + if os.path.exists(f'{vyos_certbot_dir}/live'): + for cert in certbot_list_on_disk: + if cert not in certbot_list: + # certificate is no longer active on the CLI - remove it + certbot_delete(cert) + return None def apply(pki): + systemd_certbot_name = 'certbot.timer' if not pki: + call(f'systemctl stop {systemd_certbot_name}') return None + has_certbot = False + if 'certificate' in pki: + for name, cert_conf in pki['certificate'].items(): + if 'acme' in cert_conf: + has_certbot = True + break + + if not has_certbot: + call(f'systemctl stop {systemd_certbot_name}') + elif has_certbot and not is_systemd_service_active(systemd_certbot_name): + call(f'systemctl restart {systemd_certbot_name}') + if 'changed' in pki: call_dependents() return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py index cb40acc9f..2e7ebda5a 100755 --- a/src/conf_mode/service_https.py +++ b/src/conf_mode/service_https.py @@ -1,330 +1,289 @@ #!/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 <http://www.gnu.org/licenses/>. import os import sys import json from copy import deepcopy from time import sleep import vyos.defaults -import vyos.certbot_util from vyos.base import Warning from vyos.config import Config from vyos.configdiff import get_config_diff from vyos.configverify import verify_vrf from vyos import ConfigError from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.utils.process import call -from vyos.utils.process import is_systemd_service_running -from vyos.utils.process import is_systemd_service_active from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service from vyos.utils.file import write_file from vyos import airbag airbag.enable() config_file = '/etc/nginx/sites-available/default' systemd_override = r'/run/systemd/system/nginx.service.d/override.conf' cert_dir = '/etc/ssl/certs' key_dir = '/etc/ssl/private' -certbot_dir = vyos.defaults.directories['certbot'] api_config_state = '/run/http-api-state' systemd_service = '/run/systemd/system/vyos-http-api.service' -# https config needs to coordinate several subsystems: api, certbot, +# https config needs to coordinate several subsystems: api, # self-signed certificate, as well as the virtual hosts defined within the # https config definition itself. Consequently, one needs a general dict, # encompassing the https and other configs, and a list of such virtual hosts # (server blocks in nginx terminology) to pass to the jinja2 template. default_server_block = { 'id' : '', 'address' : '*', 'port' : '443', 'name' : ['_'], 'api' : False, 'vyos_cert' : {}, - 'certbot' : False } def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'https'] if not conf.exists(base): return None diff = get_config_diff(conf) https = conf.get_config_dict(base, get_first_key=True, with_pki=True) - https['children_changed'] = diff.node_changed_children(base) https['api_add_or_delete'] = diff.node_changed_presence(base + ['api']) if 'api' not in https: return https http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, with_recursive_defaults=True) if http_api.from_defaults(['graphql']): del http_api['graphql'] # Do we run inside a VRF context? vrf_path = ['service', 'https', 'vrf'] if conf.exists(vrf_path): http_api['vrf'] = conf.return_value(vrf_path) https['api'] = http_api - return https def verify(https): from vyos.utils.dict import dict_search if https is None: return None if 'certificates' in https: certificates = https['certificates'] if 'certificate' in certificates: if not https['pki']: raise ConfigError('PKI is not configured') cert_name = certificates['certificate'] if cert_name not in https['pki']['certificate']: - raise ConfigError("Invalid certificate on https configuration") + raise ConfigError('Invalid certificate on https configuration') pki_cert = https['pki']['certificate'][cert_name] if 'certificate' not in pki_cert: - raise ConfigError("Missing certificate on https configuration") + raise ConfigError('Missing certificate on https configuration') if 'private' not in pki_cert or 'key' not in pki_cert['private']: raise ConfigError("Missing certificate private key on https configuration") - - if 'certbot' in https['certificates']: - vhost_names = [] - for _, vh_conf in https.get('virtual-host', {}).items(): - vhost_names += vh_conf.get('server-name', []) - domains = https['certificates']['certbot'].get('domain-name', []) - domains_found = [domain for domain in domains if domain in vhost_names] - if not domains_found: - raise ConfigError("At least one 'virtual-host <id> server-name' " - "matching the 'certbot domain-name' is required.") + else: + Warning('No certificate specified, using buildin self-signed certificates!') server_block_list = [] # organize by vhosts vhost_dict = https.get('virtual-host', {}) if not vhost_dict: # no specified virtual hosts (server blocks); use default server_block_list.append(default_server_block) else: for vhost in list(vhost_dict): server_block = deepcopy(default_server_block) data = vhost_dict.get(vhost, {}) server_block['address'] = data.get('listen-address', '*') server_block['port'] = data.get('port', '443') server_block_list.append(server_block) for entry in server_block_list: _address = entry.get('address') _address = '0.0.0.0' if _address == '*' else _address _port = entry.get('port') proto = 'tcp' if check_port_availability(_address, int(_port), proto) is not True and \ not is_listen_port_bind_service(int(_port), 'nginx'): raise ConfigError(f'"{proto}" port "{_port}" is used by another service') verify_vrf(https) # Verify API server settings, if present if 'api' in https: keys = dict_search('api.keys.id', https) gql_auth_type = dict_search('api.graphql.authentication.type', https) # If "api graphql" is not defined and `gql_auth_type` is None, # there's certainly no JWT auth option, and keys are required jwt_auth = (gql_auth_type == "token") # Check for incomplete key configurations in every case valid_keys_exist = False if keys: for k in keys: if 'key' not in keys[k]: raise ConfigError(f'Missing HTTPS API key string for key id "{k}"') else: valid_keys_exist = True # If only key-based methods are enabled, # fail the commit if no valid key configurations are found if (not valid_keys_exist) and (not jwt_auth): raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled') if (not valid_keys_exist) and jwt_auth: Warning(f'API keys are not configured: the classic (non-GraphQL) API will be unavailable.') return None def generate(https): if https is None: return None if 'api' not in https: if os.path.exists(systemd_service): os.unlink(systemd_service) else: render(systemd_service, 'https/vyos-http-api.service.j2', https['api']) with open(api_config_state, 'w') as f: json.dump(https['api'], f, indent=2) server_block_list = [] # organize by vhosts vhost_dict = https.get('virtual-host', {}) if not vhost_dict: # no specified virtual hosts (server blocks); use default server_block_list.append(default_server_block) else: for vhost in list(vhost_dict): server_block = deepcopy(default_server_block) server_block['id'] = vhost data = vhost_dict.get(vhost, {}) server_block['address'] = data.get('listen-address', '*') server_block['port'] = data.get('port', '443') name = data.get('server-name', ['_']) server_block['name'] = name allow_client = data.get('allow-client', {}) server_block['allow_client'] = allow_client.get('address', []) server_block_list.append(server_block) # get certificate data cert_dict = https.get('certificates', {}) if 'certificate' in cert_dict: cert_name = cert_dict['certificate'] pki_cert = https['pki']['certificate'][cert_name] cert_path = os.path.join(cert_dir, f'{cert_name}.pem') key_path = os.path.join(key_dir, f'{cert_name}.pem') server_cert = str(wrap_certificate(pki_cert['certificate'])) if 'ca-certificate' in cert_dict: ca_cert = cert_dict['ca-certificate'] server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate'])) write_file(cert_path, server_cert) write_file(key_path, wrap_private_key(pki_cert['private']['key'])) vyos_cert_data = { 'crt': cert_path, 'key': key_path } for block in server_block_list: block['vyos_cert'] = vyos_cert_data - # letsencrypt certificate using certbot - - certbot = False - cert_domains = cert_dict.get('certbot', {}).get('domain-name', []) - if cert_domains: - certbot = True - for domain in cert_domains: - sub_list = vyos.certbot_util.choose_server_block(server_block_list, - domain) - if sub_list: - for sb in sub_list: - sb['certbot'] = True - sb['certbot_dir'] = certbot_dir - # certbot organizes certificates by first domain - sb['certbot_domain_dir'] = cert_domains[0] - if 'api' in list(https): vhost_list = https.get('api-restrict', {}).get('virtual-host', []) if not vhost_list: for block in server_block_list: block['api'] = True else: for block in server_block_list: if block['id'] in vhost_list: block['api'] = True data = { 'server_block_list': server_block_list, - 'certbot': certbot } render(config_file, 'https/nginx.default.j2', data) render(systemd_override, 'https/override.conf.j2', https) return None def apply(https): # Reload systemd manager configuration call('systemctl daemon-reload') http_api_service_name = 'vyos-http-api.service' https_service_name = 'nginx.service' if https is None: - if is_systemd_service_active(f'{http_api_service_name}'): - call(f'systemctl stop {http_api_service_name}') + call(f'systemctl stop {http_api_service_name}') call(f'systemctl stop {https_service_name}') return - if 'api' in https['children_changed']: - if 'api' in https: - if is_systemd_service_running(f'{http_api_service_name}'): - call(f'systemctl reload {http_api_service_name}') - else: - call(f'systemctl restart {http_api_service_name}') - # Let uvicorn settle before (possibly) restarting nginx - sleep(1) - else: - if is_systemd_service_active(f'{http_api_service_name}'): - call(f'systemctl stop {http_api_service_name}') + if 'api' in https: + call(f'systemctl reload-or-restart {http_api_service_name}') + # Let uvicorn settle before (possibly) restarting nginx + sleep(1) + else: + call(f'systemctl stop {http_api_service_name}') - if (not is_systemd_service_running(f'{https_service_name}') or - https['api_add_or_delete'] or - set(https['children_changed']) - set(['api'])): - call(f'systemctl restart {https_service_name}') + call(f'systemctl reload-or-restart {https_service_name}') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) sys.exit(1) diff --git a/src/conf_mode/service_https_certificates_certbot.py b/src/conf_mode/service_https_certificates_certbot.py deleted file mode 100755 index 1a6a498de..000000000 --- a/src/conf_mode/service_https_certificates_certbot.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-2020 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 sys -import os - -import vyos.defaults -from vyos.config import Config -from vyos import ConfigError -from vyos.utils.process import cmd -from vyos.utils.process import call -from vyos.utils.process import is_systemd_service_running - -from vyos import airbag -airbag.enable() - -vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] -vyos_certbot_dir = vyos.defaults.directories['certbot'] - -dependencies = [ - 'service_https.py', -] - -def request_certbot(cert): - email = cert.get('email') - if email is not None: - email_flag = '-m {0}'.format(email) - else: - email_flag = '' - - domains = cert.get('domains') - if domains is not None: - domain_flag = '-d ' + ' -d '.join(domains) - else: - domain_flag = '' - - certbot_cmd = f'certbot certonly --config-dir {vyos_certbot_dir} -n --nginx --agree-tos --no-eff-email --expand {email_flag} {domain_flag}' - - cmd(certbot_cmd, - raising=ConfigError, - message="The certbot request failed for the specified domains.") - -def get_config(): - conf = Config() - if not conf.exists('service https certificates certbot'): - return None - else: - conf.set_level('service https certificates certbot') - - cert = {} - - if conf.exists('domain-name'): - cert['domains'] = conf.return_values('domain-name') - - if conf.exists('email'): - cert['email'] = conf.return_value('email') - - return cert - -def verify(cert): - if cert is None: - return None - - if 'domains' not in cert: - raise ConfigError("At least one domain name is required to" - " request a letsencrypt certificate.") - - if 'email' not in cert: - raise ConfigError("An email address is required to request" - " a letsencrypt certificate.") - -def generate(cert): - if cert is None: - return None - - # certbot will attempt to reload nginx, even with 'certonly'; - # start nginx if not active - if not is_systemd_service_running('nginx.service'): - call('systemctl start nginx.service') - - request_certbot(cert) - -def apply(cert): - if cert is not None: - call('systemctl restart certbot.timer') - else: - call('systemctl stop certbot.timer') - return None - - for dep in dependencies: - cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 7fd32c230..5bdcf2fa1 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -1,594 +1,595 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-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 <http://www.gnu.org/licenses/>. import ipaddress import os import re import jmespath from sys import exit from time import sleep from time import time from vyos.base import Warning from vyos.config import Config from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists from vyos.defaults import directories from vyos.ifconfig import Interface from vyos.pki import encode_certificate from vyos.pki import encode_public_key from vyos.pki import find_chain from vyos.pki import load_certificate from vyos.pki import load_private_key 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 is_ipv4 from vyos.template import is_ipv6 from vyos.template import render from vyos.utils.network import is_ipv6_link_local +from vyos.utils.network import interface_exists from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.process import call from vyos.utils.process import run from vyos import ConfigError from vyos import airbag airbag.enable() dhcp_wait_attempts = 2 dhcp_wait_sleep = 1 swanctl_dir = '/etc/swanctl' charon_conf = '/etc/strongswan.d/charon.conf' charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf' interface_conf = '/etc/strongswan.d/interfaces_use.conf' swanctl_conf = f'{swanctl_dir}/swanctl.conf' default_install_routes = 'yes' vici_socket = '/var/run/charon.vici' -CERT_PATH = f'{swanctl_dir}/x509/' +CERT_PATH = f'{swanctl_dir}/x509/' PUBKEY_PATH = f'{swanctl_dir}/pubkey/' -KEY_PATH = f'{swanctl_dir}/private/' -CA_PATH = f'{swanctl_dir}/x509ca/' -CRL_PATH = f'{swanctl_dir}/x509crl/' +KEY_PATH = f'{swanctl_dir}/private/' +CA_PATH = f'{swanctl_dir}/x509ca/' +CRL_PATH = f'{swanctl_dir}/x509crl/' DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting' def get_config(config=None): if config: conf = config else: conf = Config() base = ['vpn', 'ipsec'] l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings'] if not conf.exists(base): return None # retrieve common dictionary keys ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, with_recursive_defaults=True, with_pki=True) ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface']) ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel']) tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True) if tmp: ipsec['l2tp'] = conf.merge_defaults(tmp, recursive=True) ipsec['l2tp_outside_address'] = conf.return_value(['vpn', 'l2tp', 'remote-access', 'outside-address']) ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024' ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1' return ipsec def get_dhcp_address(iface): addresses = Interface(iface).get_addr() if not addresses: return None for address in addresses: if not is_ipv6_link_local(address): return ip_from_cidr(address) return None def verify_pki_x509(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_args(pki, 'ca', ca_cert_name, 'certificate'): raise ConfigError(f'Missing CA certificate on specified PKI CA certificate "{ca_cert_name}"') if not dict_search_args(pki, 'certificate', cert_name, 'certificate'): raise ConfigError(f'Missing certificate on specified PKI certificate "{cert_name}"') if not dict_search_args(pki, 'certificate', cert_name, 'private', 'key'): raise ConfigError(f'Missing private key on specified PKI certificate "{cert_name}"') return True def verify_pki_rsa(pki, rsa_conf): if not pki or 'key_pair' not in pki: raise ConfigError(f'PKI is not configured') local_key = rsa_conf['local_key'] remote_key = rsa_conf['remote_key'] if not dict_search_args(pki, 'key_pair', local_key, 'private', 'key'): raise ConfigError(f'Missing private key on specified local-key "{local_key}"') if not dict_search_args(pki, 'key_pair', remote_key, 'public', 'key'): raise ConfigError(f'Missing public key on specified remote-key "{remote_key}"') return True def verify(ipsec): if not ipsec: return None if 'authentication' in ipsec: if 'psk' in ipsec['authentication']: for psk, psk_config in ipsec['authentication']['psk'].items(): if 'id' not in psk_config or 'secret' not in psk_config: raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"') if 'interfaces' in ipsec : for ifname in ipsec['interface']: verify_interface_exists(ifname) if 'l2tp' in ipsec: if 'esp_group' in ipsec['l2tp']: if 'esp_group' not in ipsec or ipsec['l2tp']['esp_group'] not in ipsec['esp_group']: raise ConfigError(f"Invalid esp-group on L2TP remote-access config") if 'ike_group' in ipsec['l2tp']: if 'ike_group' not in ipsec or ipsec['l2tp']['ike_group'] not in ipsec['ike_group']: raise ConfigError(f"Invalid ike-group on L2TP remote-access config") if 'authentication' not in ipsec['l2tp']: raise ConfigError(f'Missing authentication settings on L2TP remote-access config') if 'mode' not in ipsec['l2tp']['authentication']: raise ConfigError(f'Missing authentication mode on L2TP remote-access config') if not ipsec['l2tp_outside_address']: raise ConfigError(f'Missing outside-address on L2TP remote-access config') if ipsec['l2tp']['authentication']['mode'] == 'pre-shared-secret': if 'pre_shared_secret' not in ipsec['l2tp']['authentication']: raise ConfigError(f'Missing pre shared secret on L2TP remote-access config') if ipsec['l2tp']['authentication']['mode'] == 'x509': if 'x509' not in ipsec['l2tp']['authentication']: raise ConfigError(f'Missing x509 settings on L2TP remote-access config') x509 = ipsec['l2tp']['authentication']['x509'] if 'ca_certificate' not in x509 or 'certificate' not in x509: raise ConfigError(f'Missing x509 certificates on L2TP remote-access config') verify_pki_x509(ipsec['pki'], x509) if 'profile' in ipsec: for profile, profile_conf in ipsec['profile'].items(): if 'esp_group' in profile_conf: if 'esp_group' not in ipsec or profile_conf['esp_group'] not in ipsec['esp_group']: raise ConfigError(f"Invalid esp-group on {profile} profile") else: raise ConfigError(f"Missing esp-group on {profile} profile") if 'ike_group' in profile_conf: if 'ike_group' not in ipsec or profile_conf['ike_group'] not in ipsec['ike_group']: raise ConfigError(f"Invalid ike-group on {profile} profile") else: raise ConfigError(f"Missing ike-group on {profile} profile") if 'authentication' not in profile_conf: raise ConfigError(f"Missing authentication on {profile} profile") if 'remote_access' in ipsec: if 'connection' in ipsec['remote_access']: for name, ra_conf in ipsec['remote_access']['connection'].items(): if 'esp_group' in ra_conf: if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']: raise ConfigError(f"Invalid esp-group on {name} remote-access config") else: raise ConfigError(f"Missing esp-group on {name} remote-access config") if 'ike_group' in ra_conf: if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']: raise ConfigError(f"Invalid ike-group on {name} remote-access config") ike = ra_conf['ike_group'] if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2': raise ConfigError('IPsec remote-access connections requires IKEv2!') else: raise ConfigError(f"Missing ike-group on {name} remote-access config") if 'authentication' not in ra_conf: raise ConfigError(f"Missing authentication on {name} remote-access config") if ra_conf['authentication']['server_mode'] == 'x509': if 'x509' not in ra_conf['authentication']: raise ConfigError(f"Missing x509 settings on {name} remote-access config") x509 = ra_conf['authentication']['x509'] if 'ca_certificate' not in x509 or 'certificate' not in x509: raise ConfigError(f"Missing x509 certificates on {name} remote-access config") verify_pki_x509(ipsec['pki'], x509) elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret': if 'pre_shared_secret' not in ra_conf['authentication']: raise ConfigError(f"Missing pre-shared-key on {name} remote-access config") if 'client_mode' not in ra_conf['authentication']: raise ConfigError('Client authentication method is required!') if dict_search('authentication.client_mode', ra_conf) == 'eap-radius': if dict_search('remote_access.radius.server', ipsec) == None: raise ConfigError('RADIUS authentication requires at least one server') if 'pool' in ra_conf: if {'dhcp', 'radius'} <= set(ra_conf['pool']): raise ConfigError(f'Can not use both DHCP and RADIUS for address allocation '\ f'at the same time for "{name}"!') if 'dhcp' in ra_conf['pool'] and len(ra_conf['pool']) > 1: raise ConfigError(f'Can not use DHCP and a predefined address pool for "{name}"!') if 'radius' in ra_conf['pool'] and len(ra_conf['pool']) > 1: raise ConfigError(f'Can not use RADIUS and a predefined address pool for "{name}"!') for pool in ra_conf['pool']: if pool == 'dhcp': if dict_search('remote_access.dhcp.server', ipsec) == None: raise ConfigError('IPsec DHCP server is not configured!') elif pool == 'radius': if dict_search('remote_access.radius.server', ipsec) == None: raise ConfigError('IPsec RADIUS server is not configured!') if dict_search('authentication.client_mode', ra_conf) != 'eap-radius': raise ConfigError('RADIUS IP pool requires eap-radius client authentication!') elif 'pool' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']: raise ConfigError(f'Requested pool "{pool}" does not exist!') if 'pool' in ipsec['remote_access']: for pool, pool_config in ipsec['remote_access']['pool'].items(): if 'prefix' not in pool_config: raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!') if 'name_server' in pool_config: if len(pool_config['name_server']) > 2: raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!') for ns in pool_config['name_server']: v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix']) v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix']) if v4_addr_and_ns or v6_addr_and_ns: raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!') if 'exclude' in pool_config: for exclude in pool_config['exclude']: v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix']) v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix']) if v4_addr_and_exclude or v6_addr_and_exclude: raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!') if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']: for server, server_config in ipsec['remote_access']['radius']['server'].items(): if 'key' not in server_config: raise ConfigError(f'Missing RADIUS secret key for server "{server}"') if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): has_default_esp = False # Peer name it is swanctl connection name and shouldn't contain dots or colons, T4118 if bool(re.search(':|\.', peer)): raise ConfigError(f'Incorrect peer name "{peer}" ' f'Peer name can contain alpha-numeric letters, hyphen and underscore') if 'remote_address' not in peer_conf: print(f'You should set correct remote-address "peer {peer} remote-address x.x.x.x"\n') if 'default_esp_group' in peer_conf: has_default_esp = True if 'esp_group' not in ipsec or peer_conf['default_esp_group'] not in ipsec['esp_group']: raise ConfigError(f"Invalid esp-group on site-to-site peer {peer}") if 'ike_group' in peer_conf: if 'ike_group' not in ipsec or peer_conf['ike_group'] not in ipsec['ike_group']: raise ConfigError(f"Invalid ike-group on site-to-site peer {peer}") else: raise ConfigError(f"Missing ike-group on site-to-site peer {peer}") if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']: raise ConfigError(f"Missing authentication on site-to-site peer {peer}") if {'id', 'use_x509_id'} <= set(peer_conf['authentication']): raise ConfigError(f"Manually set peer id and use-x509-id are mutually exclusive!") if peer_conf['authentication']['mode'] == 'x509': if 'x509' not in peer_conf['authentication']: raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}") x509 = peer_conf['authentication']['x509'] if 'ca_certificate' not in x509 or 'certificate' not in x509: raise ConfigError(f"Missing x509 certificates on site-to-site peer {peer}") verify_pki_x509(ipsec['pki'], x509) elif peer_conf['authentication']['mode'] == 'rsa': if 'rsa' not in peer_conf['authentication']: raise ConfigError(f"Missing RSA settings on site-to-site peer {peer}") rsa = peer_conf['authentication']['rsa'] if 'local_key' not in rsa: raise ConfigError(f"Missing RSA local-key on site-to-site peer {peer}") if 'remote_key' not in rsa: raise ConfigError(f"Missing RSA remote-key on site-to-site peer {peer}") verify_pki_rsa(ipsec['pki'], rsa) if 'local_address' not in peer_conf and 'dhcp_interface' not in peer_conf: raise ConfigError(f"Missing local-address or dhcp-interface on site-to-site peer {peer}") if 'dhcp_interface' in peer_conf: dhcp_interface = peer_conf['dhcp_interface'] verify_interface_exists(dhcp_interface) dhcp_base = directories['isc_dhclient_dir'] if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'): raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}") address = get_dhcp_address(dhcp_interface) count = 0 while not address and count < dhcp_wait_attempts: address = get_dhcp_address(dhcp_interface) count += 1 sleep(dhcp_wait_sleep) if not address: ipsec['dhcp_no_address'][peer] = dhcp_interface print(f"Failed to get address from dhcp-interface on site-to-site peer {peer} -- skipped") continue if 'vti' in peer_conf: if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf: raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}") if dict_search('options.disable_route_autoinstall', ipsec) == None: Warning('It\'s recommended to use ipsec vti with the next command\n[set vpn ipsec option disable-route-autoinstall]') if 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] - if not os.path.exists(f'/sys/class/net/{vti_interface}'): + if not interface_exists(vti_interface): raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!') if 'vti' not in peer_conf and 'tunnel' not in peer_conf: raise ConfigError(f"No VTI or tunnel specified on site-to-site peer {peer}") if 'tunnel' in peer_conf: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): if 'esp_group' not in tunnel_conf and not has_default_esp: raise ConfigError(f"Missing esp-group on tunnel {tunnel} for site-to-site peer {peer}") esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group'] if esp_group_name not in ipsec['esp_group']: raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}") esp_group = ipsec['esp_group'][esp_group_name] if 'mode' in esp_group and esp_group['mode'] == 'transport': if 'protocol' in tunnel_conf and ((peer in ['any', '0.0.0.0']) or ('local_address' not in peer_conf or peer_conf['local_address'] in ['any', '0.0.0.0'])): raise ConfigError(f"Fixed local-address or peer required when a protocol is defined with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") 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 cleanup_pki_files(): for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]: if not os.path.exists(path): continue for file in os.listdir(path): file_path = os.path.join(path, file) if os.path.isfile(file_path): os.unlink(file_path) def generate_pki_files_x509(pki, x509_conf): ca_cert_name = x509_conf['ca_certificate'] ca_cert_data = dict_search_args(pki, 'ca', ca_cert_name, 'certificate') ca_cert_crls = dict_search_args(pki, 'ca', ca_cert_name, 'crl') or [] ca_index = 1 crl_index = 1 ca_cert = load_certificate(ca_cert_data) pki_ca_certs = [load_certificate(ca['certificate']) for ca in pki['ca'].values()] ca_cert_chain = find_chain(ca_cert, pki_ca_certs) cert_name = x509_conf['certificate'] cert_data = dict_search_args(pki, 'certificate', cert_name, 'certificate') key_data = dict_search_args(pki, 'certificate', cert_name, 'private', 'key') protected = 'passphrase' in x509_conf for ca_cert_obj in ca_cert_chain: with open(os.path.join(CA_PATH, f'{ca_cert_name}_{ca_index}.pem'), 'w') as f: f.write(encode_certificate(ca_cert_obj)) ca_index += 1 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'x509_{cert_name}.pem'), 'w') as f: f.write(wrap_private_key(key_data, protected)) def generate_pki_files_rsa(pki, rsa_conf): local_key_name = rsa_conf['local_key'] local_key_data = dict_search_args(pki, 'key_pair', local_key_name, 'private', 'key') protected = 'passphrase' in rsa_conf remote_key_name = rsa_conf['remote_key'] remote_key_data = dict_search_args(pki, 'key_pair', remote_key_name, 'public', 'key') local_key = load_private_key(local_key_data, rsa_conf['passphrase'] if protected else None) with open(os.path.join(KEY_PATH, f'rsa_{local_key_name}.pem'), 'w') as f: f.write(wrap_private_key(local_key_data, protected)) with open(os.path.join(PUBKEY_PATH, f'{local_key_name}.pem'), 'w') as f: f.write(encode_public_key(local_key.public_key())) with open(os.path.join(PUBKEY_PATH, f'{remote_key_name}.pem'), 'w') as f: f.write(wrap_public_key(remote_key_data)) def generate(ipsec): cleanup_pki_files() if not ipsec: for config_file in [charon_dhcp_conf, charon_radius_conf, interface_conf, swanctl_conf]: if os.path.isfile(config_file): os.unlink(config_file) render(charon_conf, 'ipsec/charon.j2', {'install_routes': default_install_routes}) return if ipsec['dhcp_no_address']: with open(DHCP_HOOK_IFLIST, 'w') as f: f.write(" ".join(ipsec['dhcp_no_address'].values())) elif os.path.exists(DHCP_HOOK_IFLIST): os.unlink(DHCP_HOOK_IFLIST) for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH, PUBKEY_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 'l2tp' in ipsec: if 'authentication' in ipsec['l2tp'] and 'x509' in ipsec['l2tp']['authentication']: generate_pki_files_x509(ipsec['pki'], ipsec['l2tp']['authentication']['x509']) if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']: for rw, rw_conf in ipsec['remote_access']['connection'].items(): if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']: generate_pki_files_x509(ipsec['pki'], rw_conf['authentication']['x509']) if 'site_to_site' in ipsec and 'peer' in ipsec['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': generate_pki_files_x509(ipsec['pki'], peer_conf['authentication']['x509']) elif peer_conf['authentication']['mode'] == 'rsa': generate_pki_files_rsa(ipsec['pki'], peer_conf['authentication']['rsa']) local_ip = '' if 'local_address' in peer_conf: local_ip = peer_conf['local_address'] elif 'dhcp_interface' in peer_conf: local_ip = get_dhcp_address(peer_conf['dhcp_interface']) ipsec['site_to_site']['peer'][peer]['local_address'] = local_ip if 'tunnel' in peer_conf: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): local_prefixes = dict_search_args(tunnel_conf, 'local', 'prefix') remote_prefixes = dict_search_args(tunnel_conf, 'remote', 'prefix') if not local_prefixes or not remote_prefixes: continue passthrough = None for local_prefix in local_prefixes: for remote_prefix in remote_prefixes: local_net = ipaddress.ip_network(local_prefix) remote_net = ipaddress.ip_network(remote_prefix) if local_net.overlaps(remote_net): if passthrough is None: passthrough = [] passthrough.append(local_prefix) ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough # auth psk <tag> dhcp-interface <xxx> if jmespath.search('authentication.psk.*.dhcp_interface', ipsec): for psk, psk_config in ipsec['authentication']['psk'].items(): if 'dhcp_interface' in psk_config: for iface in psk_config['dhcp_interface']: id = get_dhcp_address(iface) if id: ipsec['authentication']['psk'][psk]['id'].append(id) render(charon_conf, 'ipsec/charon.j2', ipsec) render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec) render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec) render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec) render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec) def resync_nhrp(ipsec): if ipsec and not ipsec['nhrp_exists']: return tmp = run('/usr/libexec/vyos/conf_mode/protocols_nhrp.py') if tmp > 0: print('ERROR: failed to reapply NHRP settings!') def apply(ipsec): systemd_service = 'strongswan.service' if not ipsec: call(f'systemctl stop {systemd_service}') else: call(f'systemctl reload-or-restart {systemd_service}') resync_nhrp(ipsec) if __name__ == '__main__': try: ipsec = get_config() verify(ipsec) generate(ipsec) apply(ipsec) except ConfigError as e: print(e) exit(1) diff --git a/src/etc/systemd/system/certbot.service.d/10-override.conf b/src/etc/systemd/system/certbot.service.d/10-override.conf new file mode 100644 index 000000000..542f77eb2 --- /dev/null +++ b/src/etc/systemd/system/certbot.service.d/10-override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/bin/certbot renew --config-dir /config/auth/letsencrypt --no-random-sleep-on-renew --post-hook "/usr/libexec/vyos/vyos-certbot-renew-pki.sh" diff --git a/src/helpers/vyos-certbot-renew-pki.sh b/src/helpers/vyos-certbot-renew-pki.sh new file mode 100755 index 000000000..d0b663f7b --- /dev/null +++ b/src/helpers/vyos-certbot-renew-pki.sh @@ -0,0 +1,3 @@ +#!/bin/sh +source /opt/vyatta/etc/functions/script-template +/usr/libexec/vyos/conf_mode/pki.py certbot_renew diff --git a/src/migration-scripts/https/5-to-6 b/src/migration-scripts/https/5-to-6 new file mode 100755 index 000000000..b4159f02f --- /dev/null +++ b/src/migration-scripts/https/5-to-6 @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 <http://www.gnu.org/licenses/>. + +# T5886: Add support for ACME protocol (LetsEncrypt), migrate https certbot +# to new "pki certificate" CLI tree + +import os +import sys + +from vyos.configtree import ConfigTree +from vyos.defaults import directories + +vyos_certbot_dir = directories['certbot'] + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'https', 'certificates'] +if not config.exists(base): + # Nothing to do + sys.exit(0) + +# both domain-name and email must be set on CLI - ensured by previous verify() +domain_names = config.return_values(base + ['certbot', 'domain-name']) +email = config.return_value(base + ['certbot', 'email']) +config.delete(base) + +# Set default certname based on domain-name +cert_name = 'https-' + domain_names[0].split('.')[0] +# Overwrite certname from previous certbot calls if available +if os.path.exists(f'{vyos_certbot_dir}/live'): + for cert in [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()]: + cert_name = cert + break + +for domain in domain_names: + config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False) + config.set(['pki', 'certificate', cert_name, 'acme', 'email'], value=email) + +# Update Webserver certificate +config.set(base + ['certificate'], value=cert_name) + +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 6c854afb5..ad2c1ada0 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -1,1080 +1,1088 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-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 <http://www.gnu.org/licenses/>. import argparse import ipaddress import os import re import sys import tabulate from cryptography import x509 from cryptography.x509.oid import ExtendedKeyUsageOID from vyos.config import Config +from vyos.config import config_dict_mangle_acme from vyos.pki import encode_certificate, encode_public_key, encode_private_key, encode_dh_parameters from vyos.pki import get_certificate_fingerprint from vyos.pki import create_certificate, create_certificate_request, create_certificate_revocation_list from vyos.pki import create_private_key from vyos.pki import create_dh_parameters from vyos.pki import load_certificate, load_certificate_request, load_private_key from vyos.pki import load_crl, load_dh_parameters, load_public_key from vyos.pki import verify_certificate from vyos.utils.io import ask_input from vyos.utils.io import ask_yes_no from vyos.utils.misc import install_into_config from vyos.utils.process import cmd CERT_REQ_END = '-----END CERTIFICATE REQUEST-----' auth_dir = '/config/auth' # Helper Functions conf = Config() def get_default_values(): # Fetch default x509 values base = ['pki', 'x509', 'default'] x509_defaults = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, with_recursive_defaults=True) return x509_defaults def get_config_ca_certificate(name=None): # Fetch ca certificates from config base = ['pki', 'ca'] if not conf.exists(base): return False if name: base = base + [name] if not conf.exists(base + ['private', 'key']) or not conf.exists(base + ['certificate']): return False return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) def get_config_certificate(name=None): # Get certificates from config base = ['pki', 'certificate'] if not conf.exists(base): return False if name: base = base + [name] if not conf.exists(base + ['private', 'key']) or not conf.exists(base + ['certificate']): return False - return conf.get_config_dict(base, key_mangling=('-', '_'), + pki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + if pki: + for certificate in pki: + pki[certificate] = config_dict_mangle_acme(certificate, pki[certificate]) + + return pki def get_certificate_ca(cert, ca_certs): # Find CA certificate for given certificate if not ca_certs: return None for ca_name, ca_dict in ca_certs.items(): if 'certificate' not in ca_dict: continue ca_cert = load_certificate(ca_dict['certificate']) if not ca_cert: continue if verify_certificate(cert, ca_cert): return ca_name return None def get_config_revoked_certificates(): # Fetch revoked certificates from config ca_base = ['pki', 'ca'] cert_base = ['pki', 'certificate'] certs = [] if conf.exists(ca_base): ca_certificates = conf.get_config_dict(ca_base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) certs.extend(ca_certificates.values()) if conf.exists(cert_base): certificates = conf.get_config_dict(cert_base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) certs.extend(certificates.values()) return [cert_dict for cert_dict in certs if 'revoke' in cert_dict] def get_revoked_by_serial_numbers(serial_numbers=[]): # Return serial numbers of revoked certificates certs_out = [] certs = get_config_certificate() ca_certs = get_config_ca_certificate() if certs: for cert_name, cert_dict in certs.items(): if 'certificate' not in cert_dict: continue cert = load_certificate(cert_dict['certificate']) if cert.serial_number in serial_numbers: certs_out.append(cert_name) if ca_certs: for cert_name, cert_dict in ca_certs.items(): if 'certificate' not in cert_dict: continue cert = load_certificate(cert_dict['certificate']) if cert.serial_number in serial_numbers: certs_out.append(cert_name) return certs_out def install_certificate(name, cert='', private_key=None, key_type=None, key_passphrase=None, is_ca=False): # Show/install conf commands for certificate prefix = 'ca' if is_ca else 'certificate' base = f"pki {prefix} {name}" config_paths = [] if cert: cert_pem = "".join(encode_certificate(cert).strip().split("\n")[1:-1]) config_paths.append(f"{base} certificate '{cert_pem}'") if private_key: key_pem = "".join(encode_private_key(private_key, passphrase=key_passphrase).strip().split("\n")[1:-1]) config_paths.append(f"{base} private key '{key_pem}'") if key_passphrase: config_paths.append(f"{base} private password-protected") install_into_config(conf, config_paths) def install_crl(ca_name, crl): # Show/install conf commands for crl crl_pem = "".join(encode_certificate(crl).strip().split("\n")[1:-1]) install_into_config(conf, [f"pki ca {ca_name} crl '{crl_pem}'"]) def install_dh_parameters(name, params): # Show/install conf commands for dh params dh_pem = "".join(encode_dh_parameters(params).strip().split("\n")[1:-1]) install_into_config(conf, [f"pki dh {name} parameters '{dh_pem}'"]) def install_ssh_key(name, public_key, private_key, passphrase=None): # Show/install conf commands for ssh key key_openssh = encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH') username = os.getlogin() type_key_split = key_openssh.split(" ") base = f"system login user {username} authentication public-keys {name}" install_into_config(conf, [ f"{base} key '{type_key_split[1]}'", f"{base} type '{type_key_split[0]}'" ]) print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None, prompt=True): # Show/install conf commands for key-pair config_paths = [] if public_key: install_public_key = not prompt or ask_yes_no('Do you want to install the public key?', default=True) public_key_pem = encode_public_key(public_key) if install_public_key: install_public_pem = "".join(public_key_pem.strip().split("\n")[1:-1]) config_paths.append(f"pki key-pair {name} public key '{install_public_pem}'") else: print("Public key:") print(public_key_pem) if private_key: install_private_key = not prompt or ask_yes_no('Do you want to install the private key?', default=True) private_key_pem = encode_private_key(private_key, passphrase=passphrase) if install_private_key: install_private_pem = "".join(private_key_pem.strip().split("\n")[1:-1]) config_paths.append(f"pki key-pair {name} private key '{install_private_pem}'") if passphrase: config_paths.append(f"pki key-pair {name} private password-protected") else: print("Private key:") print(private_key_pem) install_into_config(conf, config_paths) def install_openvpn_key(name, key_data, key_version='1'): config_paths = [ f"pki openvpn shared-secret {name} key '{key_data}'", f"pki openvpn shared-secret {name} version '{key_version}'" ] install_into_config(conf, config_paths) def install_wireguard_key(interface, private_key, public_key): # Show conf commands for installing wireguard key pairs from vyos.ifconfig import Section if Section.section(interface) != 'wireguard': print(f'"{interface}" is not a WireGuard interface name!') exit(1) # Check if we are running in a config session - if yes, we can directly write to the CLI install_into_config(conf, [f"interfaces wireguard {interface} private-key '{private_key}'"]) print(f"Corresponding public-key to use on peer system is: '{public_key}'") def install_wireguard_psk(interface, peer, psk): from vyos.ifconfig import Section if Section.section(interface) != 'wireguard': print(f'"{interface}" is not a WireGuard interface name!') exit(1) # Check if we are running in a config session - if yes, we can directly write to the CLI install_into_config(conf, [f"interfaces wireguard {interface} peer {peer} preshared-key '{psk}'"]) def ask_passphrase(): passphrase = None print("Note: If you plan to use the generated key on this router, do not encrypt the private key.") if ask_yes_no('Do you want to encrypt the private key with a passphrase?'): passphrase = ask_input('Enter passphrase:') return passphrase def write_file(filename, contents): full_path = os.path.join(auth_dir, filename) directory = os.path.dirname(full_path) if not os.path.exists(directory): print('Failed to write file: directory does not exist') return False if os.path.exists(full_path) and not ask_yes_no('Do you want to overwrite the existing file?'): return False with open(full_path, 'w') as f: f.write(contents) print(f'File written to {full_path}') # Generation functions def generate_private_key(): key_type = ask_input('Enter private key type: [rsa, dsa, ec]', default='rsa', valid_responses=['rsa', 'dsa', 'ec']) size_valid = [] size_default = 0 if key_type in ['rsa', 'dsa']: size_default = 2048 size_valid = [512, 1024, 2048, 4096] elif key_type == 'ec': size_default = 256 size_valid = [224, 256, 384, 521] size = ask_input('Enter private key bits:', default=size_default, numeric_only=True, valid_responses=size_valid) return create_private_key(key_type, size), key_type def parse_san_string(san_string): if not san_string: return None output = [] san_split = san_string.strip().split(",") for pair_str in san_split: tag, value = pair_str.strip().split(":", 1) if tag == 'ipv4': output.append(ipaddress.IPv4Address(value)) elif tag == 'ipv6': output.append(ipaddress.IPv6Address(value)) elif tag == 'dns': output.append(value) return output def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, file=False, ask_san=True): if not private_key: private_key, key_type = generate_private_key() default_values = get_default_values() subject = {} subject['country'] = ask_input('Enter country code:', default=default_values['country']) subject['state'] = ask_input('Enter state:', default=default_values['state']) subject['locality'] = ask_input('Enter locality:', default=default_values['locality']) subject['organization'] = ask_input('Enter organization name:', default=default_values['organization']) subject['common_name'] = ask_input('Enter common name:', default='vyos.io') subject_alt_names = None if ask_san and ask_yes_no('Do you want to configure Subject Alternative Names?'): print("Enter alternative names in a comma separate list, example: ipv4:1.1.1.1,ipv6:fe80::1,dns:vyos.net") san_string = ask_input('Enter Subject Alternative Names:') subject_alt_names = parse_san_string(san_string) cert_req = create_certificate_request(subject, private_key, subject_alt_names) if return_request: return cert_req passphrase = ask_passphrase() if not install and not file: print(encode_certificate(cert_req)) print(encode_private_key(private_key, passphrase=passphrase)) return None if install: print("Certificate request:") print(encode_certificate(cert_req) + "\n") install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False) if file: write_file(f'{name}.csr', encode_certificate(cert_req)) write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_ca=False): valid_days = ask_input('Enter how many days certificate will be valid:', default='365' if not is_ca else '1825', numeric_only=True) cert_type = None if not is_ca: cert_type = ask_input('Enter certificate type: (client, server)', default='server', valid_responses=['client', 'server']) return create_certificate(cert_req, ca_cert, ca_private_key, valid_days, cert_type, is_ca, is_sub_ca) def generate_ca_certificate(name, install=False, file=False): private_key, key_type = generate_private_key() cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False) cert = generate_certificate(cert_req, cert_req, private_key, is_ca=True) passphrase = ask_passphrase() if not install and not file: print(encode_certificate(cert)) print(encode_private_key(private_key, passphrase=passphrase)) return None if install: install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True) if file: write_file(f'{name}.pem', encode_certificate(cert)) write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_ca_certificate_sign(name, ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) if not ca_dict: print(f"CA certificate or private key for '{ca_name}' not found") return None ca_cert = load_certificate(ca_dict['certificate']) if not ca_cert: print("Failed to load signing CA certificate, aborting") return None ca_private = ca_dict['private'] ca_private_passphrase = None if 'password_protected' in ca_private: ca_private_passphrase = ask_input('Enter signing CA private key passphrase:') ca_private_key = load_private_key(ca_private['key'], passphrase=ca_private_passphrase) if not ca_private_key: print("Failed to load signing CA private key, aborting") return None private_key = None key_type = None cert_req = None if not ask_yes_no('Do you already have a certificate request?'): private_key, key_type = generate_private_key() cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False) else: print("Paste certificate request and press enter:") lines = [] curr_line = '' while True: curr_line = input().strip() if not curr_line or curr_line == CERT_REQ_END: break lines.append(curr_line) if not lines: print("Aborted") return None wrap = lines[0].find('-----') < 0 # Only base64 pasted, add the CSR tags for parsing cert_req = load_certificate_request("\n".join(lines), wrap) if not cert_req: print("Invalid certificate request") return None cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=True, is_sub_ca=True) passphrase = ask_passphrase() if not install and not file: print(encode_certificate(cert)) print(encode_private_key(private_key, passphrase=passphrase)) return None if install: install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True) if file: write_file(f'{name}.pem', encode_certificate(cert)) write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_certificate_sign(name, ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) if not ca_dict: print(f"CA certificate or private key for '{ca_name}' not found") return None ca_cert = load_certificate(ca_dict['certificate']) if not ca_cert: print("Failed to load CA certificate, aborting") return None ca_private = ca_dict['private'] ca_private_passphrase = None if 'password_protected' in ca_private: ca_private_passphrase = ask_input('Enter CA private key passphrase:') ca_private_key = load_private_key(ca_private['key'], passphrase=ca_private_passphrase) if not ca_private_key: print("Failed to load CA private key, aborting") return None private_key = None key_type = None cert_req = None if not ask_yes_no('Do you already have a certificate request?'): private_key, key_type = generate_private_key() cert_req = generate_certificate_request(private_key, key_type, return_request=True) else: print("Paste certificate request and press enter:") lines = [] curr_line = '' while True: curr_line = input().strip() if not curr_line or curr_line == CERT_REQ_END: break lines.append(curr_line) if not lines: print("Aborted") return None wrap = lines[0].find('-----') < 0 # Only base64 pasted, add the CSR tags for parsing cert_req = load_certificate_request("\n".join(lines), wrap) if not cert_req: print("Invalid certificate request") return None cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False) passphrase = ask_passphrase() if not install and not file: print(encode_certificate(cert)) print(encode_private_key(private_key, passphrase=passphrase)) return None if install: install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False) if file: write_file(f'{name}.pem', encode_certificate(cert)) write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_certificate_selfsign(name, install=False, file=False): private_key, key_type = generate_private_key() cert_req = generate_certificate_request(private_key, key_type, return_request=True) cert = generate_certificate(cert_req, cert_req, private_key, is_ca=False) passphrase = ask_passphrase() if not install and not file: print(encode_certificate(cert)) print(encode_private_key(private_key, passphrase=passphrase)) return None if install: install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False) if file: write_file(f'{name}.pem', encode_certificate(cert)) write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_certificate_revocation_list(ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) if not ca_dict: print(f"CA certificate or private key for '{ca_name}' not found") return None ca_cert = load_certificate(ca_dict['certificate']) if not ca_cert: print("Failed to load CA certificate, aborting") return None ca_private = ca_dict['private'] ca_private_passphrase = None if 'password_protected' in ca_private: ca_private_passphrase = ask_input('Enter CA private key passphrase:') ca_private_key = load_private_key(ca_private['key'], passphrase=ca_private_passphrase) if not ca_private_key: print("Failed to load CA private key, aborting") return None revoked_certs = get_config_revoked_certificates() to_revoke = [] for cert_dict in revoked_certs: if 'certificate' not in cert_dict: continue cert_data = cert_dict['certificate'] try: cert = load_certificate(cert_data) if cert.issuer == ca_cert.subject: to_revoke.append(cert.serial_number) except ValueError: continue if not to_revoke: print("No revoked certificates to add to the CRL") return None crl = create_certificate_revocation_list(ca_cert, ca_private_key, to_revoke) if not crl: print("Failed to create CRL") return None if not install and not file: print(encode_certificate(crl)) return None if install: install_crl(ca_name, crl) if file: write_file(f'{name}.crl', encode_certificate(crl)) def generate_ssh_keypair(name, install=False, file=False): private_key, key_type = generate_private_key() public_key = private_key.public_key() passphrase = ask_passphrase() if not install and not file: print(encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH')) print("") print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) return None if install: install_ssh_key(name, public_key, private_key, passphrase) if file: write_file(f'{name}.pem', encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH')) write_file(f'{name}.key', encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) def generate_dh_parameters(name, install=False, file=False): bits = ask_input('Enter DH parameters key size:', default=2048, numeric_only=True) print("Generating parameters...") dh_params = create_dh_parameters(bits) if not dh_params: print("Failed to create DH parameters") return None if not install and not file: print("DH Parameters:") print(encode_dh_parameters(dh_params)) if install: install_dh_parameters(name, dh_params) if file: write_file(f'{name}.pem', encode_dh_parameters(dh_params)) def generate_keypair(name, install=False, file=False): private_key, key_type = generate_private_key() public_key = private_key.public_key() passphrase = ask_passphrase() if not install and not file: print(encode_public_key(public_key)) print("") print(encode_private_key(private_key, passphrase=passphrase)) return None if install: install_keypair(name, key_type, private_key, public_key, passphrase) if file: write_file(f'{name}.pem', encode_public_key(public_key)) write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_openvpn_key(name, install=False, file=False): result = cmd('openvpn --genkey secret /dev/stdout | grep -o "^[^#]*"') if not result: print("Failed to generate OpenVPN key") return None if not install and not file: print(result) return None if install: key_lines = result.split("\n") key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings key_version = '1' version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully) if version_search: key_version = version_search[1] install_openvpn_key(name, key_data, key_version) if file: write_file(f'{name}.key', result) def generate_wireguard_key(interface=None, install=False): private_key = cmd('wg genkey') public_key = cmd('wg pubkey', input=private_key) if interface and install: install_wireguard_key(interface, private_key, public_key) else: print(f'Private key: {private_key}') print(f'Public key: {public_key}', end='\n\n') def generate_wireguard_psk(interface=None, peer=None, install=False): psk = cmd('wg genpsk') if interface and peer and install: install_wireguard_psk(interface, peer, psk) else: print(f'Pre-shared key: {psk}') # Import functions def import_ca_certificate(name, path=None, key_path=None): if path: if not os.path.exists(path): print(f'File not found: {path}') return cert = None with open(path) as f: cert_data = f.read() cert = load_certificate(cert_data, wrap_tags=False) if not cert: print(f'Invalid certificate: {path}') return install_certificate(name, cert, is_ca=True) if key_path: if not os.path.exists(key_path): print(f'File not found: {key_path}') return key = None passphrase = ask_input('Enter private key passphrase: ') or None with open(key_path) as f: key_data = f.read() key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) if not key: print(f'Invalid private key or passphrase: {path}') return install_certificate(name, private_key=key, is_ca=True) def import_certificate(name, path=None, key_path=None): if path: if not os.path.exists(path): print(f'File not found: {path}') return cert = None with open(path) as f: cert_data = f.read() cert = load_certificate(cert_data, wrap_tags=False) if not cert: print(f'Invalid certificate: {path}') return install_certificate(name, cert, is_ca=False) if key_path: if not os.path.exists(key_path): print(f'File not found: {key_path}') return key = None passphrase = ask_input('Enter private key passphrase: ') or None with open(key_path) as f: key_data = f.read() key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) if not key: print(f'Invalid private key or passphrase: {path}') return install_certificate(name, private_key=key, is_ca=False) def import_crl(name, path): if not os.path.exists(path): print(f'File not found: {path}') return crl = None with open(path) as f: crl_data = f.read() crl = load_crl(crl_data, wrap_tags=False) if not crl: print(f'Invalid certificate: {path}') return install_crl(name, crl) def import_dh_parameters(name, path): if not os.path.exists(path): print(f'File not found: {path}') return dh = None with open(path) as f: dh_data = f.read() dh = load_dh_parameters(dh_data, wrap_tags=False) if not dh: print(f'Invalid DH parameters: {path}') return install_dh_parameters(name, dh) def import_keypair(name, path=None, key_path=None): if path: if not os.path.exists(path): print(f'File not found: {path}') return key = None with open(path) as f: key_data = f.read() key = load_public_key(key_data, wrap_tags=False) if not key: print(f'Invalid public key: {path}') return install_keypair(name, None, public_key=key, prompt=False) if key_path: if not os.path.exists(key_path): print(f'File not found: {key_path}') return key = None passphrase = ask_input('Enter private key passphrase: ') or None with open(key_path) as f: key_data = f.read() key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) if not key: print(f'Invalid private key or passphrase: {path}') return install_keypair(name, None, private_key=key, prompt=False) def import_openvpn_secret(name, path): if not os.path.exists(path): print(f'File not found: {path}') return key_data = None key_version = '1' with open(path) as f: key_lines = f.read().split("\n") key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', key_lines[0]) # Future-proofing (hopefully) if version_search: key_version = version_search[1] install_openvpn_key(name, key_data, key_version) # Show functions def show_certificate_authority(name=None, pem=False): headers = ['Name', 'Subject', 'Issuer CN', 'Issued', 'Expiry', 'Private Key', 'Parent'] data = [] certs = get_config_ca_certificate() if certs: for cert_name, cert_dict in certs.items(): if name and name != cert_name: continue if 'certificate' not in cert_dict: continue cert = load_certificate(cert_dict['certificate']) if name and pem: print(encode_certificate(cert)) return parent_ca_name = get_certificate_ca(cert, certs) cert_issuer_cn = cert.issuer.rfc4514_string().split(",")[0] if not parent_ca_name or parent_ca_name == cert_name: parent_ca_name = 'N/A' if not cert: continue have_private = 'Yes' if 'private' in cert_dict and 'key' in cert_dict['private'] else 'No' data.append([cert_name, cert.subject.rfc4514_string(), cert_issuer_cn, cert.not_valid_before, cert.not_valid_after, have_private, parent_ca_name]) print("Certificate Authorities:") print(tabulate.tabulate(data, headers)) def show_certificate(name=None, pem=False): headers = ['Name', 'Type', 'Subject CN', 'Issuer CN', 'Issued', 'Expiry', 'Revoked', 'Private Key', 'CA Present'] data = [] certs = get_config_certificate() if certs: ca_certs = get_config_ca_certificate() for cert_name, cert_dict in certs.items(): if name and name != cert_name: continue if 'certificate' not in cert_dict: continue cert = load_certificate(cert_dict['certificate']) if not cert: continue if name and pem: print(encode_certificate(cert)) return ca_name = get_certificate_ca(cert, ca_certs) cert_subject_cn = cert.subject.rfc4514_string().split(",")[0] cert_issuer_cn = cert.issuer.rfc4514_string().split(",")[0] cert_type = 'Unknown' try: ext = cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) if ext and ExtendedKeyUsageOID.SERVER_AUTH in ext.value: cert_type = 'Server' elif ext and ExtendedKeyUsageOID.CLIENT_AUTH in ext.value: cert_type = 'Client' except: pass revoked = 'Yes' if 'revoke' in cert_dict else 'No' have_private = 'Yes' if 'private' in cert_dict and 'key' in cert_dict['private'] else 'No' have_ca = f'Yes ({ca_name})' if ca_name else 'No' data.append([ cert_name, cert_type, cert_subject_cn, cert_issuer_cn, cert.not_valid_before, cert.not_valid_after, revoked, have_private, have_ca]) print("Certificates:") print(tabulate.tabulate(data, headers)) def show_certificate_fingerprint(name, hash): cert = get_config_certificate(name=name) cert = load_certificate(cert['certificate']) print(get_certificate_fingerprint(cert, hash)) def show_crl(name=None, pem=False): headers = ['CA Name', 'Updated', 'Revokes'] data = [] certs = get_config_ca_certificate() if certs: for cert_name, cert_dict in certs.items(): if name and name != cert_name: continue if 'crl' not in cert_dict: continue crls = cert_dict['crl'] if isinstance(crls, str): crls = [crls] for crl_data in cert_dict['crl']: crl = load_crl(crl_data) if not crl: continue if name and pem: print(encode_certificate(crl)) continue certs = get_revoked_by_serial_numbers([revoked.serial_number for revoked in crl]) data.append([cert_name, crl.last_update, ", ".join(certs)]) if name and pem: return print("Certificate Revocation Lists:") print(tabulate.tabulate(data, headers)) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--action', help='PKI action', required=True) # X509 parser.add_argument('--ca', help='Certificate Authority', required=False) parser.add_argument('--certificate', help='Certificate', required=False) parser.add_argument('--crl', help='Certificate Revocation List', required=False) parser.add_argument('--sign', help='Sign certificate with specified CA', required=False) parser.add_argument('--self-sign', help='Self-sign the certificate', action='store_true') parser.add_argument('--pem', help='Output using PEM encoding', action='store_true') parser.add_argument('--fingerprint', help='Show fingerprint and exit', action='store') # SSH parser.add_argument('--ssh', help='SSH Key', required=False) # DH parser.add_argument('--dh', help='DH Parameters', required=False) # Key pair parser.add_argument('--keypair', help='Key pair', required=False) # OpenVPN parser.add_argument('--openvpn', help='OpenVPN TLS key', required=False) # WireGuard parser.add_argument('--wireguard', help='Wireguard', action='store_true') group = parser.add_mutually_exclusive_group() group.add_argument('--key', help='Wireguard key pair', action='store_true', required=False) group.add_argument('--psk', help='Wireguard pre shared key', action='store_true', required=False) parser.add_argument('--interface', help='Install generated keys into running-config for named interface', action='store') parser.add_argument('--peer', help='Install generated keys into running-config for peer', action='store') # Global parser.add_argument('--file', help='Write generated keys into specified filename', action='store_true') parser.add_argument('--install', help='Install generated keys into running-config', action='store_true') parser.add_argument('--filename', help='Write certificate into specified filename', action='store') parser.add_argument('--key-filename', help='Write key into specified filename', action='store') args = parser.parse_args() try: if args.action == 'generate': if args.ca: if args.sign: generate_ca_certificate_sign(args.ca, args.sign, install=args.install, file=args.file) else: generate_ca_certificate(args.ca, install=args.install, file=args.file) elif args.certificate: if args.sign: generate_certificate_sign(args.certificate, args.sign, install=args.install, file=args.file) elif args.self_sign: generate_certificate_selfsign(args.certificate, install=args.install, file=args.file) else: generate_certificate_request(name=args.certificate, install=args.install, file=args.file) elif args.crl: generate_certificate_revocation_list(args.crl, install=args.install, file=args.file) elif args.ssh: generate_ssh_keypair(args.ssh, install=args.install, file=args.file) elif args.dh: generate_dh_parameters(args.dh, install=args.install, file=args.file) elif args.keypair: generate_keypair(args.keypair, install=args.install, file=args.file) elif args.openvpn: generate_openvpn_key(args.openvpn, install=args.install, file=args.file) elif args.wireguard: # WireGuard supports writing key directly into the CLI, but this # requires the vyos_libexec_dir environment variable to be set os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos" if args.key: generate_wireguard_key(args.interface, install=args.install) if args.psk: generate_wireguard_psk(args.interface, peer=args.peer, install=args.install) elif args.action == 'import': if args.ca: import_ca_certificate(args.ca, path=args.filename, key_path=args.key_filename) elif args.certificate: import_certificate(args.certificate, path=args.filename, key_path=args.key_filename) elif args.crl: import_crl(args.crl, args.filename) elif args.dh: import_dh_parameters(args.dh, args.filename) elif args.keypair: import_keypair(args.keypair, path=args.filename, key_path=args.key_filename) elif args.openvpn: import_openvpn_secret(args.openvpn, args.filename) elif args.action == 'show': if args.ca: ca_name = None if args.ca == 'all' else args.ca if ca_name: if not conf.exists(['pki', 'ca', ca_name]): print(f'CA "{ca_name}" does not exist!') exit(1) show_certificate_authority(ca_name, args.pem) elif args.certificate: cert_name = None if args.certificate == 'all' else args.certificate if cert_name: if not conf.exists(['pki', 'certificate', cert_name]): print(f'Certificate "{cert_name}" does not exist!') exit(1) if args.fingerprint is None: show_certificate(None if args.certificate == 'all' else args.certificate, args.pem) else: show_certificate_fingerprint(args.certificate, args.fingerprint) elif args.crl: show_crl(None if args.crl == 'all' else args.crl, args.pem) else: show_certificate_authority() + print('\n') show_certificate() + print('\n') show_crl() except KeyboardInterrupt: print("Aborted") sys.exit(0)