Skip to content

Commit a61eabe

Browse files
authored
Merge pull request ceph#58460 from rkachach/fix_issue_oauth2_support
adding support for SSO based on auth2-proxy Reviewed-by: Adam King <[email protected]>
2 parents 47229b8 + 677affc commit a61eabe

File tree

24 files changed

+1376
-147
lines changed

24 files changed

+1376
-147
lines changed

doc/cephadm/services/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ for details on individual services:
2121
tracing
2222
smb
2323
mgmt-gateway
24+
oauth2-proxy
2425

2526
Service Status
2627
==============
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
.. _deploy-cephadm-oauth2-proxy:
2+
3+
==================
4+
OAuth2 Proxy
5+
==================
6+
7+
Deploying oauth2-proxy
8+
======================
9+
10+
In Ceph releases starting from Squid, the `oauth2-proxy` service introduces an advanced method
11+
for managing authentication and access control for Ceph applications. This service integrates
12+
with external Identity Providers (IDPs) to provide secure, flexible authentication via the
13+
OIDC (OpenID Connect) protocol. `oauth2-proxy` acts as an authentication gateway, ensuring that
14+
access to Ceph applications including the Ceph Dashboard and monitoring stack is tightly controlled.
15+
16+
To deploy the `oauth2-proxy` service, use the following command:
17+
18+
.. prompt:: bash #
19+
20+
ceph orch apply oauth2-proxy [--placement ...] ...
21+
22+
Once applied, `cephadm` will re-configure the necessary components to use `oauth2-proxy` for authentication,
23+
thereby securing access to all Ceph applications. The service will handle login flows, redirect users
24+
to the appropriate IDP for authentication, and manage session tokens to facilitate seamless user access.
25+
26+
27+
Benefits of the oauth2-proxy service
28+
====================================
29+
* ``Enhanced Security``: Provides robust authentication through integration with external IDPs using the OIDC protocol.
30+
* ``Seamless SSO``: Enables seamless single sign-on (SSO) across all Ceph applications, improving user access control.
31+
* ``Centralized Authentication``: Centralizes authentication management, reducing complexity and improving control over access.
32+
33+
34+
Security enhancements
35+
=====================
36+
37+
The `oauth2-proxy` service ensures that all access to Ceph applications is authenticated, preventing unauthorized users from
38+
accessing sensitive information. Since it makes use of the `oauth2-proxy` open source project, this service integrates
39+
easily with a variety of `external IDPs <https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/>`_ to provide
40+
a secure and flexible authentication mechanism.
41+
42+
43+
High availability
44+
==============================
45+
`oauth2-proxy` is designed to integrate with an external IDP hence login high availability is not the responsibility of this
46+
service. In squid release high availability for the service itself is not supported yet.
47+
48+
49+
Accessing services with oauth2-proxy
50+
====================================
51+
52+
After deploying `oauth2-proxy`, access to Ceph applications will require authentication through the configured IDP. Users will
53+
be redirected to the IDP for login and then returned to the requested application. This setup ensures secure access and integrates
54+
seamlessly with the Ceph management stack.
55+
56+
57+
Service Specification
58+
=====================
59+
60+
Before deploying `oauth2-proxy` service please remember to deploy the `mgmt-gateway` service by turning on the `--enable_auth` flag. i.e:
61+
62+
.. prompt:: bash #
63+
64+
ceph orch apply mgmt-gateway --enable_auth=true
65+
66+
An `oauth2-proxy` service can be applied using a specification. An example in YAML follows:
67+
68+
.. code-block:: yaml
69+
70+
service_type: oauth2-proxy
71+
service_id: auth-proxy
72+
placement:
73+
hosts:
74+
- ceph0
75+
spec:
76+
https_address: "0.0.0.0:4180"
77+
provider_display_name: "My OIDC Provider"
78+
client_id: "your-client-id"
79+
oidc_issuer_url: "http://192.168.100.1:5556/dex"
80+
client_secret: "your-client-secret"
81+
cookie_secret: "your-cookie-secret"
82+
ssl_certificate: |
83+
-----BEGIN CERTIFICATE-----
84+
MIIDtTCCAp2gAwIBAgIYMC4xNzc1NDQxNjEzMzc2MjMyXzxvQ7EcMA0GCSqGSIb3
85+
DQEBCwUAMG0xCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMRcwFQYDVQQHDA5T
86+
[...]
87+
-----END CERTIFICATE-----
88+
ssl_certificate_key: |
89+
-----BEGIN PRIVATE KEY-----
90+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5jdYbjtNTAKW4
91+
/CwQr/7wOiLGzVxChn3mmCIF3DwbL/qvTFTX2d8bDf6LjGwLYloXHscRfxszX/4h
92+
[...]
93+
-----END PRIVATE KEY-----
94+
95+
Fields specific to the ``spec`` section of the `oauth2-proxy` service are described below. More detailed
96+
description of the fields can be found on `oauth2-proxy <https://oauth2-proxy.github.io/oauth2-proxy/>`_
97+
project documentation.
98+
99+
100+
.. py:currentmodule:: ceph.deployment.service_spec
101+
102+
.. autoclass:: OAuth2ProxySpec
103+
:members:
104+
105+
The specification can then be applied by running the below command. Once becomes available, cephadm will automatically redeploy
106+
the `mgmt-gateway` service while adapting its configuration to redirect the authentication to the newly deployed `oauth2-service`.
107+
108+
.. prompt:: bash #
109+
110+
ceph orch apply -i oauth2-proxy.yaml
111+
112+
113+
Limitations
114+
===========
115+
116+
A non-exhaustive list of important limitations for the `oauth2-proxy` service follows:
117+
118+
* High-availability configurations for `oauth2-proxy` itself are not supported.
119+
* Proper configuration of the IDP and OAuth2 parameters is crucial to avoid authentication failures. Misconfigurations can lead to access issues.
120+
121+
122+
Default images
123+
~~~~~~~~~~~~~~
124+
125+
The `oauth2-proxy` service typically uses the default container image:
126+
127+
::
128+
129+
DEFAULT_OAUTH2_PROXY = 'quay.io/oauth2-proxy/oauth2-proxy:v7.2.0'
130+
131+
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),
132+
you must redeploy the daemon(s) to apply the new image.
133+
134+
For example:
135+
136+
.. code-block:: bash
137+
138+
ceph config set mgr mgr/cephadm/container_image_oauth2_proxy <new-oauth2-proxy-image>
139+
ceph orch redeploy oauth2-proxy

