Source code for codeflare_sdk.common.utils.generate_cert

# Copyright 2022 IBM, Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
import os
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509
from cryptography.x509.oid import NameOID
import datetime
from ..kubernetes_cluster.auth import (
    config_check,
    get_api_client,
)
from kubernetes import client
from .. import _kube_api_error_handling


[docs] def generate_ca_cert(days: int = 30): """ Generates a self-signed CA certificate and private key, encoded in base64 format. Similar to: openssl req -x509 -nodes -newkey rsa:2048 -keyout ca.key -days 1826 -out ca.crt -subj '/CN=root-ca' Args: days (int): The number of days for which the CA certificate will be valid. Default is 30. Returns: Tuple[str, str]: A tuple containing the base64-encoded private key and CA certificate. """ private_key = rsa.generate_private_key( public_exponent=65537, key_size=2048, ) key = base64.b64encode( private_key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) ).decode("utf-8") # Generate Certificate one_day = datetime.timedelta(1, 0, 0) public_key = private_key.public_key() builder = ( x509.CertificateBuilder() .subject_name( x509.Name( [ x509.NameAttribute(NameOID.COMMON_NAME, "root-ca"), ] ) ) .issuer_name( x509.Name( [ x509.NameAttribute(NameOID.COMMON_NAME, "root-ca"), ] ) ) .not_valid_before(datetime.datetime.today() - one_day) .not_valid_after(datetime.datetime.today() + (one_day * days)) .serial_number(x509.random_serial_number()) .public_key(public_key) ) certificate = base64.b64encode( builder.sign(private_key=private_key, algorithm=hashes.SHA256()).public_bytes( serialization.Encoding.PEM ) ).decode("utf-8") return key, certificate
[docs] def get_secret_name(cluster_name, namespace, api_instance): """ Retrieves the name of the Kubernetes secret containing the CA certificate for the given Ray cluster. Args: cluster_name (str): The name of the Ray cluster. namespace (str): The Kubernetes namespace where the Ray cluster is located. api_instance (client.CoreV1Api): An instance of the Kubernetes CoreV1Api. Returns: str: The name of the Kubernetes secret containing the CA certificate. Raises: KeyError: If no secret matching the cluster name is found. """ label_selector = f"ray.openshift.ai/cluster-name={cluster_name}" try: secrets = api_instance.list_namespaced_secret( namespace, label_selector=label_selector ) for secret in secrets.items: if ( f"{cluster_name}-ca-secret-" in secret.metadata.name ): # Oauth secret share the same label this conditional is to make things more specific return secret.metadata.name else: continue raise KeyError(f"Unable to gather secret name for {cluster_name}") except Exception as e: # pragma: no cover return _kube_api_error_handling(e)
[docs] def generate_tls_cert(cluster_name, namespace, days=30): """ Generates a TLS certificate and key for a Ray cluster, saving them locally along with the CA certificate. Args: cluster_name (str): The name of the Ray cluster. namespace (str): The Kubernetes namespace where the Ray cluster is located. days (int): The number of days for which the TLS certificate will be valid. Default is 30. Files Created: - ca.crt: The CA certificate. - tls.crt: The TLS certificate signed by the CA. - tls.key: The private key for the TLS certificate. Raises: Exception: If an error occurs while retrieving the CA secret. """ tls_dir = os.path.join(os.getcwd(), f"tls-{cluster_name}-{namespace}") if not os.path.exists(tls_dir): os.makedirs(tls_dir) # Similar to: # oc get secret ca-secret-<cluster-name> -o template='{{index .data "ca.key"}}' # oc get secret ca-secret-<cluster-name> -o template='{{index .data "ca.crt"}}'|base64 -d > ${TLSDIR}/ca.crt config_check() v1 = client.CoreV1Api(get_api_client()) # Secrets have a suffix appended to the end so we must list them and gather the secret that includes cluster_name-ca-secret- secret_name = get_secret_name(cluster_name, namespace, v1) secret = v1.read_namespaced_secret(secret_name, namespace).data ca_cert = secret.get("ca.crt") ca_key = secret.get("ca.key") with open(os.path.join(tls_dir, "ca.crt"), "w") as f: f.write(base64.b64decode(ca_cert).decode("utf-8")) # Generate tls.key and signed tls.cert locally for ray client # Similar to running these commands: # openssl req -nodes -newkey rsa:2048 -keyout ${TLSDIR}/tls.key -out ${TLSDIR}/tls.csr -subj '/CN=local' # cat <<EOF >${TLSDIR}/domain.ext # authorityKeyIdentifier=keyid,issuer # basicConstraints=CA:FALSE # subjectAltName = @alt_names # [alt_names] # DNS.1 = 127.0.0.1 # DNS.2 = localhost # EOF # openssl x509 -req -CA ${TLSDIR}/ca.crt -CAkey ${TLSDIR}/ca.key -in ${TLSDIR}/tls.csr -out ${TLSDIR}/tls.crt -days 365 -CAcreateserial -extfile ${TLSDIR}/domain.ext key = rsa.generate_private_key( public_exponent=65537, key_size=2048, ) tls_key = key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) with open(os.path.join(tls_dir, "tls.key"), "w") as f: f.write(tls_key.decode("utf-8")) one_day = datetime.timedelta(1, 0, 0) tls_cert = ( x509.CertificateBuilder() .issuer_name( x509.Name( [ x509.NameAttribute(NameOID.COMMON_NAME, "root-ca"), ] ) ) .subject_name( x509.Name( [ x509.NameAttribute(NameOID.COMMON_NAME, "local"), ] ) ) .public_key(key.public_key()) .not_valid_before(datetime.datetime.today() - one_day) .not_valid_after(datetime.datetime.today() + (one_day * days)) .serial_number(x509.random_serial_number()) .add_extension( x509.SubjectAlternativeName( [x509.DNSName("localhost"), x509.DNSName("127.0.0.1")] ), False, ) .sign( serialization.load_pem_private_key(base64.b64decode(ca_key), None), hashes.SHA256(), ) ) with open(os.path.join(tls_dir, "tls.crt"), "w") as f: f.write(tls_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8"))
[docs] def export_env(cluster_name, namespace): """ Sets environment variables to configure TLS for a Ray cluster. Args: cluster_name (str): The name of the Ray cluster. namespace (str): The Kubernetes namespace where the Ray cluster is located. Environment Variables Set: - RAY_USE_TLS: Enables TLS for Ray. - RAY_TLS_SERVER_CERT: Path to the TLS server certificate. - RAY_TLS_SERVER_KEY: Path to the TLS server private key. - RAY_TLS_CA_CERT: Path to the CA certificate. """ tls_dir = os.path.join(os.getcwd(), f"tls-{cluster_name}-{namespace}") os.environ["RAY_USE_TLS"] = "1" os.environ["RAY_TLS_SERVER_CERT"] = os.path.join(tls_dir, "tls.crt") os.environ["RAY_TLS_SERVER_KEY"] = os.path.join(tls_dir, "tls.key") os.environ["RAY_TLS_CA_CERT"] = os.path.join(tls_dir, "ca.crt")