Repository: thelastpickle/cassandra-medusa

Issue:
Allow enabling/disabling SSL certificate verification
[Project board link](https://github.com/orgs/k8ssandra/projects/8/views/1?pane=issue&itemId=49576848)

We currently force SSL certificate verification as soon as HTTPs is enabled (via the storage.secure config setting).

It turned out there's an issue with this. It's not clear how to point medusa/boto/etc to use a custom certificate. It always uses the ones from the certify python package.

A proposed solution to this is to add a brand new setting, `ssl_verify` to control if we want to validate the certs or not.

Discussion:
sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=thelastpickle_cassandra-medusa&pullRequest=701) **Quality Gate failed**  
Failed conditions

[2 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=701&resolved=false&inNewCodePeriod=true)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=thelastpickle_cassandra-medusa&pullRequest=701)



Patch:
diff --git a/medusa-example.ini b/medusa-example.ini
index da5ad97..9d7b4d3 100644
--- a/medusa-example.ini
+++ b/medusa-example.ini
@@ -112,6 +112,10 @@ use_sudo_for_restore = True
 ; Configures the use of SSL to connect to the object storage system.
 ;secure = True
 
+; Enables verification of certificates used in case secure is set to True.
+; Enabling this is not yet supported - we don't have a good way to configure paths to custom certificates.
+; ssl_verify = False
+
 ;aws_cli_path = <Location of the aws cli binary if not in PATH>
 
 [monitoring]
diff --git a/medusa/config.py b/medusa/config.py
index d8ec0e7..60d1477 100644
--- a/medusa/config.py
+++ b/medusa/config.py
@@ -30,8 +30,8 @@ StorageConfig = collections.namedtuple(
     'StorageConfig',
     ['bucket_name', 'key_file', 'prefix', 'fqdn', 'host_file_separator', 'storage_provider',
      'base_path', 'max_backup_age', 'max_backup_count', 'api_profile', 'transfer_max_bandwidth',
-     'concurrent_transfers', 'multi_part_upload_threshold', 'host', 'region', 'port', 'secure', 'aws_cli_path',
-     'kms_id', 'backup_grace_period_in_days', 'use_sudo_for_restore', 'k8s_mode']
+     'concurrent_transfers', 'multi_part_upload_threshold', 'host', 'region', 'port', 'secure', 'ssl_verify',
+     'aws_cli_path', 'kms_id', 'backup_grace_period_in_days', 'use_sudo_for_restore', 'k8s_mode']
 )
 
 CassandraConfig = collections.namedtuple(
@@ -111,6 +111,7 @@ def _build_default_config():
         'concurrent_transfers': '1',
         'multi_part_upload_threshold': str(20 * 1024 * 1024),
         'secure': 'True',
+        'ssl_verify': 'False',      # False until we work out how to specify custom certs
         'aws_cli_path': 'aws',
         'fqdn': socket.getfqdn(),
         'region': 'default',
diff --git a/medusa/storage/s3_base_storage.py b/medusa/storage/s3_base_storage.py
index 8e5b51e..ad6648e 100644
--- a/medusa/storage/s3_base_storage.py
+++ b/medusa/storage/s3_base_storage.py
@@ -162,6 +162,7 @@ class S3BaseStorage(AbstractStorage):
     def _make_connection_arguments(self, config) -> t.Dict[str, str]:
 
         secure = config.secure or 'True'
+        ssl_verify = config.ssl_verify or 'False'   # False until we work out how to specify custom certs
         host = config.host
         port = config.port
 
@@ -175,7 +176,7 @@ class S3BaseStorage(AbstractStorage):
             s3_url = '{}://{}:{}'.format(protocol, host, port)
             return {
                 'endpoint_url': s3_url,
-                'verify': protocol == 'https'
+                'verify': ssl_verify.lower() == 'true'
             }
 
     def _make_transfer_config(self, config):


Test patch:
diff --git a/tests/storage/s3_storage_test.py b/tests/storage/s3_storage_test.py
index 0ae0719..acbaa6d 100644
--- a/tests/storage/s3_storage_test.py
+++ b/tests/storage/s3_storage_test.py
@@ -217,6 +217,7 @@ class S3StorageTest(unittest.TestCase):
                     'transfer_max_bandwidth': None,
                     'bucket_name': 'whatever-bucket',
                     'secure': 'True',
+                    'ssl_verify': 'False',
                     'host': None,
                     'port': None,
                     'concurrent_transfers': '1'
@@ -240,6 +241,7 @@ class S3StorageTest(unittest.TestCase):
                     'transfer_max_bandwidth': None,
                     'bucket_name': 'whatever-bucket',
                     'secure': 'False',
+                    'ssl_verify': 'False',
                     'host': None,
                     'port': None,
                     'concurrent_transfers': '1'
@@ -264,6 +266,7 @@ class S3StorageTest(unittest.TestCase):
                     'transfer_max_bandwidth': None,
                     'bucket_name': 'whatever-bucket',
                     'secure': 'True',
+                    'ssl_verify': 'False',
                     'host': 's3.example.com',
                     'port': '443',
                     'concurrent_transfers': '1'
@@ -286,6 +289,7 @@ class S3StorageTest(unittest.TestCase):
                     'transfer_max_bandwidth': None,
                     'bucket_name': 'whatever-bucket',
                     'secure': 'False',
+                    'ssl_verify': 'False',
                     'host': 's3.example.com',
                     'port': '8080',
                     'concurrent_transfers': '1'
@@ -296,6 +300,46 @@ class S3StorageTest(unittest.TestCase):
                     s3_storage.connection_extra_args['endpoint_url']
                 )
 
