]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/cephadm: refactor oauth2-proxy certs and cookie-secret handling
authorRedouane Kachach <rkachach@ibm.com>
Tue, 11 Mar 2025 09:15:21 +0000 (10:15 +0100)
committerRedouane Kachach <rkachach@ibm.com>
Wed, 19 Mar 2025 14:53:22 +0000 (15:53 +0100)
Moved the cookie-secret calculation to the spec level, allowing
all oauth2-poxy instances to share the same secret for high
availability. This change enables effective load balancing across
instances and ensures smooth failover in case of failures. In addition
mgmt-gateway virtual_ip is now included in the allowed_domain list
to enable HA senarios.

https://tracker.ceph.com/issues/70391

Signed-off-by: Redouane Kachach <rkachach@ibm.com>
src/pybind/mgr/cephadm/services/oauth2_proxy.py
src/pybind/mgr/cephadm/tests/test_services.py
src/python-common/ceph/deployment/service_spec.py

index 1b77d0faafd4b6ecf9fb7b9f2c3aab7aeee3cb69..bcf97968d90b622e975cb087d0a03905c4b5ca06 100644 (file)
@@ -1,10 +1,9 @@
 import logging
 from typing import List, Any, Tuple, Dict, cast, Optional
-import os
-import base64
+from copy import copy
 
 from orchestrator import DaemonDescription
-from ceph.deployment.service_spec import OAuth2ProxySpec
+from ceph.deployment.service_spec import OAuth2ProxySpec, MgmtGatewaySpec
 from cephadm.services.cephadmservice import CephadmService, CephadmDaemonDeploySpec
 from .service_registry import register_cephadm_service
 
@@ -23,6 +22,9 @@ class OAuth2ProxyService(CephadmService):
 
     def get_service_ips_and_hosts(self, service_name: str) -> List[str]:
         entries = set()
+        mgmt_gw_spec = cast(MgmtGatewaySpec, self.mgr.spec_store['mgmt-gateway'].spec)
+        if mgmt_gw_spec.virtual_ip is not None:
+            entries.add(mgmt_gw_spec.virtual_ip)
         for dd in self.mgr.cache.get_daemons_by_service(service_name):
             assert dd.hostname is not None
             addr = dd.ip if dd.ip else self.mgr.inventory.get_addr(dd.hostname)
@@ -43,9 +45,11 @@ class OAuth2ProxyService(CephadmService):
     def get_certificates(self, svc_spec: OAuth2ProxySpec, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[str, str]:
         cert = self.mgr.cert_mgr.get_cert('oauth2_proxy_cert')
         key = self.mgr.cert_mgr.get_key('oauth2_proxy_key')
+        user_made = False
         if not (cert and key):
             # not available on store, check if provided on the spec
             if svc_spec.ssl_certificate and svc_spec.ssl_certificate_key:
+                user_made = True
                 cert = svc_spec.ssl_certificate
                 key = svc_spec.ssl_certificate_key
             else:
@@ -55,25 +59,20 @@ class OAuth2ProxyService(CephadmService):
                 cert, key = self.mgr.cert_mgr.generate_cert(host_fqdn, addr)
             # save certificates
             if cert and key:
-                self.mgr.cert_mgr.save_cert('oauth2_proxy_cert', cert)
-                self.mgr.cert_mgr.save_key('oauth2_proxy_key', key)
+                self.mgr.cert_mgr.save_cert('oauth2_proxy_cert', cert, user_made=user_made)
+                self.mgr.cert_mgr.save_key('oauth2_proxy_key', key, user_made=user_made)
             else:
                 logger.error("Failed to obtain certificate and key from mgmt-gateway.")
         return cert, key
 
-    def generate_random_secret(self) -> str:
-        random_bytes = os.urandom(32)
-        base64_secret = base64.urlsafe_b64encode(random_bytes).rstrip(b'=').decode('utf-8')
-        return base64_secret
-
     def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
         assert self.TYPE == daemon_spec.daemon_type
         svc_spec = cast(OAuth2ProxySpec, self.mgr.spec_store[daemon_spec.service_name].spec)
-        allowlist_domains = svc_spec.allowlist_domains or []
+        allowlist_domains = copy(svc_spec.allowlist_domains) or []
         allowlist_domains += self.get_service_ips_and_hosts('mgmt-gateway')
         context = {
             'spec': svc_spec,
-            'cookie_secret': svc_spec.cookie_secret or self.generate_random_secret(),
+            'cookie_secret': svc_spec.cookie_secret,
             'allowlist_domains': allowlist_domains,
             'redirect_url': svc_spec.redirect_url or self.get_redirect_url()
         }
index c8682c3b5d172baf7a6c410463b9a1c07eca7435..0d0ecd115f1a24049b7c61a8fa94995c05a01f7f 100644 (file)
@@ -4606,14 +4606,17 @@ class TestMgmtGateway:
                                        enable_auth=True,
                                        virtual_ip=virtual_ip)
 
