]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/cephadm: make agent certs compatible with OpenSSL hostname and ip checks
authorAdam King <adking@redhat.com>
Thu, 9 Sep 2021 17:36:40 +0000 (13:36 -0400)
committerAdam King <adking@redhat.com>
Fri, 24 Sep 2021 11:23:51 +0000 (07:23 -0400)
Signed-off-by: Adam King <adking@redhat.com>
admin/doc-requirements.txt
src/cephadm/cephadm
src/pybind/mgr/cephadm/agent.py
src/pybind/mgr/cephadm/module.py
src/pybind/mgr/cephadm/services/cephadmservice.py
src/pybind/mgr/requirements-required.txt

index 1570b52438c92707fa4d30a31fe3590f090789d3..dd5994d51ae5c7d37ff7c97d42158989e0acc18c 100644 (file)
@@ -2,6 +2,7 @@ Sphinx == 3.5.4
 git+https://github.com/ceph/sphinx-ditaa.git@py3#egg=sphinx-ditaa
 git+https://github.com/vlasovskikh/funcparserlib.git
 breathe >= 4.20.0
+cryptography
 Jinja2
 pyyaml >= 5.1.2
 Cython
index 6aa0bd0cd335134bfd0adb3c81315ab5786b0ada..aabade527e471383e7c8bc6ba21a8cc805b04b2d 100755 (executable)
@@ -3481,30 +3481,33 @@ class MgrListener(Thread):
         secureListenSocket = ssl_ctx.wrap_socket(listenSocket, server_side=True)
         while not self.stop:
             try:
-                conn, _ = secureListenSocket.accept()
-            except socket.timeout:
-                continue
-            try:
-                length: int = int(conn.recv(10).decode())
-            except Exception as e:
-                err_str = f'Failed to extract length of payload from message: {e}'
-                conn.send(err_str.encode())
-                logger.error(err_str)
-            while True:
-                payload = conn.recv(length).decode()
-                if not payload:
-                    break
                 try:
-                    data: Dict[Any, Any] = json.loads(payload)
-                    self.handle_json_payload(data)
+                    conn, _ = secureListenSocket.accept()
+                except socket.timeout:
+                    continue
+                try:
+                    length: int = int(conn.recv(10).decode())
                 except Exception as e:
-                    err_str = f'Failed to extract json payload from message: {e}'
+                    err_str = f'Failed to extract length of payload from message: {e}'
                     conn.send(err_str.encode())
                     logger.error(err_str)
-                else:
-                    conn.send(b'ACK')
-                    self.agent.wakeup()
-                    logger.debug(f'Got mgr message {data}')
+                while True:
+                    payload = conn.recv(length).decode()
+                    if not payload:
+                        break
+                    try:
+                        data: Dict[Any, Any] = json.loads(payload)
+                        self.handle_json_payload(data)
+                    except Exception as e:
+                        err_str = f'Failed to extract json payload from message: {e}'
+                        conn.send(err_str.encode())
+                        logger.error(err_str)
+                    else:
+                        conn.send(b'ACK')
+                        self.agent.wakeup()
+                        logger.debug(f'Got mgr message {data}')
+            except Exception as e:
+                logger.error(f'Mgr Listener encountered exception: {e}')
 
     def shutdown(self) -> None:
         self.stop = True
@@ -3645,8 +3648,8 @@ WantedBy=ceph-{fsid}.target
             self.mgr_listener.start()
 
         ssl_ctx = ssl.create_default_context()
-        ssl_ctx.verify_mode = ssl.CERT_REQUIRED
         ssl_ctx.check_hostname = True
+        ssl_ctx.verify_mode = ssl.CERT_REQUIRED
         ssl_ctx.load_verify_locations(self.ca_path)
 
         while not self.stop:
index 3a8028a4d0851ae8f6d899510a53434652334d55..19a1185db9e3707562008b2ae2169c1efa135777 100644 (file)
@@ -1,4 +1,5 @@
 import cherrypy
+import ipaddress
 import json
 import socket
 import ssl
@@ -6,14 +7,21 @@ import tempfile
 import threading
 import time
 
+from orchestrator import OrchestratorError
 from mgr_util import verify_tls_files
 from ceph.utils import datetime_now
 from ceph.deployment.inventory import Devices
 from ceph.deployment.service_spec import ServiceSpec, PlacementSpec
 
