Skip to content

Commit 677affc

Browse files
committed
mgr/cephadm: adding oauth2-proxy cephadm service
adding new oauth2-proxy service. The enable_auth flag enables SSO authentication via the oauth2-proxy service. The user must ensure the oauth2-proxy service is deployed before enabling this flag in the mgmt-gateway service. FQDN related changes: previously, we were obtaining the FQDN using a call to the Python socket library run inside the container. While this generally works, the FQDN returned inside a container can sometimes differ from the one obtained outside the container. This discrepancy could cause some issues. To ensure consistency, we now use the FQDN from the inventory, which provides the correct value as recognized on the host. Signed-off-by: Redouane Kachach <[email protected]>
1 parent 094fc18 commit 677affc

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)
@@ -3603,6 +3611,9 @@ def list_daemons(
36033611
elif daemon_type == MgmtGateway.daemon_type:
36043612
version = MgmtGateway.get_version(ctx, container_id)
36053613
seen_versions[image_id] = version
3614+
elif daemon_type == OAuth2Proxy.daemon_type:
3615+
version = OAuth2Proxy.get_version(ctx, container_id)
3616+
seen_versions[image_id] = version
36063617
else:
36073618
logger.warning('version for unknown daemon type %s' % daemon_type)
36083619
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)