+        allowed_domain = '192.168.100.1:8080'
         oauth2_spec = OAuth2ProxySpec(provider_display_name='my_idp_provider',
                                       client_id='my_client_id',
                                       client_secret='my_client_secret',
                                       oidc_issuer_url='http://192.168.10.10:8888/dex',
                                       cookie_secret='kbAEM9opAmuHskQvt0AW8oeJRaOM2BYy5Loba0kZ0SQ=',
                                       ssl_certificate=ceph_generated_cert,
-                                      ssl_certificate_key=ceph_generated_key)
+                                      ssl_certificate_key=ceph_generated_key,
+                                      allowlist_domains=[allowed_domain])
 
+        whitelist_domains = f"{allowed_domain},1::4,ceph-node" if virtual_ip is None else f"{allowed_domain},{virtual_ip},1::4,ceph-node"
         redirect_url = f"https://{virtual_ip if virtual_ip else 'host_fqdn'}:5555/oauth2/callback"
         expected = {
             "fsid": "fsid",
@@ -4667,7 +4670,7 @@ class TestMgmtGateway:
                                          # Secret value for encrypting cookies.
                                          cookie_secret= "kbAEM9opAmuHskQvt0AW8oeJRaOM2BYy5Loba0kZ0SQ="
                                          email_domains= "*"
-                                         whitelist_domains= "1::4,ceph-node\""""),
+                                         whitelist_domains= "{whitelist_domains}\""""),
                     "oauth2-proxy.crt": f"{ceph_generated_cert}",
                     "oauth2-proxy.key": f"{ceph_generated_key}",
                 }
index 8fe2f1e3d0c5b03bd94079adcfaff16a0f3713de..b5661b8974694bc97a53eb9bf8428f72f5ea308f 100644 (file)
@@ -2028,7 +2028,7 @@ class OAuth2ProxySpec(ServiceSpec):
         self.redirect_url = redirect_url
         #: The secret key used for signing cookies. Its length must be 16,
         # 24, or 32 bytes to create an AES cipher.
-        self.cookie_secret = cookie_secret
+        self.cookie_secret = cookie_secret or self.generate_random_secret()
         #: The multi-line SSL certificate for encrypting communications.
         self.ssl_certificate = ssl_certificate
         #: The multi-line SSL certificate private key for decrypting communications.
@@ -2038,6 +2038,12 @@ class OAuth2ProxySpec(ServiceSpec):
         self.allowlist_domains = allowlist_domains
         self.unmanaged = unmanaged
 
+    def generate_random_secret(self) -> str:
+        import base64
+        random_bytes = os.urandom(32)
+        base64_secret = base64.urlsafe_b64encode(random_bytes).decode('utf-8')
+        return base64_secret
+
     def get_port_start(self) -> List[int]:
         ports = [4180]
         return ports