From 1a2e0ae9a949836943f92713463117cbacea94f9 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:54:29 +0100 Subject: [PATCH 01/12] feat: cyberchannel --- doc/source/user_guide/index.rst | 10 +- doc/source/user_guide/secure_grpc.rst | 162 +++++++++ src/ansys/tools/common/cyberchannel.py | 479 +++++++++++++++++++++++++ 3 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 doc/source/user_guide/secure_grpc.rst create mode 100644 src/ansys/tools/common/cyberchannel.py diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst index e8eda3c7..4bd7a076 100644 --- a/doc/source/user_guide/index.rst +++ b/doc/source/user_guide/index.rst @@ -55,6 +55,13 @@ This section explains key concepts for implementing the tools in Ansys Common To Learn how to use the local launcher tool. + .. grid-item-card:: Securing gRPC connections + :padding: 2 2 2 2 + :link: secure_grpc + :link-type: doc + + Learn how to secure gRPC connections in PyAnsys. + .. toctree:: :hidden: :maxdepth: 3 @@ -64,4 +71,5 @@ This section explains key concepts for implementing the tools in Ansys Common To ansys_exceptions versioning report - launcher/index \ No newline at end of file + launcher/index + secure_grpc diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst new file mode 100644 index 00000000..e723abbe --- /dev/null +++ b/doc/source/user_guide/secure_grpc.rst @@ -0,0 +1,162 @@ +Securing gRPC connections +######################### + +The PyAnsys ecosystem enables now various transport modes for securing gRPC connections. +This page reviews the available transport modes, and how to use them. + +Supported transport modes +========================= + +PyAnsys supports the following transport modes for gRPC connections: + +- **Mutual TLS (mTLS).** This mode allows secure connections using TLS + encryption and client/server certificates. It is recommended for production + use, especially when transmitting sensitive data. Works both locally and over + the network. + +- **Unix Domain Sockets (UDS).** This mode allows connections over a local + socket file. UDS is only supported for local inter-process communication + (IPC) on a machine running Linux. + +- **Windows Named User Authentication (WNUA).** This mode allows secure local + connections on Windows machines through user authentication. It is only + supported in Windows. + +- **Insecure.** This mode allows connections without any encryption or + authentication. It is NOT recommended for production use, but can be useful + for testing or development purposes. + + +The ``cyberchannel`` module +============================ + +The ``cyberchannel`` module eases the transition to secure gRPC. It is meant to +be used by client applications to create gRPC channels with the server. + +This module implements all transport modes described above. It also abstracts +away the details of connection setup and certificate handling, making it easier +to connect clients to gRPC servers in different environments. + +Example usage +------------- + +.. code-block:: python + + from cyberchannel import create_channel + import hello_pb2_grpc + + channel = create_channel( + host="localhost", port=50051, # Channel details + transport_mode="mtls", certs_dir="path/to/certs", # Security details + grpc_options=[ # Extra details + ('grpc.max_receive_message_length', 50 * 1024 * 1024) + ], + ) + stub = hello_pb2_grpc.GreeterStub(channel) + +API reference +------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Function + - Description + * - ``create_channel(...)`` + - Main entry point for users. + * - ``verify_transport_mode(...)`` + - Check if selected transport mode is valid. If not, it raises an error. + * - ``verify_uds_socket(...)`` + - Check if UDS socket file exists. + +Environment variables +--------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 60 20 + + * - Variable + - Description + - Default + * - ``ANSYS_GRPC_CERTIFICATES`` + - Path to folder containing ``ca.crt``, ``client.crt``, and ``client.key`` + for mTLS connections. If not set, defaults to a local ``./certs`` + directory. + - ``./certs`` + +Generating certificates for mTLS +================================ + +`OpenSSL `__ can be used to generate the necessary +certificates for mTLS. The following files are required for both the server and +the client: + +===== ======================= ========================================= +Side Required Files Purpose +===== ======================= ========================================= +Server server.crt Server identity + server.key Server private key + ca.crt To verify client certificates +Client client.crt Client identity + client.key Client private key + ca.crt To verify server certificates +===== ======================= ========================================= + +These files can be generated using `OpenSSL `__. + +Generate a certificate authority +-------------------------------- + +.. code-block:: bash + + # Generate private key for CA + openssl genrsa -out ca.key 4096 + + # Generate self-signed CA certificate + openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \ + -subj "/CN=MyRootCA" + +Generate the server certificate +------------------------------- + +.. code-block:: bash + + # Generate server private key + openssl genrsa -out server.key 4096 + + # Generate a certificate signing request (CSR) for the server + openssl req -new -key server.key -out server.csr \ + -subj "/CN=localhost" + + # Generate server certificate signed by the CA + openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -out server.crt -days 365 -sha256 + +Generate the client certificate +------------------------------- + +.. code-block:: bash + + # Generate client private key + openssl genrsa -out client.key 4096 + + # Generate a certificate signing request (CSR) for the client + openssl req -new -key client.key -out client.csr \ + -subj "/CN=grpc-client" + + # Generate client certificate signed by the CA + openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -out client.crt -days 365 -sha256 + +Verify certificates +------------------- + +.. code-block:: bash + + # Verify server certificate + openssl verify -CAfile ca.crt server.crt + + # Verify client certificate + openssl verify -CAfile ca.crt client.crt diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py new file mode 100644 index 00000000..94ee8647 --- /dev/null +++ b/src/ansys/tools/common/cyberchannel.py @@ -0,0 +1,479 @@ +"""Module to create gRPC channels with different transport modes. + +This module provides functions to create gRPC channels based on the specified +transport mode, including insecure, Unix Domain Sockets (UDS), Windows Named User +Authentication (WNUA), and Mutual TLS (mTLS). + +Example +------- + channel = create_channel( + host="localhost", + port=50051, + transport_mode="mtls", + certs_dir="path/to/certs", + grpc_options=[('grpc.max_receive_message_length', 50 * 1024 * 1024)], + ) + stub = hello_pb2_grpc.GreeterStub(channel) + +""" + +# Only the create_channel function is exposed for external use +__all__ = ["create_channel", "verify_transport_mode", "verify_uds_socket"] + +import logging +import os +from dataclasses import dataclass +from pathlib import Path +from typing import cast +from warnings import warn +from typing import TypeGuard + +import grpc + +_IS_WINDOWS = os.name == "nt" +LOOPBACK_HOSTS = ("localhost", "127.0.0.1") + +logger = logging.getLogger(__name__) + +@dataclass +class CertificateFiles: + cert_file: str | Path | None = None + key_file: str | Path | None = None + ca_file: str | Path | None = None + +def create_channel( + transport_mode: str, + host: str | None = None, + port: int | str | None = None, + uds_service: str | None = None, + uds_dir: str | Path | None = None, + uds_id: str | None = None, + certs_dir: str | Path | None = None, + cert_files: CertificateFiles | None = None, + grpc_options: list[tuple[str, object]] | None = None, +) -> grpc.Channel: + """Create a gRPC channel based on the transport mode. + + Parameters + ---------- + transport_mode : str + Transport mode selected by the user. + Options are: "insecure", "uds", "wnua", "mtls" + host : str | None + Hostname or IP address of the server. + By default `None` - however, if not using UDS transport mode, + it will be requested. + port : int | str | None + Port in which the server is running. + By default `None` - however, if not using UDS transport mode, + it will be requested. + uds_service : str | None + Optional service name for the UDS socket. + By default `None` - however, if UDS is selected, it will + be requested. + uds_dir : str | Path | None + Directory to use for Unix Domain Sockets (UDS) transport mode. + By default `None` and thus it will use the "~/.conn" folder. + uds_id : str | None + Optional ID to use for the UDS socket filename. + By default `None` and thus it will use ".sock". + Otherwise, the socket filename will be "-.sock". + certs_dir : str | Path | None + Directory to use for TLS certificates. + By default `None` and thus search for the "ANSYS_GRPC_CERTIFICATES" environment variable. + If not found, it will use the "certs" folder assuming it is in the current working + directory. + cert_files: CertificateFiles | None = None + Path to the client certificate file, client key file, and issuing certificate authority. + By default `None`. + If all three file paths are not all provided, use the certs_dir parameter. + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus no extra options are added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + def check_host_port(transport_mode, host, port) -> tuple[str, str, str]: + if host is None: + raise ValueError(f"When using {transport_mode.lower()} transport mode, 'host' must be provided.") + if port is None: + raise ValueError(f"When using {transport_mode.lower()} transport mode, 'port' must be provided.") + return transport_mode, host, port + + match transport_mode.lower(): + case "insecure": + transport_mode, host, port = check_host_port(transport_mode, host, port) + return create_insecure_channel(host, port, grpc_options) + case "uds": + return create_uds_channel(uds_service, uds_dir, uds_id, grpc_options) + case "wnua": + transport_mode, host, port = check_host_port(transport_mode, host, port) + return create_wnua_channel(host, port, grpc_options) + case "mtls": + transport_mode, host, port = check_host_port(transport_mode, host, port) + return create_mtls_channel(host, port, certs_dir, cert_files, grpc_options) + case _: + raise ValueError( + f"Unknown transport mode: {transport_mode}. " + "Valid options are: 'insecure', 'uds', 'wnua', 'mtls'." + ) + + +##################################### TRANSPORT MODE CHANNELS ##################################### + + +def create_insecure_channel( + host: str, port: int | str, grpc_options: list[tuple[str, object]] | None = None +) -> grpc.Channel: + """Create an insecure gRPC channel without TLS. + + Parameters + ---------- + host : str + Hostname or IP address of the server. + port : int | str + Port in which the server is running. + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus no extra options are added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + target = f"{host}:{port}" + warn( + f"Starting gRPC client without TLS on {target}. This is INSECURE. " + "Consider using a secure connection." + ) + logger.info(f"Connecting using INSECURE -> {target}") + return grpc.insecure_channel(target, options=grpc_options) + + +def create_uds_channel( + uds_service: str | None, + uds_dir: str | Path | None = None, + uds_id: str | None = None, + grpc_options: list[tuple[str, object]] | None = None, +) -> grpc.Channel: + """Create a gRPC channel using Unix Domain Sockets (UDS). + + Parameters + ---------- + uds_service : str + Service name for the UDS socket. + uds_dir : str | Path | None + Directory to use for Unix Domain Sockets (UDS) transport mode. + By default `None` and thus it will use the "~/.conn" folder. + uds_id : str | None + Optional ID to use for the UDS socket filename. + By default `None` and thus it will use ".sock". + Otherwise, the socket filename will be "-.sock". + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus only the default authority option is added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + if not is_uds_supported(): + raise RuntimeError( + "Unix Domain Sockets are not supported on this platform or gRPC version." + ) + + if not uds_service: + raise ValueError("When using UDS transport mode, 'uds_service' must be provided.") + + # Determine UDS folder + uds_folder = determine_uds_folder(uds_dir) + + # Make sure the folder exists + uds_folder.mkdir(parents=True, exist_ok=True) + + # Generate socket filename with optional ID + socket_filename = f"{uds_service}-{uds_id}.sock" if uds_id else f"{uds_service}.sock" + target = f"unix:{uds_folder / socket_filename}" + # Set default authority to "localhost" for UDS connection + # This is needed to avoid issues with some gRPC implementations, + # see https://github.com/grpc/grpc/issues/34305 + options: list[tuple[str, object]] = [ + ("grpc.default_authority", "localhost"), + ] + if grpc_options: + options.extend(grpc_options) + logger.info(f"Connecting using UDS -> {target}") + return grpc.insecure_channel(target, options=options) + + +def create_wnua_channel( + host: str, + port: int | str, + grpc_options: list[tuple[str, object]] | None = None, +) -> grpc.Channel: + """Create a gRPC channel using Windows Named User Authentication (WNUA). + + Parameters + ---------- + host : str + Hostname or IP address of the server. + port : int | str + Port in which the server is running. + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus only the default authority option is added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + if not _IS_WINDOWS: + raise ValueError("Windows Named User Authentication (WNUA) is only supported on Windows.") + if host not in LOOPBACK_HOSTS: + raise ValueError("Remote host connections are not supported with WNUA.") + + target = f"{host}:{port}" + # Set default authority to "localhost" for WNUA connection + # This is needed to avoid issues with some gRPC implementations, + # see https://github.com/grpc/grpc/issues/34305 + options: list[tuple[str, object]] = [ + ("grpc.default_authority", "localhost"), + ] + if grpc_options: + options.extend(grpc_options) + logger.info(f"Connecting using WNUA -> {target}") + return grpc.insecure_channel(target, options=options) + + +def create_mtls_channel( + host: str, + port: int | str, + certs_dir: str | Path | None = None, + cert_files: CertificateFiles | None = None, + grpc_options: list[tuple[str, object]] | None = None, +) -> grpc.Channel: + """Create a gRPC channel using Mutual TLS (mTLS). + + Parameters + ---------- + host : str + Hostname or IP address of the server. + port : int | str + Port in which the server is running. + certs_dir : str | Path | None + Directory to use for TLS certificates. + By default `None` and thus search for the "ANSYS_GRPC_CERTIFICATES" environment variable. + If not found, it will use the "certs" folder assuming it is in the current working + directory. + cert_files: CertificateFiles | None + Path to the client certificate file, client key file, and issuing certificate authority. + By default `None`. + If all three file paths are not all provided, use the certs_dir parameter. + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus no extra options are added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + certs_folder = None + if cert_files is not None and cert_files.cert_file is not None and cert_files.key_file is not None and cert_files.ca_file is not None: + cert_file = Path(cert_files.cert_file).resolve() + key_file = Path(cert_files.key_file).resolve() + ca_file = Path(cert_files.ca_file).resolve() + else: + # Determine certificates folder + if certs_dir: + certs_folder = Path(certs_dir) + elif os.environ.get("ANSYS_GRPC_CERTIFICATES"): + certs_folder = Path(cast(str, os.environ.get("ANSYS_GRPC_CERTIFICATES"))) + else: + certs_folder = Path("certs") + ca_file = certs_folder / "ca.crt" + cert_file = certs_folder / "client.crt" + key_file = certs_folder / "client.key" + + # Load certificates + try: + with (ca_file).open("rb") as f: + trusted_certs = f.read() + with (cert_file).open("rb") as f: + client_cert = f.read() + with (key_file).open("rb") as f: + client_key = f.read() + except FileNotFoundError as e: + error_message = f"Certificate file not found: {e.filename}. " + if certs_folder is not None: + error_message += f"Ensure that the certificates are present in the '{certs_folder}' folder or " \ + "set the 'ANSYS_GRPC_CERTIFICATES' environment variable." + raise FileNotFoundError(error_message) from e + + # Create SSL credentials + credentials = grpc.ssl_channel_credentials( + root_certificates=trusted_certs, private_key=client_key, certificate_chain=client_cert + ) + + target = f"{host}:{port}" + logger.info(f"Connecting using mTLS -> {target}") + return grpc.secure_channel(target, credentials, options=grpc_options) + + +######################################## HELPER FUNCTIONS ######################################## + + +def version_tuple(version_str: str) -> tuple[int, ...]: + """Convert a version string into a tuple of integers for comparison. + + Parameters + ---------- + version_str : str + The version string to convert. + + Returns + ------- + tuple[int, ...] + A tuple of integers representing the version. + + """ + return tuple(int(x) for x in version_str.split(".")) + + +def check_grpc_version(): + """Check if the installed gRPC version meets the minimum requirement. + + Returns + ------- + bool + True if the gRPC version is sufficient, False otherwise. + + """ + min_version = "1.63.0" + current_version = grpc.__version__ + + try: + return version_tuple(current_version) >= version_tuple(min_version) + except ValueError: + logger.warning("Unable to parse gRPC version.") + return False + + +def is_uds_supported(): + """Check if Unix Domain Sockets (UDS) are supported on the current platform. + + Returns + ------- + bool + True if UDS is supported, False otherwise. + + """ + is_grpc_version_ok = check_grpc_version() + return is_grpc_version_ok if _IS_WINDOWS else True + + +def determine_uds_folder(uds_dir: str | Path | None = None) -> Path: + """Determine the directory to use for Unix Domain Sockets (UDS). + + Parameters + ---------- + uds_dir : str | Path | None + Directory to use for Unix Domain Sockets (UDS) transport mode. + By default `None` and thus it will use the "~/.conn" folder. + + Returns + ------- + Path + The path to the UDS directory. + + """ + # If no directory is provided, use default based on OS + if uds_dir: + return uds_dir if isinstance(uds_dir, Path) else Path(uds_dir) + else: + if _IS_WINDOWS: + return Path(os.environ["USERPROFILE"]) / ".conn" + else: + # Linux/POSIX + return Path(os.environ["HOME"], ".conn") + + +def verify_transport_mode(transport_mode: str, mode: str | None = None) -> None: + """Verify that the provided transport mode is valid. + + Parameters + ---------- + transport_mode : str + The transport mode to verify. + mode : str | None + Can be one of "all", "local" or "remote" to restrict the valid transport modes. + By default `None` and thus all transport modes are accepted. + + Raises + ------ + ValueError + If the transport mode is not one of the accepted values. + + """ + if mode == "local": + valid_modes = {"insecure", "uds", "wnua"} + elif mode == "remote": + valid_modes = {"insecure", "mtls"} + elif mode == "all" or mode is None: + valid_modes = {"insecure", "uds", "wnua", "mtls"} + else: + raise ValueError(f"Invalid mode: {mode}. Valid options are: 'all', 'local', 'remote'.") + + if transport_mode.lower() not in valid_modes: + raise ValueError( + f"Invalid transport mode: {transport_mode}. " + f"Valid options are: {', '.join(valid_modes)}." + ) + + +def verify_uds_socket( + uds_service: str, uds_dir: Path | None = None, uds_id: str | None = None +) -> bool: + """Verify that the UDS socket file has been created. + + Parameters + ---------- + uds_service : str + Service name for the UDS socket. + uds_dir : Path | None + Directory where the UDS socket file is expected to be (optional). + By default `None` and thus it will use the "~/.conn" folder. + uds_id : str | None + Unique identifier for the UDS socket (optional). + By default `None` and thus it will use ".sock". + Otherwise, the socket filename will be "-.sock". + + Returns + ------- + bool + True if the UDS socket file exists, False otherwise. + """ + # Generate socket filename with optional ID + uds_filename = f"{uds_service}-{uds_id}.sock" if uds_id else f"{uds_service}.sock" + + # Full path to the UDS socket file + uds_socket_path = determine_uds_folder(uds_dir) / uds_filename + + # Check if the UDS socket file exists + return uds_socket_path.exists() From 64bf7c9a0f0a80c99f7928cafa7d5230136265cf Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:58:52 +0100 Subject: [PATCH 02/12] fix: pre-commit --- doc/source/user_guide/secure_grpc.rst | 10 +++-- src/ansys/tools/common/cyberchannel.py | 62 +++++++++++++++++--------- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst index e723abbe..2623cd15 100644 --- a/doc/source/user_guide/secure_grpc.rst +++ b/doc/source/user_guide/secure_grpc.rst @@ -46,10 +46,12 @@ Example usage import hello_pb2_grpc channel = create_channel( - host="localhost", port=50051, # Channel details - transport_mode="mtls", certs_dir="path/to/certs", # Security details - grpc_options=[ # Extra details - ('grpc.max_receive_message_length', 50 * 1024 * 1024) + host="localhost", + port=50051, # Channel details + transport_mode="mtls", + certs_dir="path/to/certs", # Security details + grpc_options=[ # Extra details + ("grpc.max_receive_message_length", 50 * 1024 * 1024) ], ) stub = hello_pb2_grpc.GreeterStub(channel) diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py index 94ee8647..0e02a939 100644 --- a/src/ansys/tools/common/cyberchannel.py +++ b/src/ansys/tools/common/cyberchannel.py @@ -1,3 +1,25 @@ +# Copyright (C) 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + """Module to create gRPC channels with different transport modes. This module provides functions to create gRPC channels based on the specified @@ -20,13 +42,12 @@ # Only the create_channel function is exposed for external use __all__ = ["create_channel", "verify_transport_mode", "verify_uds_socket"] +from dataclasses import dataclass import logging import os -from dataclasses import dataclass from pathlib import Path from typing import cast from warnings import warn -from typing import TypeGuard import grpc @@ -35,12 +56,14 @@ logger = logging.getLogger(__name__) + @dataclass class CertificateFiles: cert_file: str | Path | None = None key_file: str | Path | None = None ca_file: str | Path | None = None + def create_channel( transport_mode: str, host: str | None = None, @@ -98,6 +121,7 @@ def create_channel( The created gRPC channel """ + def check_host_port(transport_mode, host, port) -> tuple[str, str, str]: if host is None: raise ValueError(f"When using {transport_mode.lower()} transport mode, 'host' must be provided.") @@ -119,8 +143,7 @@ def check_host_port(transport_mode, host, port) -> tuple[str, str, str]: return create_mtls_channel(host, port, certs_dir, cert_files, grpc_options) case _: raise ValueError( - f"Unknown transport mode: {transport_mode}. " - "Valid options are: 'insecure', 'uds', 'wnua', 'mtls'." + f"Unknown transport mode: {transport_mode}. Valid options are: 'insecure', 'uds', 'wnua', 'mtls'." ) @@ -150,10 +173,7 @@ def create_insecure_channel( """ target = f"{host}:{port}" - warn( - f"Starting gRPC client without TLS on {target}. This is INSECURE. " - "Consider using a secure connection." - ) + warn(f"Starting gRPC client without TLS on {target}. This is INSECURE. Consider using a secure connection.") logger.info(f"Connecting using INSECURE -> {target}") return grpc.insecure_channel(target, options=grpc_options) @@ -189,9 +209,7 @@ def create_uds_channel( """ if not is_uds_supported(): - raise RuntimeError( - "Unix Domain Sockets are not supported on this platform or gRPC version." - ) + raise RuntimeError("Unix Domain Sockets are not supported on this platform or gRPC version.") if not uds_service: raise ValueError("When using UDS transport mode, 'uds_service' must be provided.") @@ -295,7 +313,12 @@ def create_mtls_channel( """ certs_folder = None - if cert_files is not None and cert_files.cert_file is not None and cert_files.key_file is not None and cert_files.ca_file is not None: + if ( + cert_files is not None + and cert_files.cert_file is not None + and cert_files.key_file is not None + and cert_files.ca_file is not None + ): cert_file = Path(cert_files.cert_file).resolve() key_file = Path(cert_files.key_file).resolve() ca_file = Path(cert_files.ca_file).resolve() @@ -322,8 +345,10 @@ def create_mtls_channel( except FileNotFoundError as e: error_message = f"Certificate file not found: {e.filename}. " if certs_folder is not None: - error_message += f"Ensure that the certificates are present in the '{certs_folder}' folder or " \ - "set the 'ANSYS_GRPC_CERTIFICATES' environment variable." + error_message += ( + f"Ensure that the certificates are present in the '{certs_folder}' folder or " + "set the 'ANSYS_GRPC_CERTIFICATES' environment variable." + ) raise FileNotFoundError(error_message) from e # Create SSL credentials @@ -441,15 +466,10 @@ def verify_transport_mode(transport_mode: str, mode: str | None = None) -> None: raise ValueError(f"Invalid mode: {mode}. Valid options are: 'all', 'local', 'remote'.") if transport_mode.lower() not in valid_modes: - raise ValueError( - f"Invalid transport mode: {transport_mode}. " - f"Valid options are: {', '.join(valid_modes)}." - ) + raise ValueError(f"Invalid transport mode: {transport_mode}. Valid options are: {', '.join(valid_modes)}.") -def verify_uds_socket( - uds_service: str, uds_dir: Path | None = None, uds_id: str | None = None -) -> bool: +def verify_uds_socket(uds_service: str, uds_dir: Path | None = None, uds_id: str | None = None) -> bool: """Verify that the UDS socket file has been created. Parameters From c42d8ce18fb8e1d2b85bf9bffcb13629aea0d6c7 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:04:59 +0100 Subject: [PATCH 03/12] fix: changes --- doc/source/user_guide/secure_grpc.rst | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst index 2623cd15..be326431 100644 --- a/doc/source/user_guide/secure_grpc.rst +++ b/doc/source/user_guide/secure_grpc.rst @@ -1,18 +1,20 @@ Securing gRPC connections ######################### -The PyAnsys ecosystem enables now various transport modes for securing gRPC connections. -This page reviews the available transport modes, and how to use them. +With the release of Ansys product service packs adding enhanced security to +gRPC communication, the PyAnsys ecosystem enables various transport modes for +securing gRPC connections. This page reviews the available transport modes, +and how to use them. Supported transport modes ========================= PyAnsys supports the following transport modes for gRPC connections: -- **Mutual TLS (mTLS).** This mode allows secure connections using TLS - encryption and client/server certificates. It is recommended for production - use, especially when transmitting sensitive data. Works both locally and over - the network. +- **Mutual TLS (mTLS).** This mode, which works both locally and over the + network, allows secure connections using TLS encryption and client/server + certificates. It is recommended for production use, especially when + transmitting sensitive data. - **Unix Domain Sockets (UDS).** This mode allows connections over a local socket file. UDS is only supported for local inter-process communication @@ -33,9 +35,9 @@ The ``cyberchannel`` module The ``cyberchannel`` module eases the transition to secure gRPC. It is meant to be used by client applications to create gRPC channels with the server. -This module implements all transport modes described above. It also abstracts -away the details of connection setup and certificate handling, making it easier -to connect clients to gRPC servers in different environments. +This module implements all transport modes described previously. It also +abstracts away the details of connection setup and certificate handling, making +it easier to connect clients to gRPC servers in different environments. Example usage ------------- @@ -117,7 +119,7 @@ Generate a certificate authority openssl genrsa -out ca.key 4096 # Generate self-signed CA certificate - openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \ + openssl req -x509 -new -nodes -key ca.key -sha256 -days 200 -out ca.crt \ -subj "/CN=MyRootCA" Generate the server certificate @@ -134,7 +136,7 @@ Generate the server certificate # Generate server certificate signed by the CA openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out server.crt -days 365 -sha256 + -out server.crt -days 200 -sha256 Generate the client certificate ------------------------------- @@ -150,7 +152,7 @@ Generate the client certificate # Generate client certificate signed by the CA openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out client.crt -days 365 -sha256 + -out client.crt -days 200 -sha256 Verify certificates ------------------- From b1ff5168c065f0db9685d53068ff8c68fe3855cf Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:56:31 +0000 Subject: [PATCH 04/12] chore: adding changelog file 107.miscellaneous.md [dependabot-skip] --- doc/source/changelog/107.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/source/changelog/107.miscellaneous.md diff --git a/doc/source/changelog/107.miscellaneous.md b/doc/source/changelog/107.miscellaneous.md new file mode 100644 index 00000000..40d31e6d --- /dev/null +++ b/doc/source/changelog/107.miscellaneous.md @@ -0,0 +1 @@ +Feat: adding cyberchannel module From 174ccf274b3e1ec356a57e32fe5386f7fd8e1a00 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:58:52 +0100 Subject: [PATCH 05/12] fix: safe wrap grpc import --- src/ansys/tools/common/cyberchannel.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py index 0e02a939..a5ce1b35 100644 --- a/src/ansys/tools/common/cyberchannel.py +++ b/src/ansys/tools/common/cyberchannel.py @@ -49,7 +49,11 @@ from typing import cast from warnings import warn -import grpc +try: + import grpc +except ImportError: # pragma: no cover + warn("grpc module is not available - reach out to the library maintainers to include it into their dependencies") + _IS_WINDOWS = os.name == "nt" LOOPBACK_HOSTS = ("localhost", "127.0.0.1") From e27ad122387ea87978dc65d982769f6f8f2675ed Mon Sep 17 00:00:00 2001 From: Jorge Martinez Date: Mon, 24 Nov 2025 18:05:26 +0100 Subject: [PATCH 06/12] fix(doc): talbe formatting --- doc/source/user_guide/secure_grpc.rst | 33 ++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst index be326431..e9772751 100644 --- a/doc/source/user_guide/secure_grpc.rst +++ b/doc/source/user_guide/secure_grpc.rst @@ -94,19 +94,26 @@ Generating certificates for mTLS ================================ `OpenSSL `__ can be used to generate the necessary -certificates for mTLS. The following files are required for both the server and -the client: - -===== ======================= ========================================= -Side Required Files Purpose -===== ======================= ========================================= -Server server.crt Server identity - server.key Server private key - ca.crt To verify client certificates -Client client.crt Client identity - client.key Client private key - ca.crt To verify server certificates -===== ======================= ========================================= +certificates for mTLS. + +Certificate files used by the server: + +======================= ========================================= +Required Files Purpose +======================= ========================================= +server.crt Server identity +server.key Server private key +ca.crt To verify client certificates + +Certificate files used by the client: + +======================= ========================================= +Required Files Purpose +======================= ========================================= +client.crt Client identity +client.key Client private key +ca.crt To verify server certificates +======================= ========================================= These files can be generated using `OpenSSL `__. From c66149ba6f79332a86624d7984e38b5fda87340c Mon Sep 17 00:00:00 2001 From: Jorge Martinez Date: Mon, 24 Nov 2025 18:12:53 +0100 Subject: [PATCH 07/12] fix(doc): talbe formatting --- doc/source/user_guide/secure_grpc.rst | 42 +++++++++++++++------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst index e9772751..0c567545 100644 --- a/doc/source/user_guide/secure_grpc.rst +++ b/doc/source/user_guide/secure_grpc.rst @@ -96,24 +96,30 @@ Generating certificates for mTLS `OpenSSL `__ can be used to generate the necessary certificates for mTLS. -Certificate files used by the server: - -======================= ========================================= -Required Files Purpose -======================= ========================================= -server.crt Server identity -server.key Server private key -ca.crt To verify client certificates - -Certificate files used by the client: - -======================= ========================================= -Required Files Purpose -======================= ========================================= -client.crt Client identity -client.key Client private key -ca.crt To verify server certificates -======================= ========================================= +.. list-table:: Server certificate files + :widths: auto + :header-rows: 1 + + * - Required Files + - Purpose + * - server.crt + - Server identity + * - server.key + - Server private key + * - ca.crt + - To verify client certificates + +.. list-table:: Client certificate files + :widths: auto + :header-rows: 1 + * - Required Files + - Purpose + * - client.crt + - Client identity + * - client.key + - Client private key + * - ca.crt + - To verify server certificates These files can be generated using `OpenSSL `__. From 688bfa4cc649e88cdd46ad7f98ba6413f84801ff Mon Sep 17 00:00:00 2001 From: Jorge Martinez Date: Mon, 24 Nov 2025 18:14:19 +0100 Subject: [PATCH 08/12] fix(doc): talbe formatting --- doc/source/user_guide/secure_grpc.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst index 0c567545..45f1a89a 100644 --- a/doc/source/user_guide/secure_grpc.rst +++ b/doc/source/user_guide/secure_grpc.rst @@ -112,6 +112,7 @@ certificates for mTLS. .. list-table:: Client certificate files :widths: auto :header-rows: 1 + * - Required Files - Purpose * - client.crt From 7a96769d851366fee49ca86c620aa4fe1319c1a3 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:55:28 +0100 Subject: [PATCH 09/12] docs: fix table rendering --- doc/source/user_guide/secure_grpc.rst | 42 +++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst index 45f1a89a..da507533 100644 --- a/doc/source/user_guide/secure_grpc.rst +++ b/doc/source/user_guide/secure_grpc.rst @@ -97,30 +97,30 @@ Generating certificates for mTLS certificates for mTLS. .. list-table:: Server certificate files - :widths: auto - :header-rows: 1 + :header-rows: 1 + :widths: auto - * - Required Files - - Purpose - * - server.crt - - Server identity - * - server.key - - Server private key - * - ca.crt - - To verify client certificates + * - Required Files + - Purpose + * - server.crt + - Server identity + * - server.key + - Server private key + * - ca.crt + - To verify client certificates .. list-table:: Client certificate files - :widths: auto - :header-rows: 1 - - * - Required Files - - Purpose - * - client.crt - - Client identity - * - client.key - - Client private key - * - ca.crt - - To verify server certificates + :header-rows: 1 + :widths: auto + + * - Required Files + - Purpose + * - client.crt + - Client identity + * - client.key + - Client private key + * - ca.crt + - To verify server certificates These files can be generated using `OpenSSL `__. From fdc83fece65831f37bc8563425243be84885889e Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:56:13 +0100 Subject: [PATCH 10/12] docs: fix table rendering --- doc/source/user_guide/secure_grpc.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst index da507533..2bc3a792 100644 --- a/doc/source/user_guide/secure_grpc.rst +++ b/doc/source/user_guide/secure_grpc.rst @@ -78,17 +78,17 @@ Environment variables --------------------- .. list-table:: - :header-rows: 1 - :widths: 20 60 20 - - * - Variable - - Description - - Default - * - ``ANSYS_GRPC_CERTIFICATES`` - - Path to folder containing ``ca.crt``, ``client.crt``, and ``client.key`` - for mTLS connections. If not set, defaults to a local ``./certs`` - directory. - - ``./certs`` + :header-rows: 1 + :widths: 20 60 20 + + * - Variable + - Description + - Default + * - ``ANSYS_GRPC_CERTIFICATES`` + - Path to folder containing ``ca.crt``, ``client.crt``, and ``client.key`` + for mTLS connections. If not set, defaults to a local ``./certs`` + directory. + - ``./certs`` Generating certificates for mTLS ================================ From eed192b0a127b78692907f6a81e47bccbdc9c1bf Mon Sep 17 00:00:00 2001 From: Jorge Martinez Date: Tue, 25 Nov 2025 08:51:01 +0100 Subject: [PATCH 11/12] fix(doc): code directive --- src/ansys/tools/common/cyberchannel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py index a5ce1b35..71ad2f7f 100644 --- a/src/ansys/tools/common/cyberchannel.py +++ b/src/ansys/tools/common/cyberchannel.py @@ -28,6 +28,9 @@ Example ------- + +.. code-block:: python + channel = create_channel( host="localhost", port=50051, From 6d586f1b318f29c9a13408b3f7e606396e3ef19e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 07:52:29 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/ansys/tools/common/cyberchannel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py index 71ad2f7f..06d6a27c 100644 --- a/src/ansys/tools/common/cyberchannel.py +++ b/src/ansys/tools/common/cyberchannel.py @@ -36,7 +36,7 @@ port=50051, transport_mode="mtls", certs_dir="path/to/certs", - grpc_options=[('grpc.max_receive_message_length', 50 * 1024 * 1024)], + grpc_options=[("grpc.max_receive_message_length", 50 * 1024 * 1024)], ) stub = hello_pb2_grpc.GreeterStub(channel)