Repository: ansible/ansible

Issue:
Azure Stack module fails with self-signed certificates
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
azure_rm_storageaccount

##### ANSIBLE VERSION
```
ansible 2.4.2.0
  config file = None
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
```

##### CONFIGURATION

##### OS / ENVIRONMENT
CentOS Linux release 7.3.1611 (Core)

##### SUMMARY
After adding a trusted root certificate to centos the Azure plugin still fails to verify the certificate.

##### STEPS TO REPRODUCE
**Create an environment variable with the Azure Stack metadata endpoint**
```
export AZURE_CLOUD_ENVIRONMENT="https://management.local.azurestack.external/metadata/endpoints?api-version=2015-01-01"
```

**Validate that the certificate is not trusted with curl**
```
[root@f028d414edf2 /]# curl $AZURE_CLOUD_ENVIRONMENT
curl: (60) Peer's Certificate issuer is not recognized.
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.
```

**Install the root certificate in the trusted CA store**
```
[root@f028d414edf2 /]# mv /tmp/azurestack.cer /etc/pki/ca-trust/source/anchors/azurestack.cer
[root@f028d414edf2 /]# update-ca-trust extract
```

**Validate that the certificate is now trusted by Centos**
```
[root@f028d414edf2 /]# curl $AZURE_CLOUD_ENVIRONMENT
{"galleryEndpoint":"https://portal.local.azurestack.external:30015/","graphEndpoint":"https://graph.windows.net/","portalEndpoint":"https://portal.local.azurestack.external/","authentication":{"loginEndpoint":"https://login.windows.net/","audiences":["https://management.charliejllewellyngmail.onmicrosoft.com/7ac7a838-c3a3-4b6b-9858-71cf0d7240cd"]}}
```

**Run the playbook**
```
PLAY [localhost] ***************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Create storage account] **************************************************************************************************************************************************************************************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: Please ensure you have network connection. Error detail: HTTPSConnectionPool(host='management.local.azurestack.external', port=443): Max retries exceeded with url: /metadata/endpoints?api-version=2015-01-01/metadata/endpoints?api-version=2015-01-01 (Caused by SSLError(SSLError("bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)",),))
fatal: [localhost]: FAILED! => {"changed": false, "msg": "cloud_environment https://management.local.azurestack.external/metadata/endpoints?api-version=2015-01-01 could not be resolved: Unable to get endpoints from the cloud.\nPlease ensure you have network connection. Error detail: HTTPSConnectionPool(host='management.local.azurestack.external', port=443): Max retries exceeded with url: /metadata/endpoints?api-version=2015-01-01/metadata/endpoints?api-version=2015-01-01 (Caused by SSLError(SSLError(\"bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)\",),))"}
	to retry, use: --limit @/test.retry

PLAY RECAP *********************************************************************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1
```

<!--- Paste example playbooks or commands between quotes below -->
```
---
- hosts: localhost
  tasks:
  - name: Create storage account
    azure_rm_storageaccount:
      resource_group: Testing
      name: testaccount001
      account_type: Standard_LRS
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
I expected the certificate to be trusted and create a storage account

##### ACTUAL RESULTS
The certificate verification failed

```
PLAYBOOK: test.yaml ************************************************************************************************************************************************************************************************************************
1 plays in test.yaml

PLAY [localhost] ***************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************************************
Using module file /usr/lib/python2.7/site-packages/ansible/modules/system/setup.py
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: root
<127.0.0.1> EXEC /bin/sh -c 'echo ~ && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp/ansible-tmp-1512128613.81-45687956999877 `" && echo ansible-tmp-1512128613.81-45687956999877="` echo /root/.ansible/tmp/ansible-tmp-1512128613.81-45687956999877 `" ) && sleep 0'
<127.0.0.1> PUT /tmp/tmpLaEe9H TO /root/.ansible/tmp/ansible-tmp-1512128613.81-45687956999877/setup.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1512128613.81-45687956999877/ /root/.ansible/tmp/ansible-tmp-1512128613.81-45687956999877/setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python2 /root/.ansible/tmp/ansible-tmp-1512128613.81-45687956999877/setup.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1512128613.81-45687956999877/" > /dev/null 2>&1 && sleep 0'
ok: [localhost]
META: ran handlers

