Skip to content

Commit

Permalink
Merge pull request #58460 from rkachach/fix_issue_oauth2_support
Browse files Browse the repository at this point in the history
adding support for SSO based on auth2-proxy

Reviewed-by: Adam King <[email protected]>
  • Loading branch information
adk3798 authored Aug 20, 2024
2 parents 47229b8 + 677affc commit a61eabe
Show file tree
Hide file tree
Showing 24 changed files with 1,376 additions and 147 deletions.
1 change: 1 addition & 0 deletions doc/cephadm/services/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ for details on individual services:
tracing
smb
mgmt-gateway
oauth2-proxy

Service Status
==============
Expand Down
139 changes: 139 additions & 0 deletions doc/cephadm/services/oauth2-proxy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
.. _deploy-cephadm-oauth2-proxy:

==================
OAuth2 Proxy
==================

Deploying oauth2-proxy
======================

In Ceph releases starting from Squid, the `oauth2-proxy` service introduces an advanced method
for managing authentication and access control for Ceph applications. This service integrates
with external Identity Providers (IDPs) to provide secure, flexible authentication via the
OIDC (OpenID Connect) protocol. `oauth2-proxy` acts as an authentication gateway, ensuring that
access to Ceph applications including the Ceph Dashboard and monitoring stack is tightly controlled.

To deploy the `oauth2-proxy` service, use the following command:

.. prompt:: bash #

ceph orch apply oauth2-proxy [--placement ...] ...

Once applied, `cephadm` will re-configure the necessary components to use `oauth2-proxy` for authentication,
thereby securing access to all Ceph applications. The service will handle login flows, redirect users
to the appropriate IDP for authentication, and manage session tokens to facilitate seamless user access.


Benefits of the oauth2-proxy service
====================================
* ``Enhanced Security``: Provides robust authentication through integration with external IDPs using the OIDC protocol.
* ``Seamless SSO``: Enables seamless single sign-on (SSO) across all Ceph applications, improving user access control.
* ``Centralized Authentication``: Centralizes authentication management, reducing complexity and improving control over access.


Security enhancements
=====================

The `oauth2-proxy` service ensures that all access to Ceph applications is authenticated, preventing unauthorized users from
accessing sensitive information. Since it makes use of the `oauth2-proxy` open source project, this service integrates
easily with a variety of `external IDPs <https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/>`_ to provide
a secure and flexible authentication mechanism.


High availability
==============================
`oauth2-proxy` is designed to integrate with an external IDP hence login high availability is not the responsibility of this
service. In squid release high availability for the service itself is not supported yet.


Accessing services with oauth2-proxy
====================================

After deploying `oauth2-proxy`, access to Ceph applications will require authentication through the configured IDP. Users will
be redirected to the IDP for login and then returned to the requested application. This setup ensures secure access and integrates
seamlessly with the Ceph management stack.


Service Specification
=====================

Before deploying `oauth2-proxy` service please remember to deploy the `mgmt-gateway` service by turning on the `--enable_auth` flag. i.e:

.. prompt:: bash #

ceph orch apply mgmt-gateway --enable_auth=true

An `oauth2-proxy` service can be applied using a specification. An example in YAML follows:

.. code-block:: yaml
service_type: oauth2-proxy
service_id: auth-proxy
placement:
hosts:
- ceph0
spec:
https_address: "0.0.0.0:4180"
provider_display_name: "My OIDC Provider"
client_id: "your-client-id"
oidc_issuer_url: "http://192.168.100.1:5556/dex"
client_secret: "your-client-secret"
cookie_secret: "your-cookie-secret"
ssl_certificate: |
-----BEGIN CERTIFICATE-----
MIIDtTCCAp2gAwIBAgIYMC4xNzc1NDQxNjEzMzc2MjMyXzxvQ7EcMA0GCSqGSIb3
DQEBCwUAMG0xCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMRcwFQYDVQQHDA5T
[...]
-----END CERTIFICATE-----
ssl_certificate_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5jdYbjtNTAKW4
/CwQr/7wOiLGzVxChn3mmCIF3DwbL/qvTFTX2d8bDf6LjGwLYloXHscRfxszX/4h
[...]
-----END PRIVATE KEY-----
Fields specific to the ``spec`` section of the `oauth2-proxy` service are described below. More detailed
description of the fields can be found on `oauth2-proxy <https://oauth2-proxy.github.io/oauth2-proxy/>`_
project documentation.


.. py:currentmodule:: ceph.deployment.service_spec
.. autoclass:: OAuth2ProxySpec
:members:

The specification can then be applied by running the below command. Once becomes available, cephadm will automatically redeploy
the `mgmt-gateway` service while adapting its configuration to redirect the authentication to the newly deployed `oauth2-service`.

.. prompt:: bash #

ceph orch apply -i oauth2-proxy.yaml


Limitations
===========

A non-exhaustive list of important limitations for the `oauth2-proxy` service follows:

* High-availability configurations for `oauth2-proxy` itself are not supported.
* Proper configuration of the IDP and OAuth2 parameters is crucial to avoid authentication failures. Misconfigurations can lead to access issues.


Default images
~~~~~~~~~~~~~~

The `oauth2-proxy` service typically uses the default container image:

::

DEFAULT_OAUTH2_PROXY = 'quay.io/oauth2-proxy/oauth2-proxy:v7.2.0'

Admins can specify the image to be used by changing the `container_image_oauth2_proxy` cephadm module option. If there were already running daemon(s),
you must redeploy the daemon(s) to apply the new image.

For example:

.. code-block:: bash
ceph config set mgr mgr/cephadm/container_image_oauth2_proxy <new-oauth2-proxy-image>
ceph orch redeploy oauth2-proxy
11 changes: 11 additions & 0 deletions src/cephadm/cephadm.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
SMB,
SNMPGateway,
MgmtGateway,
OAuth2Proxy,
Tracing,
NodeProxy,
)
Expand Down Expand Up @@ -230,6 +231,7 @@ def get_supported_daemons():
supported_daemons.append(CephadmAgent.daemon_type)
supported_daemons.append(SNMPGateway.daemon_type)
supported_daemons.append(MgmtGateway.daemon_type)
supported_daemons.append(OAuth2Proxy.daemon_type)
supported_daemons.extend(Tracing.components)
supported_daemons.append(NodeProxy.daemon_type)
supported_daemons.append(SMB.daemon_type)
Expand Down Expand Up @@ -468,6 +470,8 @@ def update_default_image(ctx: CephadmContext) -> None:
ctx.image = SNMPGateway.default_image
if type_ == MgmtGateway.daemon_type:
ctx.image = MgmtGateway.default_image
if type_ == OAuth2Proxy.daemon_type:
ctx.image = OAuth2Proxy.default_image
if type_ == CephNvmeof.daemon_type:
ctx.image = CephNvmeof.default_image
if type_ in Tracing.components:
Expand Down Expand Up @@ -864,6 +868,10 @@ def create_daemon_dirs(
cg = MgmtGateway.init(ctx, fsid, ident.daemon_id)
cg.create_daemon_dirs(data_dir, uid, gid)

elif daemon_type == OAuth2Proxy.daemon_type:
co = OAuth2Proxy.init(ctx, fsid, ident.daemon_id)
co.create_daemon_dirs(data_dir, uid, gid)

elif daemon_type == NodeProxy.daemon_type:
node_proxy = NodeProxy.init(ctx, fsid, ident.daemon_id)
node_proxy.create_daemon_dirs(data_dir, uid, gid)
Expand Down Expand Up @@ -3605,6 +3613,9 @@ def list_daemons(
elif daemon_type == MgmtGateway.daemon_type:
version = MgmtGateway.get_version(ctx, container_id)
seen_versions[image_id] = version
elif daemon_type == OAuth2Proxy.daemon_type:
version = OAuth2Proxy.get_version(ctx, container_id)
seen_versions[image_id] = version
else:
logger.warning('version for unknown daemon type %s' % daemon_type)
else:
Expand Down
1 change: 1 addition & 0 deletions src/cephadm/cephadmlib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
DEFAULT_JAEGER_QUERY_IMAGE = 'quay.io/jaegertracing/jaeger-query:1.29'
DEFAULT_SMB_IMAGE = 'quay.io/samba.org/samba-server:devbuilds-centos-amd64'
DEFAULT_NGINX_IMAGE = 'quay.io/ceph/nginx:1.26.1'
DEFAULT_OAUTH2_PROXY_IMAGE = 'quay.io/oauth2-proxy/oauth2-proxy:v7.6.0'
DEFAULT_REGISTRY = 'docker.io' # normalize unqualified digests to this
# ------------------------------------------------------------------------------

Expand Down
2 changes: 2 additions & 0 deletions src/cephadm/cephadmlib/daemons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .tracing import Tracing
from .node_proxy import NodeProxy
from .mgmt_gateway import MgmtGateway
from .oauth2_proxy import OAuth2Proxy

__all__ = [
'Ceph',
Expand All @@ -27,4 +28,5 @@
'Tracing',
'NodeProxy',
'MgmtGateway',
'OAuth2Proxy',
]
165 changes: 165 additions & 0 deletions src/cephadm/cephadmlib/daemons/oauth2_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import logging
import os
from typing import Dict, List, Tuple, Optional
import re

from ..call_wrappers import call, CallVerbosity
from ..container_daemon_form import ContainerDaemonForm, daemon_to_container
from ..container_types import CephContainer
from ..context import CephadmContext
from ..context_getters import fetch_configs
from ..daemon_form import register as register_daemon_form
from ..daemon_identity import DaemonIdentity
from ..deployment_utils import to_deployment_container
from ..constants import DEFAULT_OAUTH2_PROXY_IMAGE, UID_NOBODY, GID_NOGROUP
from ..data_utils import dict_get, is_fsid
from ..file_utils import populate_files, makedirs, recursive_chown
from ..exceptions import Error


logger = logging.getLogger()


@register_daemon_form
class OAuth2Proxy(ContainerDaemonForm):
"""Define the configs for the jaeger tracing containers"""

default_image = DEFAULT_OAUTH2_PROXY_IMAGE
daemon_type = 'oauth2-proxy'
required_files = [
'oauth2-proxy.conf',
'oauth2-proxy.crt',
'oauth2-proxy.key',
]

@classmethod
def for_daemon_type(cls, daemon_type: str) -> bool:
return cls.daemon_type == daemon_type

def __init__(
self,
ctx: CephadmContext,
fsid: str,
daemon_id: str,
config_json: Dict,
image: str = DEFAULT_OAUTH2_PROXY_IMAGE,
):
self.ctx = ctx
self.fsid = fsid
self.daemon_id = daemon_id
self.image = image
self.files = dict_get(config_json, 'files', {})
self.validate()

@classmethod
def init(
cls, ctx: CephadmContext, fsid: str, daemon_id: str
) -> 'OAuth2Proxy':
return cls(ctx, fsid, daemon_id, fetch_configs(ctx), ctx.image)

@classmethod
def create(
cls, ctx: CephadmContext, ident: DaemonIdentity
) -> 'OAuth2Proxy':
return cls.init(ctx, ident.fsid, ident.daemon_id)

@property
def identity(self) -> DaemonIdentity:
return DaemonIdentity(self.fsid, self.daemon_type, self.daemon_id)

def container(self, ctx: CephadmContext) -> CephContainer:
ctr = daemon_to_container(ctx, self)
return to_deployment_container(ctx, ctr)

def uid_gid(self, ctx: CephadmContext) -> Tuple[int, int]:
return UID_NOBODY, GID_NOGROUP

def get_daemon_args(self) -> List[str]:
return [
'--config=/etc/oauth2-proxy.conf',
'--tls-cert-file=/etc/oauth2-proxy.crt',
'--tls-key-file=/etc/oauth2-proxy.key',
]

def default_entrypoint(self) -> str:
return ''

def create_daemon_dirs(self, data_dir: str, uid: int, gid: int) -> None:
"""Create files under the container data dir"""
if not os.path.isdir(data_dir):
raise OSError('data_dir is not a directory: %s' % (data_dir))
logger.info('Writing oauth2-proxy config...')
config_dir = os.path.join(data_dir, 'etc/')
makedirs(config_dir, uid, gid, 0o755)
recursive_chown(config_dir, uid, gid)
populate_files(config_dir, self.files, uid, gid)

def validate(self) -> None:
if not is_fsid(self.fsid):
raise Error(f'not an fsid: {self.fsid}')
if not self.daemon_id:
raise Error(f'invalid daemon_id: {self.daemon_id}')
if not self.image:
raise Error(f'invalid image: {self.image}')

# check for the required files
if self.required_files:
for fname in self.required_files:
if fname not in self.files:
raise Error(
'required file missing from config-json: %s' % fname
)

@staticmethod
def get_version(ctx: CephadmContext, container_id: str) -> Optional[str]:
"""Return the version of the oauth2-proxy container"""
version = None
out, err, code = call(
ctx,
[
ctx.container_engine.path,
'exec',
container_id,
'oauth2-proxy',
'--version',
],
verbosity=CallVerbosity.QUIET,
)
if code == 0:
match = re.search(r'oauth2-proxy (v\d+\.\d+\.\d+)', out)
if match:
version = match.group(1)
return version

def customize_container_mounts(
self, ctx: CephadmContext, mounts: Dict[str, str]
) -> None:
data_dir = self.identity.data_dir(ctx.data_dir)
mounts.update(
{
os.path.join(
data_dir, 'etc/oauth2-proxy.conf'
): '/etc/oauth2-proxy.conf:Z',
os.path.join(
data_dir, 'etc/oauth2-proxy.crt'
): '/etc/oauth2-proxy.crt:Z',
os.path.join(
data_dir, 'etc/oauth2-proxy.key'
): '/etc/oauth2-proxy.key:Z',
}
)

def customize_container_args(
self, ctx: CephadmContext, args: List[str]
) -> None:
uid, _ = self.uid_gid(ctx)
other_args = [
'--user',
str(uid),
]
args.extend(other_args)

def customize_process_args(
self, ctx: CephadmContext, args: List[str]
) -> None:
args.extend(self.get_daemon_args())
4 changes: 2 additions & 2 deletions src/pybind/mgr/cephadm/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(self, mgr: "CephadmOrchestrator") -> None:
self.service_discovery = ServiceDiscovery(mgr)
self.cherrypy_shutdown_event = threading.Event()
self._service_discovery_port = self.mgr.service_discovery_port
security_enabled, mgmt_gw_enabled = self.mgr._get_security_config()
security_enabled, _, _ = self.mgr._get_security_config()
self.security_enabled = security_enabled
super().__init__(target=self.run)

Expand All @@ -50,7 +50,7 @@ def configure(self) -> None:

def config_update(self) -> None:
self.service_discovery_port = self.mgr.service_discovery_port
security_enabled, mgmt_gw_enabled = self.mgr._get_security_config()
security_enabled, _, _ = self.mgr._get_security_config()
if self.security_enabled != security_enabled:
self.security_enabled = security_enabled
self.restart()
Expand Down
Loading

0 comments on commit a61eabe

Please sign in to comment.