+from datetime import datetime, timedelta
 from OpenSSL import crypto
+from cryptography import x509
+from cryptography.x509.oid import NameOID
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.backends import default_backend
+
 from typing import Any, Dict, Set, Tuple, TYPE_CHECKING
-from uuid import uuid4
 
 if TYPE_CHECKING:
     from cephadm.module import CephadmOrchestrator
@@ -191,6 +199,7 @@ class AgentMessageThread(threading.Thread):
     def __init__(self, host: str, port: int, data: Dict[Any, Any], mgr: "CephadmOrchestrator") -> None:
         self.mgr = mgr
         self.host = host
+        self.addr = self.mgr.inventory.get_addr(host)
         self.port = port
         self.data: str = json.dumps(data)
         super(AgentMessageThread, self).__init__(target=self.run)
@@ -236,8 +245,8 @@ class AgentMessageThread(threading.Thread):
         for retry_wait in [3, 5]:
             try:
                 agent_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-                secure_agent_socket = ssl_ctx.wrap_socket(agent_socket, server_hostname=self.host)
-                secure_agent_socket.connect((self.mgr.inventory.get_addr(self.host), self.port))
+                secure_agent_socket = ssl_ctx.wrap_socket(agent_socket, server_hostname=self.addr)
+                secure_agent_socket.connect((self.addr, self.port))
                 msg = (bytes_len + self.data)
                 secure_agent_socket.sendall(msg.encode('utf-8'))
                 agent_response = secure_agent_socket.recv(1024).decode()
@@ -306,47 +315,95 @@ class SSLCerts:
         self.root_subj: Any
 
     def generate_root_cert(self) -> Tuple[str, str]:
-        self.root_key = crypto.PKey()
-        self.root_key.generate_key(crypto.TYPE_RSA, 2048)
-
-        self.root_cert = crypto.X509()
-        self.root_cert.set_serial_number(int(uuid4()))
-
-        self.root_subj = self.root_cert.get_subject()
-        self.root_subj.commonName = "cephadm-root"
+        self.root_key = rsa.generate_private_key(
+            public_exponent=65537, key_size=4096, backend=default_backend())
+        root_public_key = self.root_key.public_key()
+
+        root_builder = x509.CertificateBuilder()
+
+        root_builder = root_builder.subject_name(x509.Name([
+            x509.NameAttribute(NameOID.COMMON_NAME, u'cephadm-root'),
+        ]))
+
+        root_builder = root_builder.issuer_name(x509.Name([
+            x509.NameAttribute(NameOID.COMMON_NAME, u'cephadm-root'),
+        ]))
+
+        root_builder = root_builder.not_valid_before(datetime.now())
+        root_builder = root_builder.not_valid_after(datetime.now() + timedelta(days=(365 * 10 + 3)))
+        root_builder = root_builder.serial_number(x509.random_serial_number())
+        root_builder = root_builder.public_key(root_public_key)
+        root_builder = root_builder.add_extension(
+            x509.SubjectAlternativeName(
+                [x509.IPAddress(ipaddress.IPv4Address(str(self.mgr.get_mgr_ip())))]
+            ),
+            critical=False
+        )
+        root_builder = root_builder.add_extension(
+            x509.BasicConstraints(ca=True, path_length=None), critical=True,
+        )
 
-        self.root_cert.set_issuer(self.root_subj)
-        self.root_cert.set_pubkey(self.root_key)
-        self.root_cert.gmtime_adj_notBefore(0)
-        self.root_cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)  # 10 years
-        self.root_cert.sign(self.root_key, 'sha256')
+        self.root_cert = root_builder.sign(
+            private_key=self.root_key, algorithm=hashes.SHA256(), backend=default_backend()
+        )
 
         cert_str = crypto.dump_certificate(crypto.FILETYPE_PEM, self.root_cert).decode('utf-8')
-        key_str = crypto.dump_privatekey(crypto.FILETYPE_PEM, self.root_key).decode('utf-8')
+        key_str = self.root_key.private_bytes(
+            encoding=serialization.Encoding.PEM,
+            format=serialization.PrivateFormat.TraditionalOpenSSL,
+            encryption_algorithm=serialization.NoEncryption()
+        ).decode('utf-8')
 
         return (cert_str, key_str)
 