TASK [Create storage account] **************************************************************************************************************************************************************************************************************
task path: /test.yaml:4
Using module file /usr/lib/python2.7/site-packages/ansible/modules/cloud/azure/azure_rm_storageaccount.py
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: root
<127.0.0.1> EXEC /bin/sh -c 'echo ~ && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp/ansible-tmp-1512128614.68-124802220519587 `" && echo ansible-tmp-1512128614.68-124802220519587="` echo /root/.ansible/tmp/ansible-tmp-1512128614.68-124802220519587 `" ) && sleep 0'
<127.0.0.1> PUT /tmp/tmpZfm_PM TO /root/.ansible/tmp/ansible-tmp-1512128614.68-124802220519587/azure_rm_storageaccount.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1512128614.68-124802220519587/ /root/.ansible/tmp/ansible-tmp-1512128614.68-124802220519587/azure_rm_storageaccount.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python2 /root/.ansible/tmp/ansible-tmp-1512128614.68-124802220519587/azure_rm_storageaccount.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1512128614.68-124802220519587/" > /dev/null 2>&1 && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_CDk4HC/ansible_modlib.zip/ansible/module_utils/azure_rm_common.py", line 249, in __init__
    self._cloud_environment = azure_cloud.get_cloud_from_metadata_endpoint(raw_cloud_env)
  File "/usr/lib/python2.7/site-packages/msrestazure/azure_cloud.py", line 246, in get_cloud_from_metadata_endpoint
    _populate_from_metadata_endpoint(cloud, arm_endpoint)
  File "/usr/lib/python2.7/site-packages/msrestazure/azure_cloud.py", line 223, in _populate_from_metadata_endpoint
    raise MetadataEndpointError(error_msg_fmt.format(msg))
MetadataEndpointError: Unable to get endpoints from the cloud.
Please ensure you have network connection. Error detail: HTTPSConnectionPool(host='management.local.azurestack.external', port=443): Max retries exceeded with url: /metadata/endpoints?api-version=2015-01-01/metadata/endpoints?api-version=2015-01-01 (Caused by SSLError(SSLError("bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)",),))

fatal: [localhost]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "access_tier": null,
            "account_type": "Standard_LRS",
            "ad_user": null,
            "append_tags": true,
            "cli_default_profile": null,
            "client_id": null,
            "cloud_environment": null,
            "custom_domain": null,
            "force": false,
            "kind": "Storage",
            "location": null,
            "name": "testaccount001",
            "password": null,
            "profile": null,
            "resource_group": "Testing",
            "secret": null,
            "state": "present",
            "subscription_id": null,
            "tags": null,
            "tenant": null
        }
    },
    "msg": "cloud_environment https://management.local.azurestack.external/metadata/endpoints?api-version=2015-01-01 could not be resolved: Unable to get endpoints from the cloud.\nPlease ensure you have network connection. Error detail: HTTPSConnectionPool(host='management.local.azurestack.external', port=443): Max retries exceeded with url: /metadata/endpoints?api-version=2015-01-01/metadata/endpoints?api-version=2015-01-01 (Caused by SSLError(SSLError(\"bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)\",),))"
}
	to retry, use: --limit @/test.retry