src/cephadm/cephadm.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
SMB,
179179
SNMPGateway,
180180
MgmtGateway,
181+
OAuth2Proxy,
181182
Tracing,
182183
NodeProxy,
183184
)
@@ -230,6 +231,7 @@ def get_supported_daemons():
230231
supported_daemons.append(CephadmAgent.daemon_type)
231232
supported_daemons.append(SNMPGateway.daemon_type)
232233
supported_daemons.append(MgmtGateway.daemon_type)
234+
supported_daemons.append(OAuth2Proxy.daemon_type)
233235
supported_daemons.extend(Tracing.components)
234236
supported_daemons.append(NodeProxy.daemon_type)
235237
supported_daemons.append(SMB.daemon_type)
@@ -468,6 +470,8 @@ def update_default_image(ctx: CephadmContext) -> None:
468470
ctx.image = SNMPGateway.default_image
469471
if type_ == MgmtGateway.daemon_type:
470472
ctx.image = MgmtGateway.default_image
473+
if type_ == OAuth2Proxy.daemon_type:
474+
ctx.image = OAuth2Proxy.default_image
471475
if type_ == CephNvmeof.daemon_type:
472476
ctx.image = CephNvmeof.default_image
473477
if type_ in Tracing.components:
@@ -864,6 +868,10 @@ def create_daemon_dirs(
864868
cg = MgmtGateway.init(ctx, fsid, ident.daemon_id)
865869
cg.create_daemon_dirs(data_dir, uid, gid)
866870

871+
elif daemon_type == OAuth2Proxy.daemon_type:
872+
co = OAuth2Proxy.init(ctx, fsid, ident.daemon_id)
873+
co.create_daemon_dirs(data_dir, uid, gid)
874+
867875
elif daemon_type == NodeProxy.daemon_type:
868876
node_proxy = NodeProxy.init(ctx, fsid, ident.daemon_id)
869877
node_proxy.create_daemon_dirs(data_dir, uid, gid)
@@ -3605,6 +3613,9 @@ def list_daemons(
36053613
elif daemon_type == MgmtGateway.daemon_type:
36063614
version = MgmtGateway.get_version(ctx, container_id)
36073615
seen_versions[image_id] = version
3616+
elif daemon_type == OAuth2Proxy.daemon_type:
3617+
version = OAuth2Proxy.get_version(ctx, container_id)
3618+
seen_versions[image_id] = version
36083619
else:
36093620
logger.warning('version for unknown daemon type %s' % daemon_type)
36103621
else:

src/cephadm/cephadmlib/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
DEFAULT_JAEGER_QUERY_IMAGE = 'quay.io/jaegertracing/jaeger-query:1.29'
2121
DEFAULT_SMB_IMAGE = 'quay.io/samba.org/samba-server:devbuilds-centos-amd64'
2222
DEFAULT_NGINX_IMAGE = 'quay.io/ceph/nginx:1.26.1'
23+
DEFAULT_OAUTH2_PROXY_IMAGE = 'quay.io/oauth2-proxy/oauth2-proxy:v7.6.0'
2324
DEFAULT_REGISTRY = 'docker.io' # normalize unqualified digests to this
2425
# ------------------------------------------------------------------------------
2526

src/cephadm/cephadmlib/daemons/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .tracing import Tracing
1111
from .node_proxy import NodeProxy
1212
from .mgmt_gateway import MgmtGateway
13+
from .oauth2_proxy import OAuth2Proxy
1314

1415
__all__ = [
1516
'Ceph',
@@ -27,4 +28,5 @@
2728
'Tracing',
2829
'NodeProxy',
2930
'MgmtGateway',
31+
'OAuth2Proxy',
3032
]
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import logging
2+
import os
3+
from typing import Dict, List, Tuple, Optional
4+
import re
5+
6+
from ..call_wrappers import call, CallVerbosity
7+
from ..container_daemon_form import ContainerDaemonForm, daemon_to_container
8+
from ..container_types import CephContainer
9+
from ..context import CephadmContext
10+
from ..context_getters import fetch_configs
11+
from ..daemon_form import register as register_daemon_form
12+
from ..daemon_identity import DaemonIdentity
13+
from ..deployment_utils import to_deployment_container
14+
from ..constants import DEFAULT_OAUTH2_PROXY_IMAGE, UID_NOBODY, GID_NOGROUP
15+
from ..data_utils import dict_get, is_fsid
16+
from ..file_utils import populate_files, makedirs, recursive_chown
17+
from ..exceptions import Error
18+
19+
20+
logger = logging.getLogger()
21+
22+
23+
@register_daemon_form
24+
class OAuth2Proxy(ContainerDaemonForm):
25+
"""Define the configs for the jaeger tracing containers"""
26+
27+
default_image = DEFAULT_OAUTH2_PROXY_IMAGE
28+
daemon_type = 'oauth2-proxy'
29+
required_files = [
30+
'oauth2-proxy.conf',
31+
'oauth2-proxy.crt',
32+
'oauth2-proxy.key',
33+
]
34+
35+
@classmethod
36+
def for_daemon_type(cls, daemon_type: str) -> bool:
37+
return cls.daemon_type == daemon_type
38+
39+
def __init__(
40+
self,
41+
ctx: CephadmContext,
42+
fsid: str,
43+
daemon_id: str,
44+
config_json: Dict,
45+
image: str = DEFAULT_OAUTH2_PROXY_IMAGE,
46+
):
47+
self.ctx = ctx
48+
self.fsid = fsid
49+
self.daemon_id = daemon_id
50+
self.image = image
51+
self.files = dict_get(config_json, 'files', {})
52+
self.validate()
53+
54+
@classmethod
55+
def init(
56+
cls, ctx: CephadmContext, fsid: str, daemon_id: str
57+
) -> 'OAuth2Proxy':
58+
return cls(ctx, fsid, daemon_id, fetch_configs(ctx), ctx.image)
59+
60+
@classmethod
61+
def create(
62+
cls, ctx: CephadmContext, ident: DaemonIdentity
63+
) -> 'OAuth2Proxy':
64+
return cls.init(ctx, ident.fsid, ident.daemon_id)
65+
66+
@property
67+
def identity(self) -> DaemonIdentity:
68+
return DaemonIdentity(self.fsid, self.daemon_type, self.daemon_id)
69+
70+
def container(self, ctx: CephadmContext) -> CephContainer:
71+
ctr = daemon_to_container(ctx, self)
72+
return to_deployment_container(ctx, ctr)
73+
74+
def uid_gid(self, ctx: CephadmContext) -> Tuple[int, int]:
75+
return UID_NOBODY, GID_NOGROUP
76+
77+
def get_daemon_args(self) -> List[str]:
78+
return [
79+
'--config=/etc/oauth2-proxy.conf',
80+
'--tls-cert-file=/etc/oauth2-proxy.crt',
81+
'--tls-key-file=/etc/oauth2-proxy.key',
82+
]
83+
84+
def default_entrypoint(self) -> str:
85+
return ''
86+
87+
def create_daemon_dirs(self, data_dir: str, uid: int, gid: int) -> None:
88+
"""Create files under the container data dir"""
89+
if not os.path.isdir(data_dir):
90+
raise OSError('data_dir is not a directory: %s' % (data_dir))
91+
logger.info('Writing oauth2-proxy config...')
92+
config_dir = os.path.join(data_dir, 'etc/')
93+
makedirs(config_dir, uid, gid, 0o755)
94+
recursive_chown(config_dir, uid, gid)
95+
populate_files(config_dir, self.files, uid, gid)
96+
97+
def validate(self) -> None:
98+
if not is_fsid(self.fsid):
99+
raise Error(f'not an fsid: {self.fsid}')
100+
if not self.daemon_id:
101+
raise Error(f'invalid daemon_id: {self.daemon_id}')
102+
if not self.image:
103+
raise Error(f'invalid image: {self.image}')
104+
105+
# check for the required files
106+
if self.required_files:
107+
for fname in self.required_files:
108+
if fname not in self.files:
109+
raise Error(
110+
'required file missing from config-json: %s' % fname
111+
)
112+
113+
@staticmethod
114+
def get_version(ctx: CephadmContext, container_id: str) -> Optional[str]:
115+
"""Return the version of the oauth2-proxy container"""
116+
version = None
117+
out, err, code = call(
118+
ctx,
119+
[
120+
ctx.container_engine.path,
121+
'exec',
122+
container_id,
123+
'oauth2-proxy',
124+
'--version',
125+
],
126+
verbosity=CallVerbosity.QUIET,
127+
)
128+
if code == 0:
129+
match = re.search(r'oauth2-proxy (v\d+\.\d+\.\d+)', out)
130+
if match:
131+
version = match.group(1)
132+
return version
133+
134+
def customize_container_mounts(
135+
self, ctx: CephadmContext, mounts: Dict[str, str]
136+
) -> None:
137+
data_dir = self.identity.data_dir(ctx.data_dir)
138+
mounts.update(
139+
{
140+
os.path.join(
141+
data_dir, 'etc/oauth2-proxy.conf'
142+
): '/etc/oauth2-proxy.conf:Z',
143+
os.path.join(
144+
data_dir, 'etc/oauth2-proxy.crt'
145+
): '/etc/oauth2-proxy.crt:Z',
146+
os.path.join(
147+
data_dir, 'etc/oauth2-proxy.key'
148+
): '/etc/oauth2-proxy.key:Z',
149+
}
150+
)
151+
152+
def customize_container_args(
153+
self, ctx: CephadmContext, args: List[str]
154+
) -> None:
155+
uid, _ = self.uid_gid(ctx)
156+
other_args = [
157+
'--user',
158+
str(uid),
159+
]
160+
args.extend(other_args)
161+
162+
def customize_process_args(
163+
self, ctx: CephadmContext, args: List[str]
164+
) -> None:
165+
args.extend(self.get_daemon_args())

src/pybind/mgr/cephadm/http_server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(self, mgr: "CephadmOrchestrator") -> None:
3131
self.service_discovery = ServiceDiscovery(mgr)
3232
self.cherrypy_shutdown_event = threading.Event()
3333
self._service_discovery_port = self.mgr.service_discovery_port
34-
security_enabled, mgmt_gw_enabled = self.mgr._get_security_config()
34+
security_enabled, _, _ = self.mgr._get_security_config()
3535
self.security_enabled = security_enabled
3636
super().__init__(target=self.run)
3737

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

5151
def config_update(self) -> None:
5252
self.service_discovery_port = self.mgr.service_discovery_port
53-
security_enabled, mgmt_gw_enabled = self.mgr._get_security_config()
53+
security_enabled, _, _ = self.mgr._get_security_config()
5454
if self.security_enabled != security_enabled:
5555
self.security_enabled = security_enabled
5656
self.restart()

0 commit comments

Comments
 (0)