diff --git a/simplyblock_core/models/storage_node.py b/simplyblock_core/models/storage_node.py index 83ae1c1a2..331e21873 100644 --- a/simplyblock_core/models/storage_node.py +++ b/simplyblock_core/models/storage_node.py @@ -150,7 +150,7 @@ def client(self, **kwargs): """Return API client to this node """ host = self.api_endpoint - if Settings().tls_enabled: + if Settings().tls_connect != "disabled": port = self.api_endpoint.rsplit(":", 1)[1] host = f"{self._k8s_node_label()}.simplyblock-storage-node-api.{self.cr_namespace}.svc.cluster.local:{port}" return SNodeClient(host, **kwargs) @@ -159,7 +159,7 @@ def rpc_client(self, **kwargs): """Return rpc client to this node """ host = self.mgmt_ip - if Settings().tls_enabled: + if Settings().tls_connect != "disabled": host = f"{self._k8s_node_label()}.simplyblock-spdk-proxy.{self.cr_namespace}.svc.cluster.local" return RPCClient( host, self.rpc_port, diff --git a/simplyblock_core/rpc_client.py b/simplyblock_core/rpc_client.py index 269be37c1..254f65447 100755 --- a/simplyblock_core/rpc_client.py +++ b/simplyblock_core/rpc_client.py @@ -99,14 +99,14 @@ def __init__(self, host, port, username, password, timeout=180, retry=3): self.host = host self.port = port settings = Settings() - scheme = "https" if settings.tls_enabled else "http" + scheme = "https" if settings.tls_connect != "disabled" else "http" self.url = '%s://%s:%s/' % (scheme, self.host, self.port) self.username = username self.password = password self.timeout = timeout self.session = requests.session() - if settings.tls_enabled: - self.session.verify = str(settings.certificate_authority) + if settings.tls_connect != "disabled": + self.session.verify = str(settings.tls_certificate_authority) self.session.auth = (self.username, self.password) retries = Retry(total=retry, backoff_factor=1, connect=retry, read=retry, allowed_methods=self.DEFAULT_ALLOWED_METHODS) diff --git a/simplyblock_core/services/spdk_http_proxy_server.py b/simplyblock_core/services/spdk_http_proxy_server.py index 5e71742eb..dba46a8a3 100644 --- a/simplyblock_core/services/spdk_http_proxy_server.py +++ b/simplyblock_core/services/spdk_http_proxy_server.py @@ -258,7 +258,7 @@ def run_server(host, port, user, password, is_threading_enabled=False): ServerHandler.key = key httpd = (ThreadingHTTPServer if is_threading_enabled else HTTPServer)((host, port), ServerHandler) settings = Settings() - if settings.tls_enabled: + if settings.tls_serve: context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(settings.tls_certificate, settings.tls_key) httpd.socket = context.wrap_socket(httpd.socket, server_side=True) diff --git a/simplyblock_core/settings.py b/simplyblock_core/settings.py index eb2931ce0..7b7b12aea 100644 --- a/simplyblock_core/settings.py +++ b/simplyblock_core/settings.py @@ -1,19 +1,61 @@ from pathlib import Path +from typing import Annotated, Literal, Optional +from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="sb_", case_sensitive=False) + tls_serve: Annotated[ + bool, + Field( + description="Run servers in TLS mode. Requires certificate and key to be present." + ), + ] = False + tls_connect: Annotated[ + Literal["disabled", "anonymous"], + Field(description="Connect to internal services via TLS."), + ] = "disabled" + tls_provider: Annotated[ + Optional[Literal["openshift", "cert-manager"]], + Field(description="Provider for TLS certificates in the cluster."), + ] = None tls_certificate: Path = Path("/etc/simplyblock/tls/tls.crt") tls_key: Path = Path("/etc/simplyblock/tls/tls.key") - certificate_authority: Path = Path("/etc/simplyblock/tls/ca.crt") - - @property - def tls_enabled(self) -> bool: - return all([ - self.tls_certificate.is_file(), - self.tls_key.is_file(), - self.certificate_authority.is_file(), - ]) + tls_certificate_authority: Path = Path("/etc/simplyblock/tls/ca.crt") + + @model_validator(mode="after") + def validate_tls_files(self): + if not self.tls_serve and self.tls_connect == "disabled": + return self + + if self.tls_serve and ( + missing := [ + name + for name in ["tls_certificate", "tls_key"] + if not getattr(self, name).is_file() + ] + ): + raise ValueError( + "SB_TLS_SERVE=true requires TLS files to exist: " + ", ".join(missing) + ) + + if ( + self.tls_connect != "disabled" + and not self.tls_certificate_authority.is_file() + ): + raise ValueError( + "SB_TLS_CONNECT != 'disabled' requires certificate authority to exist" + ) + + return self + + @model_validator(mode="after") + def validate_tls_provider(self): + if self.tls_connect != "disabled" and self.tls_provider is None: + raise ValueError( + "TLS provider needs to be configured for TLS connections to be used" + ) + return self diff --git a/simplyblock_core/snode_client.py b/simplyblock_core/snode_client.py index 6bbd5cb2b..f807244a3 100644 --- a/simplyblock_core/snode_client.py +++ b/simplyblock_core/snode_client.py @@ -20,12 +20,12 @@ class SNodeClient: def __init__(self, host, timeout=300, retry=5): settings = Settings() - scheme = "https" if settings.tls_enabled else "http" + scheme = "https" if settings.tls_connect != "disabled" else "http" self.url = f'{scheme}://{host}/snode/' self.timeout = timeout self.session = requests.session() - if settings.tls_enabled: - self.session.verify = str(settings.certificate_authority) + if settings.tls_connect != "disabled": + self.session.verify = str(settings.tls_certificate_authority) self.session.headers['Content-Type'] = "application/json" retries = Retry(total=retry, backoff_factor=1, connect=retry, read=retry) self.session.mount("http://", HTTPAdapter(max_retries=retries)) diff --git a/simplyblock_web/api/internal/storage_node/kubernetes.py b/simplyblock_web/api/internal/storage_node/kubernetes.py index 8194db391..1a088918b 100644 --- a/simplyblock_web/api/internal/storage_node/kubernetes.py +++ b/simplyblock_web/api/internal/storage_node/kubernetes.py @@ -281,6 +281,7 @@ class SPDKParams(BaseModel): })}}}, }) def spdk_process_start(body: SPDKParams): + settings = Settings() ssd_pcie_params = " ".join(" -A " + addr for addr in body.ssd_pcie) if body.ssd_pcie else "none" ssd_pcie_list = " ".join(body.ssd_pcie) @@ -373,7 +374,8 @@ def spdk_process_start(body: SPDKParams): 'FW_PORT': body.firewall_port, 'CPU_TOPOLOGY_ENABLED': cpu_topology_enabled, 'RESERVED_SYSTEM_CPUS': reserved_system_cpus, - 'TLS_ENABLED': Settings().tls_enabled, + 'TLS_ENABLED': settings.tls_serve, + 'TLS_PROVIDER': settings.tls_provider, } if ubuntu_host: diff --git a/simplyblock_web/app.py b/simplyblock_web/app.py index fe5b6c377..a827baa94 100644 --- a/simplyblock_web/app.py +++ b/simplyblock_web/app.py @@ -93,8 +93,8 @@ def main() -> None: access_log=False, proxy_headers=True, forwarded_allow_ips='192.168.1.0/24', - ssl_certfile=settings.tls_certificate if settings.tls_enabled else None, - ssl_keyfile=settings.tls_key if settings.tls_enabled else None, + ssl_certfile=settings.tls_certificate if settings.tls_serve else None, + ssl_keyfile=settings.tls_key if settings.tls_serve else None, ) server: uvicorn.Server = uvicorn.Server(config) server.run() diff --git a/simplyblock_web/node_webapp.py b/simplyblock_web/node_webapp.py index 772ed9f7d..df601ac3e 100644 --- a/simplyblock_web/node_webapp.py +++ b/simplyblock_web/node_webapp.py @@ -44,5 +44,5 @@ def status(): app.register_api(internal_api.storage_node.kubernetes.api) settings = Settings() - ssl_ctx = (settings.tls_certificate, settings.tls_key) if settings.tls_enabled else None + ssl_ctx = (settings.tls_certificate, settings.tls_key) if settings.tls_serve else None app.run(host='0.0.0.0', debug=constants.LOG_WEB_DEBUG, ssl_context=ssl_ctx) diff --git a/simplyblock_web/templates/storage_deploy_spdk.yaml.j2 b/simplyblock_web/templates/storage_deploy_spdk.yaml.j2 index 6f6e58979..a334b87d1 100644 --- a/simplyblock_web/templates/storage_deploy_spdk.yaml.j2 +++ b/simplyblock_web/templates/storage_deploy_spdk.yaml.j2 @@ -58,6 +58,10 @@ spec: path: /var/log/pods {% if TLS_ENABLED %} - name: tls + {% if TLS_PROVIDER == "cert-manager" %} + secret: + secretName: simplyblock-spdk-proxy-tls + {% else %} projected: sources: - secret: @@ -68,6 +72,7 @@ spec: - key: service-ca.crt path: ca.crt {% endif %} + {% endif %} containers: - name: spdk-container @@ -155,6 +160,10 @@ spec: value: "True" - name: TIMEOUT value: "300" + - name: SB_TLS_ENABLED + value: "{{ TLS_ENABLED }}" + - name: SB_TLS_PROVIDER + value: "{{ TLS_PROVIDER }}" {% if CPU_TOPOLOGY_ENABLED %} resources: limits: diff --git a/tests/test_storage_deploy_template.py b/tests/test_storage_deploy_template.py new file mode 100644 index 000000000..b8c501543 --- /dev/null +++ b/tests/test_storage_deploy_template.py @@ -0,0 +1,59 @@ +import unittest +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + + +TEMPLATE_DIR = Path(__file__).resolve().parents[1] / "simplyblock_web" / "templates" + + +def _render_storage_deploy(tls_provider: str) -> str: + env = Environment( + loader=FileSystemLoader(str(TEMPLATE_DIR)), + trim_blocks=True, + lstrip_blocks=True, + ) + template = env.get_template("storage_deploy_spdk.yaml.j2") + return template.render( + SPDK_IMAGE="spdk:test", + L_CORES="0-1", + SPDK_MEM=1024, + CORES=2, + SERVER_IP="10.0.0.10", + RPC_PORT=8080, + RPC_USERNAME="admin", + RPC_PASSWORD="secret", + HOSTNAME="node-a", + NAMESPACE="simplyblock", + SIMPLYBLOCK_DOCKER_IMAGE="proxy:test", + GRAYLOG_SERVER_IP="10.0.0.20", + MODE="kubernetes", + CLUSTER_ID="cluster1", + SSD_PCIE="none", + PCI_ALLOWED="", + TOTAL_HP="", + NSOCKET=0, + FW_PORT=50001, + CPU_TOPOLOGY_ENABLED=False, + MEM_MEGA=1536, + MEM2_MEGA=1024, + TLS_ENABLED=True, + TLS_PROVIDER=tls_provider, + ) + + +class TestStorageDeployTemplate(unittest.TestCase): + + def test_openshift_uses_service_ca_key(self): + rendered = _render_storage_deploy("openshift") + self.assertIn("key: service-ca.crt", rendered) + self.assertIn('name: SB_TLS_PROVIDER', rendered) + self.assertIn('value: "openshift"', rendered) + + def test_cert_manager_mounts_secret_directly(self): + rendered = _render_storage_deploy("cert-manager") + self.assertIn("secretName: simplyblock-spdk-proxy-tls", rendered) + self.assertNotIn("simplyblock-certificate-authority", rendered) + self.assertNotIn("projected:", rendered) + self.assertIn('name: SB_TLS_PROVIDER', rendered) + self.assertIn('value: "cert-manager"', rendered)