PLAY RECAP *********************************************************************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1
```

<!--- Paste verbatim command output between quotes below -->
```
ansible-playbook test.yaml
```



Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
This could actually be caused by the azure-python-SDK, I have made a comment on this similar issue, https://github.com/Azure/azure-sdk-for-python/issues/1384. However we would still need Ansible to allow us to specify we do not want certs verified.
Files identified in the description:
* [lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @chouseknecht @haroldwongms @nitzmahone @tstringer @xscript @yuwzho @zikalino
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/module_utils/azure_rm_common.py b/lib/ansible/module_utils/azure_rm_common.py
--- a/lib/ansible/module_utils/azure_rm_common.py
+++ b/lib/ansible/module_utils/azure_rm_common.py
@@ -44,6 +44,7 @@
     ad_user=dict(type='str', no_log=True),
     password=dict(type='str', no_log=True),
     cloud_environment=dict(type='str'),
+    cert_validation_mode=dict(type='str', choices=['validate', 'ignore'])
     # debug=dict(type='bool', default=False),
 )
 
@@ -57,6 +58,7 @@
     ad_user='AZURE_AD_USER',
     password='AZURE_PASSWORD',
     cloud_environment='AZURE_CLOUD_ENVIRONMENT',
+    cert_validation_mode='AZURE_CERT_VALIDATION_MODE',
 )
 
 AZURE_TAG_ARGS = dict(
@@ -255,6 +257,13 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
             self.fail("Failed to get credentials. Either pass as parameters, set environment variables, "
                       "or define a profile in ~/.azure/credentials or be logged using AzureCLI.")
 
+        # cert validation mode precedence: module-arg, credential profile, env, "validate"
+        self._cert_validation_mode = self.module.params['cert_validation_mode'] or self.credentials.get('cert_validation_mode') or \
+            os.environ.get('AZURE_CERT_VALIDATION_MODE') or 'validate'
+
+        if self._cert_validation_mode not in ['validate', 'ignore']:
+            self.fail('invalid cert_validation_mode: {0}'.format(self._cert_validation_mode))
+
         # if cloud_environment specified, look up/build Cloud object
         raw_cloud_env = self.credentials.get('cloud_environment')
         if not raw_cloud_env:
@@ -286,7 +295,8 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
             self.azure_credentials = ServicePrincipalCredentials(client_id=self.credentials['client_id'],
                                                                  secret=self.credentials['secret'],
                                                                  tenant=self.credentials['tenant'],
-                                                                 cloud_environment=self._cloud_environment)
+                                                                 cloud_environment=self._cloud_environment,
+                                                                 verify=self._cert_validation_mode == 'validate')
 
         elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None:
             tenant = self.credentials.get('tenant')
@@ -296,7 +306,8 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
             self.azure_credentials = UserPassCredentials(self.credentials['ad_user'],
                                                          self.credentials['password'],
                                                          tenant=tenant,
-                                                         cloud_environment=self._cloud_environment)
+                                                         cloud_environment=self._cloud_environment,
+                                                         verify=self._cert_validation_mode == 'validate')
         else:
             self.fail("Failed to authenticate with provided credentials. Some attributes were missing. "
                       "Credentials must include client_id, secret and tenant or ad_user and password or "
@@ -718,6 +729,10 @@ def create_default_securitygroup(self, resource_group, location, name, os_type,
 
         return self.get_poller_result(poller)
 
+    @staticmethod
+    def _validation_ignore_callback(session, global_config, local_config, **kwargs):
+        session.verify = False
+
     def get_mgmt_svc_client(self, client_type, base_url=None, api_version=None):
         self.log('Getting management service client {0}'.format(client_type.__name__))
         self.check_client_version(client_type)
@@ -740,6 +755,9 @@ def get_mgmt_svc_client(self, client_type, base_url=None, api_version=None):
         if VSCODEEXT_USER_AGENT_KEY in os.environ:
             client.config.add_user_agent(os.environ[VSCODEEXT_USER_AGENT_KEY])
 
+        if self._cert_validation_mode == 'ignore':
+            client.config.session_configuration_callback = self._validation_ignore_callback
+
         return client
 
     @property
diff --git a/lib/ansible/utils/module_docs_fragments/azure.py b/lib/ansible/utils/module_docs_fragments/azure.py
--- a/lib/ansible/utils/module_docs_fragments/azure.py
+++ b/lib/ansible/utils/module_docs_fragments/azure.py
@@ -67,6 +67,15 @@ class ModuleDocFragment(object):
               C(AzureUSGovernment)), or a metadata discovery endpoint URL (required for Azure Stack). Can also be set via credential file profile or
               the C(AZURE_CLOUD_ENVIRONMENT) environment variable.
         default: AzureCloud
+        version_added: 2.4
+    cert_validation_mode:
+        description:
+            - Controls the certificate validation behavior for Azure endpoints. By default, all modules will validate the server certificate, but
+              when an HTTPS proxy is in use, or against Azure Stack, it may be necessary to disable this behavior by passing C(ignore). Can also be
+              set via credential file profile or the C(AZURE_CERT_VALIDATION) environment variable.
+        choices: [validate, ignore]
+        default: null
+        version_added: 2.5
 requirements:
     - "python >= 2.7"
     - "azure >= 2.0.0"


Test patch: