From 541ceec4018e311cef1517a62eefa28cd53bc162 Mon Sep 17 00:00:00 2001 From: Samuel Walladge Date: Wed, 31 May 2023 12:23:11 +0930 Subject: [PATCH] Enable rgw trust forwarded https when https proxy This option is required for server-side encryption to be allowed if radosgw is behind a reverse proxy, such as here when certificates are configured and apache2 is running. ref. https://docs.ceph.com/en/latest/radosgw/encryption/ It is safe to always enable when https is configured in the charm, because it will be securely behind the reverse proxy in the unit. This option must not be enabled when https is not configured in the charm, because this would allow clients to spoof headers. Closes-Bug: #2021560 Change-Id: I940f9b2f424a3d98936b5f185bf8f87b71091317 --- hooks/ceph_radosgw_context.py | 1 + templates/ceph.conf | 3 + unit_tests/test_ceph_radosgw_context.py | 112 +++++++++++++++++++++--- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/hooks/ceph_radosgw_context.py b/hooks/ceph_radosgw_context.py index c951fb61..e57ceef2 100644 --- a/hooks/ceph_radosgw_context.py +++ b/hooks/ceph_radosgw_context.py @@ -295,6 +295,7 @@ class MonContext(context.CephContext): 'rgw_swift_versioning': config('rgw-swift-versioning-enabled'), 'relaxed_s3_bucket_names': config('relaxed-s3-bucket-names'), 'frontend': http_frontend, + 'behind_https_proxy': https(), } # NOTE(dosaboy): these sections must correspond to what is supported in diff --git a/templates/ceph.conf b/templates/ceph.conf index d728ac03..d126b20b 100644 --- a/templates/ceph.conf +++ b/templates/ceph.conf @@ -16,6 +16,9 @@ ms bind ipv6 = true {% endif %} rgw swift versioning enabled = {{ rgw_swift_versioning }} rgw relaxed s3 bucket names = {{ relaxed_s3_bucket_names }} +{% if behind_https_proxy -%} +rgw trust forwarded https = true +{% endif %} {% if global -%} # The following are user-provided options provided via the config-flags charm option. # User-provided [global] section config diff --git a/unit_tests/test_ceph_radosgw_context.py b/unit_tests/test_ceph_radosgw_context.py index cfd07e07..f3f9553a 100644 --- a/unit_tests/test_ceph_radosgw_context.py +++ b/unit_tests/test_ceph_radosgw_context.py @@ -74,6 +74,7 @@ class HAProxyContextTests(CharmTestCase): class MonContextTest(CharmTestCase): + maxDiff = None def setUp(self): super(MonContextTest, self).setUp(context, TO_PATCH) @@ -95,10 +96,16 @@ class MonContextTest(CharmTestCase): else: return [] + @patch('charmhelpers.contrib.hahelpers.cluster.relation_ids') + @patch('charmhelpers.contrib.hahelpers.cluster.config_get') @patch.object(ceph, 'config', lambda *args: '{"client.radosgw.gateway": {"rgw init timeout": 60}}') @patch.object(context, 'ensure_host_resolvable_v6') - def test_ctxt(self, mock_ensure_rsv_v6): + def test_ctxt( + self, mock_ensure_rsv_v6, mock_config_get, mock_relation_ids + ): + mock_relation_ids.return_value = [] + mock_config_get.side_effect = self.test_config.get self.socket.gethostname.return_value = 'testhost' mon_ctxt = context.MonContext() addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3'] @@ -136,7 +143,8 @@ class MonContextTest(CharmTestCase): 'frontend': 'beast', 'relaxed_s3_bucket_names': False, 'rgw_zonegroup': 'zonegroup1', - 'rgw_realm': 'realmX' + 'rgw_realm': 'realmX', + 'behind_https_proxy': False, } self.assertEqual(expect, mon_ctxt()) self.assertFalse(mock_ensure_rsv_v6.called) @@ -148,10 +156,72 @@ class MonContextTest(CharmTestCase): self.assertEqual(expect, mon_ctxt()) self.assertTrue(mock_ensure_rsv_v6.called) + @patch('ceph_radosgw_context.https') @patch.object(ceph, 'config', lambda *args: '{"client.radosgw.gateway": {"rgw init timeout": 60}}') @patch.object(context, 'ensure_host_resolvable_v6') - def test_list_of_addresses_from_ceph_proxy(self, mock_ensure_rsv_v6): + def test_ctxt_with_https_proxy(self, mock_ensure_rsv_v6, mock_https): + mock_https.return_value = True + self.socket.gethostname.return_value = 'testhost' + mon_ctxt = context.MonContext() + addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3'] + + def _relation_get(attr, unit, rid): + if attr == 'ceph-public-address': + return addresses.pop() + elif attr == 'auth': + return 'cephx' + elif attr == 'rgw.testhost_key': + return 'testkey' + elif attr == 'fsid': + return 'testfsid' + + self.relation_get.side_effect = _relation_get + self.relation_ids.return_value = ['mon:6'] + self.related_units.return_value = ['ceph/0', 'ceph/1', 'ceph/2'] + self.multisite.plain_list = self.plain_list_stub + self.determine_api_port.return_value = 70 + expect = { + 'auth_supported': 'cephx', + 'hostname': 'testhost', + 'mon_hosts': '10.5.4.1 10.5.4.2 10.5.4.3', + 'old_auth': False, + 'systemd_rgw': True, + 'unit_public_ip': '10.255.255.255', + 'use_syslog': 'false', + 'loglevel': 1, + 'port': 70, + 'client_radosgw_gateway': {'rgw init timeout': 60}, + 'ipv6': False, + 'rgw_zone': 'default', + 'fsid': 'testfsid', + 'rgw_swift_versioning': False, + 'frontend': 'beast', + 'relaxed_s3_bucket_names': False, + 'rgw_zonegroup': 'zonegroup1', + 'rgw_realm': 'realmX', + 'behind_https_proxy': True, + } + self.assertEqual(expect, mon_ctxt()) + self.assertFalse(mock_ensure_rsv_v6.called) + + self.test_config.set('prefer-ipv6', True) + addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3'] + expect['ipv6'] = True + expect['port'] = "[::]:%s" % (70) + self.assertEqual(expect, mon_ctxt()) + self.assertTrue(mock_ensure_rsv_v6.called) + + @patch('charmhelpers.contrib.hahelpers.cluster.relation_ids') + @patch('charmhelpers.contrib.hahelpers.cluster.config_get') + @patch.object(ceph, 'config', lambda *args: + '{"client.radosgw.gateway": {"rgw init timeout": 60}}') + @patch.object(context, 'ensure_host_resolvable_v6') + def test_list_of_addresses_from_ceph_proxy( + self, mock_ensure_rsv_v6, mock_config_get, mock_relation_ids + ): + mock_relation_ids.return_value = [] + mock_config_get.side_effect = self.test_config.get self.socket.gethostname.return_value = 'testhost' mon_ctxt = context.MonContext() addresses = ['10.5.4.1 10.5.4.2 10.5.4.3'] @@ -190,7 +260,8 @@ class MonContextTest(CharmTestCase): 'frontend': 'beast', 'relaxed_s3_bucket_names': False, 'rgw_zonegroup': 'zonegroup1', - 'rgw_realm': 'realmX' + 'rgw_realm': 'realmX', + 'behind_https_proxy': False, } self.assertEqual(expect, mon_ctxt()) self.assertFalse(mock_ensure_rsv_v6.called) @@ -202,9 +273,13 @@ class MonContextTest(CharmTestCase): self.assertEqual(expect, mon_ctxt()) self.assertTrue(mock_ensure_rsv_v6.called) + @patch('charmhelpers.contrib.hahelpers.cluster.relation_ids') + @patch('charmhelpers.contrib.hahelpers.cluster.config_get') @patch.object(ceph, 'config', lambda *args: '{"client.radosgw.gateway": {"rgw init timeout": 60}}') - def test_ctxt_missing_data(self): + def test_ctxt_missing_data(self, mock_config_get, mock_relation_ids): + mock_relation_ids.return_value = [] + mock_config_get.side_effect = self.test_config.get self.socket.gethostname.return_value = 'testhost' mon_ctxt = context.MonContext() self.relation_get.return_value = None @@ -212,9 +287,13 @@ class MonContextTest(CharmTestCase): self.related_units.return_value = ['ceph/0', 'ceph/1', 'ceph/2'] self.assertEqual({}, mon_ctxt()) + @patch('charmhelpers.contrib.hahelpers.cluster.relation_ids') + @patch('charmhelpers.contrib.hahelpers.cluster.config_get') @patch.object(ceph, 'config', lambda *args: '{"client.radosgw.gateway": {"rgw init timeout": 60}}') - def test_ctxt_inconsistent_auths(self): + def test_ctxt_inconsistent_auths(self, mock_config_get, mock_relation_ids): + mock_relation_ids.return_value = [] + mock_config_get.side_effect = self.test_config.get self.socket.gethostname.return_value = 'testhost' mon_ctxt = context.MonContext() addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3'] @@ -253,13 +332,18 @@ class MonContextTest(CharmTestCase): 'frontend': 'beast', 'relaxed_s3_bucket_names': False, 'rgw_zonegroup': 'zonegroup1', - 'rgw_realm': 'realmX' + 'rgw_realm': 'realmX', + 'behind_https_proxy': False, } self.assertEqual(expect, mon_ctxt()) + @patch('charmhelpers.contrib.hahelpers.cluster.relation_ids') + @patch('charmhelpers.contrib.hahelpers.cluster.config_get') @patch.object(ceph, 'config', lambda *args: '{"client.radosgw.gateway": {"rgw init timeout": 60}}') - def test_ctxt_consistent_auths(self): + def test_ctxt_consistent_auths(self, mock_config_get, mock_relation_ids): + mock_relation_ids.return_value = [] + mock_config_get.side_effect = self.test_config.get self.socket.gethostname.return_value = 'testhost' mon_ctxt = context.MonContext() addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3'] @@ -298,7 +382,8 @@ class MonContextTest(CharmTestCase): 'frontend': 'beast', 'relaxed_s3_bucket_names': False, 'rgw_zonegroup': 'zonegroup1', - 'rgw_realm': 'realmX' + 'rgw_realm': 'realmX', + 'behind_https_proxy': False, } self.assertEqual(expect, mon_ctxt()) @@ -360,9 +445,13 @@ class MonContextTest(CharmTestCase): _test_version = '16.2.0' context.validate_http_frontend('beast') + @patch('charmhelpers.contrib.hahelpers.cluster.relation_ids') + @patch('charmhelpers.contrib.hahelpers.cluster.config_get') @patch.object(ceph, 'config', lambda *args: '{"client.radosgw.gateway": {"rgw init timeout": 60}}') - def test_ctxt_inconsistent_fsids(self): + def test_ctxt_inconsistent_fsids(self, mock_config_get, mock_relation_ids): + mock_relation_ids.return_value = [] + mock_config_get.side_effect = self.test_config.get self.socket.gethostname.return_value = 'testhost' mon_ctxt = context.MonContext() addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3'] @@ -401,7 +490,8 @@ class MonContextTest(CharmTestCase): 'frontend': 'beast', 'relaxed_s3_bucket_names': False, 'rgw_zonegroup': 'zonegroup1', - 'rgw_realm': 'realmX' + 'rgw_realm': 'realmX', + 'behind_https_proxy': False, } self.assertEqual(expect, mon_ctxt())