From 84edffdc96e93bcdf6d45047d104832f5c8cc81d Mon Sep 17 00:00:00 2001 From: Redouane Kachach Date: Tue, 11 Mar 2025 10:15:21 +0100 Subject: [PATCH] mgr/cephadm: refactor oauth2-proxy certs and cookie-secret handling 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 --- .../mgr/cephadm/services/oauth2_proxy.py | 23 +++++++++---------- src/pybind/mgr/cephadm/tests/test_services.py | 7 ++++-- .../ceph/deployment/service_spec.py | 8 ++++++- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/pybind/mgr/cephadm/services/oauth2_proxy.py b/src/pybind/mgr/cephadm/services/oauth2_proxy.py index 1b77d0faafd..bcf97968d90 100644 --- a/src/pybind/mgr/cephadm/services/oauth2_proxy.py +++ b/src/pybind/mgr/cephadm/services/oauth2_proxy.py @@ -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() } diff --git a/src/pybind/mgr/cephadm/tests/test_services.py b/src/pybind/mgr/cephadm/tests/test_services.py index c8682c3b5d1..0d0ecd115f1 100644 --- a/src/pybind/mgr/cephadm/tests/test_services.py +++ b/src/pybind/mgr/cephadm/tests/test_services.py @@ -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}", } diff --git a/src/python-common/ceph/deployment/service_spec.py b/src/python-common/ceph/deployment/service_spec.py index 8fe2f1e3d0c..b5661b89746 100644 --- a/src/python-common/ceph/deployment/service_spec.py +++ b/src/python-common/ceph/deployment/service_spec.py @@ -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 -- 2.39.5