From 34eadcf2f74ae57342997bed77ce64bddd34219b Mon Sep 17 00:00:00 2001
From: Christian Breunig <christian@breunig.cc>
Date: Sat, 6 Jan 2024 10:55:42 +0100
Subject: https: T5902: remove virtual-host configuration

We have not seen the adoption of the https virtual-host CLI option.

What it did?
* Create multiple webservers each listening on a different IP/port
  (but in the same VRF)
* All webservers shared one common document root
* All webservers shared the same SSL certificates
* All webservers could have had individual allow-client configurations
* API could be enabled for a particular virtual-host but was always enabled on
  the default host

This configuration tried to provide a full webserver via the CLI but VyOS is a
router and the Webserver is there for an API or to serve files for a local-ui.

Changes

Remove support for virtual-hosts as it's an incomplete and thus mostly useless
"thing". Migrate all allow-client statements to one top-level allow statement.

(cherry picked from commit d0d3071e99eb65edb888c26ef2fdc9e038438887)
---
 data/templates/https/nginx.default.j2              | 101 ++++-----
 data/templates/https/vyos-http-api.service.j2      |   2 +-
 interface-definitions/include/pki/dh-params.xml.i  |  10 +
 interface-definitions/interfaces_openvpn.xml.in    |   9 +-
 interface-definitions/service_https.xml.in         |  89 +++-----
 python/vyos/defaults.py                            |  12 +-
 smoketest/config-tests/basic-api-service           |  12 +-
 smoketest/configs/basic-api-service                |   1 +
 smoketest/scripts/cli/test_pki.py                  |  52 +++--
 smoketest/scripts/cli/test_service_https.py        |  87 ++++----
 src/conf_mode/service_https.py                     | 237 ++++++++-------------
 .../system/nginx.service.d/10-override.conf        |   3 +
 src/migration-scripts/https/5-to-6                 |  76 +++++--
 src/services/vyos-http-api-server                  |  10 +-
 14 files changed, 341 insertions(+), 360 deletions(-)
 create mode 100644 interface-definitions/include/pki/dh-params.xml.i
 create mode 100644 src/etc/systemd/system/nginx.service.d/10-override.conf

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
-- 
cgit v1.2.3