-    def generate_cert(self, name: str = '') -> Tuple[str, str]:
-        key = crypto.PKey()
-        key.generate_key(crypto.TYPE_RSA, 2048)
-
-        cert = crypto.X509()
-        cert.set_serial_number(int(uuid4()))
-
-        subj = cert.get_subject()
-        if not name:
-            subj.commonName = str(self.mgr.get_mgr_ip())
-        else:
-            subj.commonName = name
+    def generate_cert(self, addr: str = '') -> Tuple[str, str]:
+        if addr:
+            try:
+                ipaddress.IPv4Address(addr)
+            except Exception:
+                raise OrchestratorError(
+                    f'Address supplied to build cert ({addr}) is not valid IPv4 address')
+
+        private_key = rsa.generate_private_key(
+            public_exponent=65537, key_size=4096, backend=default_backend())
+        public_key = private_key.public_key()
+
+        builder = x509.CertificateBuilder()
+
+        builder = builder.subject_name(x509.Name([
+            x509.NameAttribute(NameOID.COMMON_NAME, addr if addr else str(self.mgr.get_mgr_ip())),
+        ]))
+
+        builder = builder.issuer_name(x509.Name([
+            x509.NameAttribute(NameOID.COMMON_NAME, u'cephadm-root'),
+        ]))
+
+        builder = builder.not_valid_before(datetime.now())
+        builder = builder.not_valid_after(datetime.now() + timedelta(days=(365 * 10 + 3)))
+        builder = builder.serial_number(x509.random_serial_number())
+        builder = builder.public_key(public_key)
+        builder = builder.add_extension(
+            x509.SubjectAlternativeName(
+                [x509.IPAddress(ipaddress.IPv4Address(
+                    addr if addr else str(self.mgr.get_mgr_ip())))]
+            ),
+            critical=False
+        )
+        builder = builder.add_extension(
+            x509.BasicConstraints(ca=False, path_length=None), critical=True,
+        )
 
-        cert.set_issuer(self.root_subj)
-        cert.set_pubkey(key)
-        cert.gmtime_adj_notBefore(0)
-        cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)  # 10 years
-        cert.sign(self.root_key, 'sha256')
+        cert = builder.sign(
+            private_key=self.root_key, algorithm=hashes.SHA256(), backend=default_backend()
+        )
 
         cert_str = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')
-        key_str = crypto.dump_privatekey(crypto.FILETYPE_PEM, key).decode('utf-8')
+        key_str = private_key.private_bytes(
+            encoding=serialization.Encoding.PEM,
+            format=serialization.PrivateFormat.TraditionalOpenSSL,
+            encryption_algorithm=serialization.NoEncryption()
+        ).decode('utf-8')
+
         return (cert_str, key_str)
 
     def get_root_cert(self) -> str:
index 09112b542fb7d88bfe996c1290d80feb8bdc4d03..b46cce02f476f40e2c5a38cbd317aafdd3b67e23 100644 (file)
@@ -2264,8 +2264,10 @@ Then run the following:
             deps = [d.name() for d in daemons if d.daemon_type == 'haproxy']
         elif daemon_type == 'agent':
             root_cert = ''
-            if self.cherrypy_thread and self.cherrypy_thread.ssl_certs.root_cert:
+            try:
                 root_cert = self.cherrypy_thread.ssl_certs.get_root_cert()
+            except Exception:
+                pass
             deps = sorted([self.get_mgr_ip(), str(self.endpoint_port), root_cert,
                           str(self.get_module_option('device_enhanced_scan'))])
         else:
index df86156dcbc0c6df050ec66c82bfadc08b437561..31f51f0db153c22a2ec2aaec99c5ca3540c9627d 100644 (file)
@@ -1033,7 +1033,7 @@ class CephadmAgent(CephService):
             raise OrchestratorError(
                 'Cannot deploy agent daemons until cephadm endpoint has finished generating certs')
         listener_cert, listener_key = self.mgr.cherrypy_thread.ssl_certs.generate_cert(
-            daemon_spec.host)
+            self.mgr.inventory.get_addr(daemon_spec.host))
         config = {
             'agent.json': json.dumps(cfg),
             'cephadm': self.mgr._cephadm,
index 49ea088e12cc73ecac2ebf6e5a7b226992ab0946..d71f25a027f47bee6e56eebda19a08b08ce6eed5 100644 (file)
@@ -1,6 +1,7 @@
 -e../../python-common
 asyncmock
 cherrypy
+cryptography
 jsonpatch
 Jinja2
 pecan