+    def test_make_connection_arguments_without_ssl_verify(self):
+        with patch('botocore.httpsession.URLLib3Session', return_value=_make_instance_metadata_mock()):
+            config = AttributeDict({
+                'storage_provider': 's3_compatible',
+                'region': 'default',
+                'key_file': '/tmp/whatever',
+                'api_profile': None,
+                'kms_id': None,
+                'transfer_max_bandwidth': None,
+                'bucket_name': 'whatever-bucket',
+                'secure': 'False',
+                'ssl_verify': 'False',
+                'host': 's3.example.com',
+                'port': '8080',
+                'concurrent_transfers': '1'
+            })
+            s3_storage = S3BaseStorage(config)
+            connection_args = s3_storage._make_connection_arguments(config)
+            self.assertEqual(False, connection_args['verify'])
+
+    def test_make_connection_arguments_with_ssl_verify(self):
+        with patch('botocore.httpsession.URLLib3Session', return_value=_make_instance_metadata_mock()):
+            config = AttributeDict({
+                'storage_provider': 's3_compatible',
+                'region': 'default',
+                'key_file': '/tmp/whatever',
+                'api_profile': None,
+                'kms_id': None,
+                'transfer_max_bandwidth': None,
+                'bucket_name': 'whatever-bucket',
+                'secure': 'False',
+                'ssl_verify': 'True',
+                'host': 's3.example.com',
+                'port': '8080',
+                'concurrent_transfers': '1'
+            })
+            s3_storage = S3BaseStorage(config)
+            connection_args = s3_storage._make_connection_arguments(config)
+            self.assertEqual(True, connection_args['verify'])
+
     def test_assume_role_authentication(self):
         with patch('botocore.httpsession.URLLib3Session', new=_make_assume_role_with_web_identity_mock()):
             if os.environ.get('AWS_ACCESS_KEY_ID', None):
@@ -328,6 +372,7 @@ class S3StorageTest(unittest.TestCase):
                 'transfer_max_bandwidth': None,
                 'bucket_name': 'whatever-bucket',
                 'secure': 'True',
+                'ssl_verify': 'False',
                 'host': None,
                 'port': None,
                 'concurrent_transfers': '1'