summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/https/nginx.default.j2101
-rw-r--r--data/templates/https/vyos-http-api.service.j22
-rw-r--r--interface-definitions/include/pki/dh-params.xml.i10
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in9
-rw-r--r--interface-definitions/service_https.xml.in89
-rw-r--r--python/vyos/defaults.py12
-rw-r--r--smoketest/config-tests/basic-api-service12
-rw-r--r--smoketest/configs/basic-api-service1
-rwxr-xr-xsmoketest/scripts/cli/test_pki.py52
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py87
-rwxr-xr-xsrc/conf_mode/service_https.py237
-rw-r--r--src/etc/systemd/system/nginx.service.d/10-override.conf3
-rwxr-xr-xsrc/migration-scripts/https/5-to-676
-rwxr-xr-xsrc/services/vyos-http-api-server10
14 files changed, 341 insertions, 360 deletions
diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2
index a530c14ba..5d17df001 100644
--- a/data/templates/https/nginx.default.j2
+++ b/data/templates/https/nginx.default.j2
@@ -1,60 +1,65 @@
### Autogenerated by service_https.py ###
-# Default server configuration
-{% for server in server_block_list %}
+{% if enable_http_redirect is vyos_defined %}
server {
- # SSL configuration
- #
-{% if server.address == '*' %}
- listen {{ server.port }} ssl;
- listen [::]:{{ server.port }} ssl;
-{% else %}
- listen {{ server.address | bracketize_ipv6 }}:{{ server.port }} ssl;
-{% endif %}
+ listen 80 default_server;
+ server_name {{ hostname }};
+ return 301 https://$host$request_uri;
+}
+{% endif %}
-{% for name in server.name %}
- server_name {{ name }};
+server {
+{% if listen_address is vyos_defined %}
+{% for address in listen_address %}
+ listen {{ address | bracketize_ipv6 }}:{{ port }} ssl;
{% endfor %}
+{% else %}
+ listen {{ port }} ssl;
+ listen [::]:{{ port }} ssl;
+{% endif %}
- root /srv/localui;
+ server_name {{ hostname }};
+ root /srv/localui;
-{% 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;
+ # SSL configuration
+{% if certificates.cert_path is vyos_defined and certificates.key_path is vyos_defined %}
+ ssl_certificate {{ certificates.cert_path }};
+ ssl_certificate_key {{ certificates.key_path }};
+{% if certificates.dh_file is vyos_defined %}
+ ssl_dhparam {{ certificates.dh_file }};
{% endif %}
- ssl_session_cache shared:le_nginx_SSL:10m;
- ssl_session_timeout 1440m;
- ssl_session_tickets off;
+{% else %}
+ # Self signed certs generated by the ssl-cert package
+ # Don't use them in a production server!
+ include snippets/snakeoil.conf;
+{% endif %}
- 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';
+ # Improve HTTPS performance with session resumption
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
+ ssl_protocols {{ 'TLSv' ~ ' TLSv'.join(tls_version) }};
- # 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 %}
- }
+ # From LetsEncrypt
+ ssl_prefer_server_ciphers on;
+ 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';
- error_page 497 =301 https://$host:{{ server.port }}$request_uri;
+ # 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 api is vyos_defined %}
+ 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 allow_client.address is vyos_defined %}
+{% for address in allow_client.address %}
+ allow {{ address }};
+{% endfor %}
+ deny all;
+{% endif %}
+ }
+ error_page 497 =301 https://$host:{{ port }}$request_uri;
}
-
-{% endfor %}
diff --git a/data/templates/https/vyos-http-api.service.j2 b/data/templates/https/vyos-http-api.service.j2
index f620b3248..aa4da7666 100644
--- a/data/templates/https/vyos-http-api.service.j2
+++ b/data/templates/https/vyos-http-api.service.j2
@@ -3,6 +3,7 @@
Description=VyOS HTTP API service
After=vyos-router.service
Requires=vyos-router.service
+ConditionPathExists={{ api_config_state }}
[Service]
ExecStart={{ vrf_command }}/usr/libexec/vyos/services/vyos-http-api-server
@@ -20,4 +21,3 @@ Group=vyattacfg
[Install]
WantedBy=vyos.target
-
diff --git a/interface-definitions/include/pki/dh-params.xml.i b/interface-definitions/include/pki/dh-params.xml.i
new file mode 100644
index 000000000..a422df832
--- /dev/null
+++ b/interface-definitions/include/pki/dh-params.xml.i
@@ -0,0 +1,10 @@
+<!-- include start from pki/certificate-multi.xml.i -->
+<leafNode name="dh-params">
+ <properties>
+ <help>Diffie Hellman parameters (server only)</help>
+ <completionHelp>
+ <path>pki dh</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in
index dadf5cb48..f7e8f8b9f 100644
--- a/interface-definitions/interfaces_openvpn.xml.in
+++ b/interface-definitions/interfaces_openvpn.xml.in
@@ -736,14 +736,7 @@
</leafNode>
#include <include/pki/certificate.xml.i>
#include <include/pki/ca-certificate-multi.xml.i>
- <leafNode name="dh-params">
- <properties>
- <help>Diffie Hellman parameters (server only)</help>
- <completionHelp>
- <path>pki dh</path>
- </completionHelp>
- </properties>
- </leafNode>
+ #include <include/pki/dh-params.xml.i>
<leafNode name="crypt-key">
<properties>
<help>Static key to use to authenticate control channel</help>
diff --git a/interface-definitions/service_https.xml.in b/interface-definitions/service_https.xml.in
index 57f36a982..b60c7ff2e 100644
--- a/interface-definitions/service_https.xml.in
+++ b/interface-definitions/service_https.xml.in
@@ -8,52 +8,6 @@
<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>
@@ -172,19 +126,18 @@
</node>
</children>
</node>
- <node name="api-restrict">
+ #include <include/allow-client.xml.i>
+ <leafNode name="enable-http-redirect">
<properties>
- <help>Restrict api proxy to subset of virtual hosts</help>
+ <help>Enable HTTP to HTTPS redirect</help>
+ <valueless/>
</properties>
- <children>
- <leafNode name="virtual-host">
- <properties>
- <help>Restrict proxy to virtual host(s)</help>
- <multi/>
- </properties>
- </leafNode>
- </children>
- </node>
+ </leafNode>
+ #include <include/listen-address.xml.i>
+ #include <include/port-number.xml.i>
+ <leafNode name='port'>
+ <defaultValue>443</defaultValue>
+ </leafNode>
<node name="certificates">
<properties>
<help>TLS certificates</help>
@@ -192,8 +145,30 @@
<children>
#include <include/pki/ca-certificate.xml.i>
#include <include/pki/certificate.xml.i>
+ #include <include/pki/dh-params.xml.i>
</children>
</node>
+ <leafNode name="tls-version">
+ <properties>
+ <help>Specify available TLS version(s)</help>
+ <completionHelp>
+ <list>1.2 1.3</list>
+ </completionHelp>
+ <valueHelp>
+ <format>1.2</format>
+ <description>TLSv1.2</description>
+ </valueHelp>
+ <valueHelp>
+ <format>1.3</format>
+ <description>TLSv1.3</description>
+ </valueHelp>
+ <constraint>
+ <regex>(1.2|1.3)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ <defaultValue>1.2 1.3</defaultValue>
+ </leafNode>
#include <include/interface/vrf.xml.i>
</children>
</node>
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 2f3580571..64145a42e 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -37,6 +37,7 @@ directories = {
}
config_status = '/tmp/vyos-config-status'
+api_config_state = '/run/http-api-state'
cfg_group = 'vyattacfg'
@@ -45,14 +46,3 @@ cfg_vintage = 'vyos'
commit_lock = '/opt/vyatta/config/.lock'
component_version_json = os.path.join(directories['data'], 'component-versions.json')
-
-https_data = {
- 'listen_addresses' : { '*': ['_'] }
-}
-
-vyos_cert_data = {
- 'conf' : '/etc/nginx/snippets/vyos-cert.conf',
- 'crt' : '/etc/ssl/certs/vyos-selfsigned.crt',
- 'key' : '/etc/ssl/private/vyos-selfsign',
- 'lifetime' : '365',
-}
diff --git a/smoketest/config-tests/basic-api-service b/smoketest/config-tests/basic-api-service
index 1d2dc3472..dc54929b9 100644
--- a/smoketest/config-tests/basic-api-service
+++ b/smoketest/config-tests/basic-api-service
@@ -4,15 +4,11 @@ set interfaces loopback lo
set service ntp server time1.vyos.net
set service ntp server time2.vyos.net
set service ntp server time3.vyos.net
+set service https allow-client address '172.16.0.0/12'
+set service https allow-client address '192.168.0.0/16'
+set service https allow-client address '10.0.0.0/8'
+set service https allow-client address '2001:db8::/32'
set service https api keys id 1 key 'S3cur3'
-set service https virtual-host bar allow-client address '172.16.0.0/12'
-set service https virtual-host bar port '5555'
-set service https virtual-host foo allow-client address '10.0.0.0/8'
-set service https virtual-host foo allow-client address '2001:db8::/32'
-set service https virtual-host foo port '7777'
-set service https virtual-host baz allow-client address '192.168.0.0/16'
-set service https virtual-host baz port '6666'
-set service https virtual-host baz server-name 'baz'
set system config-management commit-revisions '100'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/'
diff --git a/smoketest/configs/basic-api-service b/smoketest/configs/basic-api-service
index f5b56ac98..f997ccd73 100644
--- a/smoketest/configs/basic-api-service
+++ b/smoketest/configs/basic-api-service
@@ -29,6 +29,7 @@ service {
allow-client {
address 192.168.0.0/16
}
+ listen-address "*"
listen-port 6666
server-name baz
}
diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py
index 2ccc63b2c..940ff9ec0 100755
--- a/smoketest/scripts/cli/test_pki.py
+++ b/smoketest/scripts/cli/test_pki.py
@@ -19,6 +19,8 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
+
base_path = ['pki']
valid_ca_cert = """
@@ -153,10 +155,10 @@ class TestPKI(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
super(TestPKI, 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, ['service', 'https'])
def tearDown(self):
self.cli_delete(base_path)
@@ -181,68 +183,72 @@ class TestPKI(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
def test_invalid_ca_valid_certificate(self):
- self.cli_set(base_path + ['ca', 'smoketest', 'certificate', valid_cert.replace('\n','')])
+ self.cli_set(base_path + ['ca', 'invalid-ca', 'certificate', valid_cert.replace('\n','')])
with self.assertRaises(ConfigSessionError):
self.cli_commit()
def test_certificate_in_use(self):
- self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')])
- self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')])
+ cert_name = 'smoketest'
+
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')])
self.cli_commit()
- self.cli_set(['service', 'https', 'certificates', 'certificate', 'smoketest'])
+ self.cli_set(['service', 'https', 'certificates', 'certificate', cert_name])
self.cli_commit()
- self.cli_delete(base_path + ['certificate', 'smoketest'])
+ self.cli_delete(base_path + ['certificate', cert_name])
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_delete(['service', 'https', 'certificates', 'certificate'])
def test_certificate_https_update(self):
- self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')])
- self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')])
+ cert_name = 'smoketest'
+ cert_path = f'/run/nginx/certs/{cert_name}_cert.pem'
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')])
self.cli_commit()
- self.cli_set(['service', 'https', 'certificates', 'certificate', 'smoketest'])
+ self.cli_set(['service', 'https', 'certificates', 'certificate', cert_name])
self.cli_commit()
cert_data = None
- with open('/etc/ssl/certs/smoketest.pem') as f:
- cert_data = f.read()
+ cert_data = read_file(cert_path)
- self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_update_cert.replace('\n','')])
- self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_update_private_key.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_update_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_update_private_key.replace('\n','')])
self.cli_commit()
- with open('/etc/ssl/certs/smoketest.pem') as f:
- self.assertNotEqual(cert_data, f.read())
+ self.assertNotEqual(cert_data, read_file(cert_path))
self.cli_delete(['service', 'https', 'certificates', 'certificate'])
def test_certificate_eapol_update(self):
- self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')])
- self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')])
+ cert_name = 'eapol'
+ interface = 'eth1'
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')])
self.cli_commit()
- self.cli_set(['interfaces', 'ethernet', 'eth1', 'eapol', 'certificate', 'smoketest'])
+ self.cli_set(['interfaces', 'ethernet', interface, 'eapol', 'certificate', cert_name])
self.cli_commit()
cert_data = None
- with open('/run/wpa_supplicant/eth1_cert.pem') as f:
+ with open(f'/run/wpa_supplicant/{interface}_cert.pem') as f:
cert_data = f.read()
- self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_update_cert.replace('\n','')])
- self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_update_private_key.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_update_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_update_private_key.replace('\n','')])
self.cli_commit()
- with open('/run/wpa_supplicant/eth1_cert.pem') as f:
+ with open(f'/run/wpa_supplicant/{interface}_cert.pem') as f:
self.assertNotEqual(cert_data, f.read())
- self.cli_delete(['interfaces', 'ethernet', 'eth1', 'eapol'])
+ self.cli_delete(['interfaces', 'ethernet', interface, 'eapol'])
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py
index 280932fd7..8d9b8459e 100755
--- a/smoketest/scripts/cli/test_service_https.py
+++ b/smoketest/scripts/cli/test_service_https.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# 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
@@ -23,6 +23,7 @@ from urllib3.exceptions import InsecureRequestWarning
from base_vyostest_shim import VyOSUnitTestSHIM
from base_vyostest_shim import ignore_warning
from vyos.utils.file import read_file
+from vyos.utils.file import write_file
from vyos.utils.process import call
from vyos.utils.process import process_named_running
@@ -52,7 +53,22 @@ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx
u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww
"""
+dh_1024 = """
+MIGHAoGBAM3nvMkHGi/xmRs8cYg4pcl5sAanxel9EM+1XobVhUViXw8JvlmSEVOj
+n2aXUifc4SEs3WDzVPRC8O8qQWjvErpTq/HOgt3aqBCabMgvflmt706XP0KiqnpW
+EyvNiI27J3wBUzEXLIS110MxPAX5Tcug974PecFcOxn1RWrbWcx/AgEC
+"""
+
+dh_2048 = """
+MIIBCAKCAQEA1mld/V7WnxxRinkOlhx/BoZkRELtIUQFYxyARBqYk4C5G3YnZNNu
+zjaGyPnfIKHu8SIUH85OecM+5/co9nYlcUJuph2tbR6qNgPw7LOKIhf27u7WhvJk
+iVsJhwZiWmvvMV4jTParNEI2svoooMyhHXzeweYsg6YtgLVmwiwKj3XP3gRH2i3B
+Mq8CDS7X6xaKvjfeMPZBFqOM5nb6HhsbaAUyiZxrfipLvXxtnbzd/eJUQVfVdxM3
+pn0i+QrO2tuNAzX7GoPc9pefrbb5xJmGS50G0uqsR59+7LhYmyZSBASA0lxTEW9t
+kv/0LPvaYTY57WL7hBeqqHy/WPZHPzDI3wIBAg==
+"""
# to test load config via HTTP URL
+nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest'
nginx_conf_smoketest = """
server {
listen 8000;
@@ -81,6 +97,11 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
cls.cli_delete(cls, base_path)
cls.cli_delete(cls, pki_base)
+ @classmethod
+ def tearDownClass(cls):
+ super(TestHTTPSService, cls).tearDownClass()
+ call(f'sudo rm -f {nginx_tmp_site}')
+
def tearDown(self):
self.cli_delete(base_path)
self.cli_delete(pki_base)
@@ -89,33 +110,31 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
# Check for stopped process
self.assertFalse(process_named_running(PROCESS_NAME))
- def test_server_block(self):
- vhost_id = 'example'
- address = '0.0.0.0'
- port = '8443'
- name = 'example.org'
-
- test_path = base_path + ['virtual-host', vhost_id]
-
- self.cli_set(test_path + ['listen-address', address])
- self.cli_set(test_path + ['port', port])
- self.cli_set(test_path + ['server-name', name])
-
- self.cli_commit()
-
- nginx_config = read_file('/etc/nginx/sites-enabled/default')
- self.assertIn(f'listen {address}:{port} ssl;', nginx_config)
- self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config)
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_certificate(self):
- self.cli_set(pki_base + ['certificate', 'test_https', 'certificate', cert_data.replace('\n','')])
- self.cli_set(pki_base + ['certificate', 'test_https', 'private', 'key', key_data.replace('\n','')])
-
- self.cli_set(base_path + ['certificates', 'certificate', 'test_https'])
+ cert_name = 'test_https'
+ dh_name = 'dh-test'
+
+ self.cli_set(base_path + ['certificates', 'certificate', cert_name])
+ # verify() - certificates do not exist (yet)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(pki_base + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')])
+ self.cli_set(pki_base + ['certificate', cert_name, 'private', 'key', key_data.replace('\n','')])
+
+ self.cli_set(base_path + ['certificates', 'dh-params', dh_name])
+ # verify() - dh-params do not exist (yet)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n','')])
+ # verify() - dh-param minimum length is 2048 bit
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n','')])
self.cli_commit()
self.assertTrue(process_named_running(PROCESS_NAME))
+ self.debug = False
def test_api_missing_keys(self):
self.cli_set(base_path + ['api'])
@@ -135,15 +154,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
key = 'MySuperSecretVyOS'
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
- test_path = base_path + ['virtual-host', vhost_id]
- self.cli_set(test_path + ['listen-address', address])
- self.cli_set(test_path + ['server-name', name])
+ self.cli_set(base_path + ['listen-address', address])
self.cli_commit()
nginx_config = read_file('/etc/nginx/sites-enabled/default')
self.assertIn(f'listen {address}:{port} ssl;', nginx_config)
- self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config)
+ self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default
url = f'https://{address}/retrieve'
payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
@@ -402,19 +419,15 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
url_config = f'https://{address}/configure'
headers = {}
tmp_file = 'tmp-config.boot'
- nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest'
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_commit()
# load config via HTTP requires nginx config
call(f'sudo touch {nginx_tmp_site}')
- call(f'sudo chown vyos:vyattacfg {nginx_tmp_site}')
- call(f'sudo chmod +w {nginx_tmp_site}')
-
- with open(nginx_tmp_site, 'w') as f:
- f.write(nginx_conf_smoketest)
- call('sudo nginx -s reload')
+ call(f'sudo chmod 666 {nginx_tmp_site}')
+ write_file(nginx_tmp_site, nginx_conf_smoketest)
+ call('sudo systemctl reload nginx')
# save config
payload = {
@@ -441,8 +454,8 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
self.assertEqual(r.status_code, 200)
# cleanup tmp nginx conf
- call(f'sudo rm -rf {nginx_tmp_site}')
- call('sudo nginx -s reload')
+ call(f'sudo rm -f {nginx_tmp_site}')
+ call('sudo systemctl reload nginx')
if __name__ == '__main__':
unittest.main(verbosity=5)
diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py
index 2e7ebda5a..46efc3c93 100755
--- a/src/conf_mode/service_https.py
+++ b/src/conf_mode/service_https.py
@@ -15,51 +15,41 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import socket
import sys
import json
-from copy import deepcopy
from time import sleep
-import vyos.defaults
-
from vyos.base import Warning
from vyos.config import Config
+from vyos.config import config_dict_merge
from vyos.configdiff import get_config_diff
from vyos.configverify import verify_vrf
-from vyos import ConfigError
+from vyos.defaults import api_config_state
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
+from vyos.pki import wrap_dh_parameters
+from vyos.pki import load_dh_parameters
from vyos.template import render
+from vyos.utils.dict import dict_search
from vyos.utils.process import call
+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 ConfigError
from vyos import airbag
airbag.enable()
-config_file = '/etc/nginx/sites-available/default'
+config_file = '/etc/nginx/sites-enabled/default'
systemd_override = r'/run/systemd/system/nginx.service.d/override.conf'
-cert_dir = '/etc/ssl/certs'
-key_dir = '/etc/ssl/private'
-
-api_config_state = '/run/http-api-state'
-systemd_service = '/run/systemd/system/vyos-http-api.service'
-
-# 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' : {},
-}
+cert_dir = '/run/nginx/certs'
+
+user = 'www-data'
+group = 'www-data'
+
+systemd_service_api = '/run/systemd/system/vyos-http-api.service'
def get_config(config=None):
if config:
@@ -71,83 +61,70 @@ def get_config(config=None):
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 = conf.get_config_dict(base, get_first_key=True,
+ key_mangling=('-', '_'),
+ with_pki=True)
- https['api_add_or_delete'] = diff.node_changed_presence(base + ['api'])
+ # store path to API config file for later use in templates
+ https['api_config_state'] = api_config_state
+ # get fully qualified system hsotname
+ https['hostname'] = socket.getfqdn()
- if 'api' not in https:
- return https
+ # 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(**https.kwargs, recursive=True)
+ if 'api' not in https or 'graphql' not in https['api']:
+ del default_values['api']
- 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
+ # merge CLI and default dictionary
+ https = config_dict_merge(default_values, https)
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 'certificates' in https and 'certificate' in https['certificates']:
+ cert_name = https['certificates']['certificate']
+ if 'pki' not in https:
+ raise ConfigError('PKI is not configured!')
- if 'certificate' in certificates:
- if not https['pki']:
- raise ConfigError('PKI is not configured')
+ if cert_name not in https['pki']['certificate']:
+ raise ConfigError('Invalid certificate in configuration!')
- cert_name = certificates['certificate']
+ pki_cert = https['pki']['certificate'][cert_name]
- if cert_name not in https['pki']['certificate']:
- raise ConfigError('Invalid certificate on https configuration')
+ if 'certificate' not in pki_cert:
+ raise ConfigError('Missing certificate in configuration!')
- pki_cert = https['pki']['certificate'][cert_name]
+ if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+ raise ConfigError('Missing certificate private key in configuration!')
- if 'certificate' not in pki_cert:
- raise ConfigError('Missing certificate on https configuration')
+ if 'dh_params' in https['certificates']:
+ dh_name = https['certificates']['dh_params']
+ if dh_name not in https['pki']['dh']:
+ raise ConfigError('Invalid DH parameter in configuration!')
- if 'private' not in pki_cert or 'key' not in pki_cert['private']:
- raise ConfigError("Missing certificate private key on https configuration")
- else:
- Warning('No certificate specified, using buildin self-signed certificates!')
+ pki_dh = https['pki']['dh'][dh_name]
+ dh_params = load_dh_parameters(pki_dh['parameters'])
+ dh_numbers = dh_params.parameter_numbers()
+ dh_bits = dh_numbers.p.bit_length()
+ if dh_bits < 2048:
+ raise ConfigError(f'Minimum DH key-size is 2048 bits')
- server_block_list = []
+ else:
+ Warning('No certificate specified, using build-in self-signed certificates. '\
+ 'Do not use them in a production environment!')
- # organize by vhosts
- vhost_dict = https.get('virtual-host', {})
+ # Check if server port is already in use by a different appliaction
+ listen_address = ['0.0.0.0']
+ port = int(https['port'])
+ if 'listen_address' in https:
+ listen_address = https['listen_address']
- 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')
+ for address in listen_address:
+ if not check_port_availability(address, port, 'tcp') and not is_listen_port_bind_service(port, 'nginx'):
+ raise ConfigError(f'TCP port "{port}" is used by another service!')
verify_vrf(https)
@@ -172,89 +149,61 @@ def verify(https):
# 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')
+ 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.')
+ Warning(f'API keys are not configured: classic (non-GraphQL) API will be unavailable!')
return None
def generate(https):
if https is None:
+ for file in [systemd_service_api, config_file, systemd_override]:
+ if os.path.exists(file):
+ os.unlink(file)
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'])
+ if 'api' in https:
+ render(systemd_service_api, 'https/vyos-http-api.service.j2', https)
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)
+ if os.path.exists(systemd_service_api):
+ os.unlink(systemd_service_api)
# get certificate data
-
- cert_dict = https.get('certificates', {})
-
- if 'certificate' in cert_dict:
- cert_name = cert_dict['certificate']
+ if 'certificates' in https and 'certificate' in https['certificates']:
+ cert_name = https['certificates']['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')
+ cert_path = os.path.join(cert_dir, f'{cert_name}_cert.pem')
+ key_path = os.path.join(cert_dir, f'{cert_name}_key.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']))
+ # Append CA certificate if specified to form a full chain
+ if 'ca_certificate' in https['certificates']:
+ ca_cert = https['certificates']['ca_certificate']
+ server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate']))
- vyos_cert_data = {
- 'crt': cert_path,
- 'key': key_path
- }
+ write_file(cert_path, server_cert, user=user, group=group, mode=0o644)
+ write_file(key_path, wrap_private_key(pki_cert['private']['key']),
+ user=user, group=group, mode=0o600)
- for block in server_block_list:
- block['vyos_cert'] = vyos_cert_data
+ tmp_path = {'cert_path': cert_path, 'key_path': key_path}
- 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
+ if 'dh_params' in https['certificates']:
+ dh_name = https['certificates']['dh_params']
+ pki_dh = https['pki']['dh'][dh_name]
+ if 'parameters' in pki_dh:
+ dh_path = os.path.join(cert_dir, f'{dh_name}_dh.pem')
+ write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']),
+ user=user, group=group, mode=0o600)
+ tmp_path.update({'dh_file' : dh_path})
- data = {
- 'server_block_list': server_block_list,
- }
+ https['certificates'].update(tmp_path)
- render(config_file, 'https/nginx.default.j2', data)
+ render(config_file, 'https/nginx.default.j2', https)
render(systemd_override, 'https/override.conf.j2', https)
return None
@@ -273,7 +222,7 @@ def apply(https):
call(f'systemctl reload-or-restart {http_api_service_name}')
# Let uvicorn settle before (possibly) restarting nginx
sleep(1)
- else:
+ elif is_systemd_service_active(http_api_service_name):
call(f'systemctl stop {http_api_service_name}')
call(f'systemctl reload-or-restart {https_service_name}')
diff --git a/src/etc/systemd/system/nginx.service.d/10-override.conf b/src/etc/systemd/system/nginx.service.d/10-override.conf
new file mode 100644
index 000000000..1be5cec81
--- /dev/null
+++ b/src/etc/systemd/system/nginx.service.d/10-override.conf
@@ -0,0 +1,3 @@
+[Unit]
+After=
+After=vyos-router.service
diff --git a/src/migration-scripts/https/5-to-6 b/src/migration-scripts/https/5-to-6
index b4159f02f..6d6efd32c 100755
--- a/src/migration-scripts/https/5-to-6
+++ b/src/migration-scripts/https/5-to-6
@@ -16,12 +16,14 @@
# T5886: Add support for ACME protocol (LetsEncrypt), migrate https certbot
# to new "pki certificate" CLI tree
+# T5902: Remove virtual-host
import os
import sys
from vyos.configtree import ConfigTree
from vyos.defaults import directories
+from vyos.utils.process import cmd
vyos_certbot_dir = directories['certbot']
@@ -36,30 +38,68 @@ with open(file_name, 'r') as f:
config = ConfigTree(config_file)
-base = ['service', 'https', 'certificates']
+base = ['service', 'https']
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)
+if config.exists(base + ['certificates']):
+ # both domain-name and email must be set on CLI - ensured by previous verify()
+ domain_names = config.return_values(base + ['certificates', 'certbot', 'domain-name'])
+ email = config.return_value(base + ['certificates', 'certbot', 'email'])
+ config.delete(base + ['certificates'])
+
+ # Set default certname based on domain-name
+ cert_name = 'https-' + domain_names[0].split('.')[0]
+ # Overwrite certname from previous certbot calls if available
+ # We can not use python code like os.scandir due to filesystem permissions.
+ # This must be run as root
+ certbot_live = f'{vyos_certbot_dir}/live/' # we need the trailing /
+ if os.path.exists(certbot_live):
+ tmp = cmd(f'sudo find {certbot_live} -maxdepth 1 -type d')
+ tmp = tmp.split() # tmp = ['/config/auth/letsencrypt/live', '/config/auth/letsencrypt/live/router.vyos.net']
+ tmp.remove(certbot_live)
+ cert_name = tmp[0].replace(certbot_live, '')
+
config.set(['pki', 'certificate', cert_name, 'acme', 'email'], value=email)
+ config.set_tag(['pki', 'certificate'])
+ for domain in domain_names:
+ config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False)
+
+ # Update Webserver certificate
+ config.set(base + ['certificates', 'certificate'], value=cert_name)
+
+if config.exists(base + ['virtual-host']):
+ allow_client = []
+ listen_port = []
+ listen_address = []
+ for virtual_host in config.list_nodes(base + ['virtual-host']):
+ allow_path = base + ['virtual-host', virtual_host, 'allow-client', 'address']
+ if config.exists(allow_path):
+ tmp = config.return_values(allow_path)
+ allow_client.extend(tmp)
+
+ port_path = base + ['virtual-host', virtual_host, 'listen-port']
+ if config.exists(port_path):
+ tmp = config.return_value(port_path)
+ listen_port.append(tmp)
+
+ listen_address_path = base + ['virtual-host', virtual_host, 'listen-address']
+ if config.exists(listen_address_path):
+ tmp = config.return_value(listen_address_path)
+ listen_address.append(tmp)
+
+ config.delete(base + ['virtual-host'])
+ for client in allow_client:
+ config.set(base + ['allow-client', 'address'], value=client, replace=False)
+
+ # clear listen-address if "all" were specified
+ if '*' in listen_address:
+ listen_address = []
+ for address in listen_address:
+ config.set(base + ['listen-address'], value=address, replace=False)
+
-# Update Webserver certificate
-config.set(base + ['certificate'], value=cert_name)
try:
with open(file_name, 'w') as f:
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index b64e58132..40d442e30 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -1,6 +1,6 @@
#!/usr/share/vyos-http-api-tools/bin/python3
#
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# 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
@@ -13,8 +13,6 @@
#
# 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
@@ -25,6 +23,7 @@ import logging
import signal
import traceback
import threading
+
from time import sleep
from typing import List, Union, Callable, Dict
@@ -46,11 +45,12 @@ from ariadne.asgi import GraphQL
from vyos.config import Config
from vyos.configtree import ConfigTree
from vyos.configdiff import get_config_diff
-from vyos.configsession import ConfigSession, ConfigSessionError
+from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
+from vyos.defaults import api_config_state
import api.graphql.state
-api_config_state = '/run/http-api-state'
CFG_GROUP = 'vyattacfg'
debug = True