Repository: Azure/WALinuxAgent
Pull request #843 (2017-08-17 20:07:44)

Restrict access to Azure fabric services
Signed-off-by: Brendan Dixon <brendandixon@me.com>


Patch:
diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py
index 5dd9f702..75a02486 100644
--- a/azurelinuxagent/common/conf.py
+++ b/azurelinuxagent/common/conf.py
@@ -86,11 +86,13 @@ def load_conf_from_file(conf_file_path, conf=__conf__):
                                 "").format(conf_file_path, err))
 
 __SWITCH_OPTIONS__ = {
+    "OS.AllowHTTP" : False,
+    "OS.EnableFirewall" : False,
+    "OS.EnableFIPS" : False,
     "OS.EnableRDMA" : False,
     "OS.UpdateRdmaDriver" : False,
     "OS.CheckRdmaDriver" : False,
     "Logs.Verbose" : False,
-    "OS.EnableFIPS" : False,
     "Provisioning.Enabled" : True,
     "Provisioning.UseCloudInit" : False,
     "Provisioning.AllowResetSysUser" : False,
@@ -105,8 +107,7 @@ __SWITCH_OPTIONS__ = {
     "ResourceDisk.Format" : False,
     "ResourceDisk.EnableSwap" : False,
     "AutoUpdate.Enabled" : True,
-    "EnableOverProvisioning" : False,
-    "OS.AllowHTTP" : False
+    "EnableOverProvisioning" : False
 }
 
 __STRING_OPTIONS__ = {
@@ -149,6 +150,9 @@ def get_configuration(conf=__conf__):
 
     return options
 
+def enable_firewall(conf=__conf__):
+    return conf.get_switch("OS.EnableFirewall", False)
+
 def enable_rdma(conf=__conf__):
     return conf.get_switch("OS.EnableRDMA", False) or \
            conf.get_switch("OS.UpdateRdmaDriver", False) or \
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index ead13856..6fe49814 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -50,6 +50,10 @@ for all distros. Each concrete distro classes could overwrite default behavior
 if needed.
 """
 
+FIREWALL_ACCEPT = "iptables -t security -{0} OUTPUT -d {1} -p tcp -m owner --uid-owner {2} -j ACCEPT"
+FIREWALL_DROP = "iptables -t security -{0} OUTPUT -d {1} -p tcp -j DROP"
+FIREWALL_LIST = "iptables -t security -L"
+
 DMIDECODE_CMD = 'dmidecode --string system-uuid'
 PRODUCT_ID_FILE = '/sys/class/dmi/id/product_uuid'
 UUID_PATTERN = re.compile(
@@ -63,6 +67,40 @@ class DefaultOSUtil(object):
         self.selinux = None
         self.disable_route_warning = False
 
+    def enable_firewall(self, dst_ip=None, uid=None):
+        try:
+            if dst_ip is None or uid is None:
+                raise Exception("Missing arguments to enable_firewall")
+
+            # If either firewall rule exists, make no changes
+            accept_rule = FIREWALL_ACCEPT.format("C", dst_ip, uid)
+            drop_rule = FIREWALL_DROP.format("C", dst_ip)
+
+            if shellutil.run(accept_rule, chk_err=False) == 0 or \
+                shellutil.run(drop_rule, chk_err=False) == 0:
+                return
+
+            # Neither rule exists, append both rules
+            accept_rule = FIREWALL_ACCEPT.format("A", dst_ip, uid)
+            drop_rule = FIREWALL_DROP.format("A", dst_ip)
+
+            if shellutil.run(accept_rule) != 0:
+                logger.warn("Unable to add ACCEPT firewall rule '{0}'".format(
+                    accept_rule))
+                return
+
+            if shellutil.run(drop_rule) != 0:
+                logger.warn("Unable to add DROP firewall rule '{0}'".format(
+                    drop_rule))
+                return
+
+            rc, output = shellutil.run_get_output(FIREWALL_LIST)
+            logger.info("Successfully added firewall rules:\n{0}".format(
+                output))
+
+        except Exception as e:
+            logger.info("Unable to establish firewall: {0}".format(ustr(e)))
+
     def _correct_instance_id(self, id):
         '''
         Azure stores the instance ID with an incorrect byte ordering for the
diff --git a/azurelinuxagent/common/protocol/util.py b/azurelinuxagent/common/protocol/util.py
index bb3500a5..3071d7a5 100644
--- a/azurelinuxagent/common/protocol/util.py
+++ b/azurelinuxagent/common/protocol/util.py
@@ -16,11 +16,14 @@
 #
 # Requires Python 2.4+ and Openssl 1.0+
 #
+
+import errno
 import os
 import re
 import shutil
 import time
 import threading
+
 import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
 from azurelinuxagent.common.exception import ProtocolError, OSUtilError, \
@@ -231,6 +234,9 @@ class ProtocolUtil(object):
         try:
             os.remove(protocol_file_path)
         except IOError as e:
+            # Ignore file-not-found errors (since the file is being removed)
+            if e.errno == errno.ENOENT:
+                return
             logger.error("Failed to clear protocol endpoint: {0}", e)
 
     def get_protocol(self):
diff --git a/azurelinuxagent/common/utils/restutil.py b/azurelinuxagent/common/utils/restutil.py
index b671c312..39d21550 100644
--- a/azurelinuxagent/common/utils/restutil.py
+++ b/azurelinuxagent/common/utils/restutil.py
@@ -137,34 +137,33 @@ def _get_http_proxy(secure=False):
 
 def _http_request(method, host, rel_uri, port=None, data=None, secure=False,
                   headers=None, proxy_host=None, proxy_port=None):
-    url, conn = None, None
+
+    headers = {} if headers is None else headers
+    use_proxy = proxy_host is not None and proxy_port is not None
+
+    if port is None:
+        port = 443 if secure else 80
+
+    if use_proxy:
+        conn_host, conn_port = proxy_host, proxy_port
+        scheme = "https" if secure else "http"
+        url = "{0}://{1}:{2}{3}".format(scheme, host, port, rel_uri)
+
+    else:
+        conn_host, conn_port = host, port
+        url = rel_uri
+
     if secure:
-        port = 443 if port is None else port
-        if proxy_host is not None and proxy_port is not None:
-            conn = httpclient.HTTPSConnection(proxy_host,
-                                              proxy_port,
-                                              timeout=10)
+        conn = httpclient.HTTPSConnection(conn_host,
+                                        conn_port,
+                                        timeout=10)
+        if use_proxy:
             conn.set_tunnel(host, port)
-            # If proxy is used, full url is needed.
-            url = "https://{0}:{1}{2}".format(host, port, rel_uri)
-        else:
-            conn = httpclient.HTTPSConnection(host,
-                                              port,
-                                              timeout=10)
-            url = rel_uri
+
     else:
-        port = 80 if port is None else port
-        if proxy_host is not None and proxy_port is not None:
-            conn = httpclient.HTTPConnection(proxy_host,
-                                             proxy_port,
-                                             timeout=10)
-            # If proxy is used, full url is needed.
-            url = "http://{0}:{1}{2}".format(host, port, rel_uri)
-        else:
-            conn = httpclient.HTTPConnection(host,
-                                             port,
-                                             timeout=10)
-            url = rel_uri
+        conn = httpclient.HTTPConnection(conn_host,
+                                        conn_port,
+                                        timeout=10)
 
     logger.verbose("HTTP connection [{0}] [{1}] [{2}] [{3}]",
                    method,
@@ -172,10 +171,8 @@ def _http_request(method, host, rel_uri, port=None, data=None, secure=False,
                    data,
                    headers)
 
-    headers = {} if headers is None else headers
     conn.request(method=method, url=url, body=data, headers=headers)
-    resp = conn.getresponse()
-    return resp
+    return conn.getresponse()
 
 
 def http_request(method,
diff --git a/azurelinuxagent/ga/env.py b/azurelinuxagent/ga/env.py
index c81eed73..7273319c 100644
--- a/azurelinuxagent/ga/env.py
+++ b/azurelinuxagent/ga/env.py
@@ -27,6 +27,7 @@ import azurelinuxagent.common.logger as logger
 
 from azurelinuxagent.common.dhcp import get_dhcp_handler
 from azurelinuxagent.common.osutil import get_osutil
+from azurelinuxagent.common.protocol import get_protocol_util
 
 def get_env_handler():
     return EnvHandler()
@@ -42,6 +43,7 @@ class EnvHandler(object):
     def __init__(self):
         self.osutil = get_osutil()
         self.dhcp_handler = get_dhcp_handler()
+        self.protocol_util = get_protocol_util()
         self.stopped = True
         self.hostname = None
         self.dhcpid = None
@@ -64,17 +66,28 @@ class EnvHandler(object):
 
     def monitor(self):
         """
+        Monitor firewall rules
         Monitor dhcp client pid and hostname.
         If dhcp clinet process re-start has occurred, reset routes.
         """
+        protocol = self.protocol_util.get_protocol()
         while not self.stopped:
             self.osutil.remove_rules_files()
+
+            if conf.enable_firewall():
+                self.osutil.enable_firewall(
+                    dst_ip=protocol.endpoint,
+                    uid=os.getuid())
+
             timeout = conf.get_root_device_scsi_timeout()
             if timeout is not None:
                 self.osutil.set_scsi_disks_timeout(timeout)
+
             if conf.get_monitor_hostname():
                 self.handle_hostname_update()
+
             self.handle_dhclient_restart()
+
             time.sleep(5)
 
     def handle_hostname_update(self):
diff --git a/config/alpine/waagent.conf b/config/alpine/waagent.conf
index 2e3f6a5f..99495d51 100644
--- a/config/alpine/waagent.conf
+++ b/config/alpine/waagent.conf
@@ -81,3 +81,12 @@ OS.SshDir=/etc/ssh
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/arch/waagent.conf b/config/arch/waagent.conf
index 686b90c3..200a458e 100644
--- a/config/arch/waagent.conf
+++ b/config/arch/waagent.conf
@@ -107,3 +107,12 @@ OS.SshDir=/etc/ssh
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/bigip/waagent.conf b/config/bigip/waagent.conf
index a6a380b7..9ff6ee1f 100644
--- a/config/bigip/waagent.conf
+++ b/config/bigip/waagent.conf
@@ -76,4 +76,21 @@ OS.SshdConfigPath=/config/ssh/sshd_config
 OS.EnableRDMA=n
 
 # Enable or disable goal state processing auto-update, default is enabled
-AutoUpdate.Enabled=y
\ No newline at end of file
+AutoUpdate.Enabled=y
+
+# Determine the update family, this should not be changed
+# AutoUpdate.GAFamily=Prod
+
+# Determine if the overprovisioning feature is enabled. If yes, hold extension
+# handling until inVMArtifactsProfile.OnHold is false.
+# Default is disabled
+# EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/clearlinux/waagent.conf b/config/clearlinux/waagent.conf
index 6606cd73..81094257 100644
--- a/config/clearlinux/waagent.conf
+++ b/config/clearlinux/waagent.conf
@@ -79,3 +79,12 @@ AutoUpdate.GAFamily=Prod
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/coreos/waagent.conf b/config/coreos/waagent.conf
index ac19b5f2..cbb327fc 100644
--- a/config/coreos/waagent.conf
+++ b/config/coreos/waagent.conf
@@ -111,3 +111,8 @@ OS.OpensslPath=None
 # Allow fallback to HTTP if HTTPS is unavailable
 # Note: Allowing HTTP (vs. HTTPS) may cause security risks
 OS.AllowHTTP=y
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/freebsd/waagent.conf b/config/freebsd/waagent.conf
index 5149573e..6406c750 100644
--- a/config/freebsd/waagent.conf
+++ b/config/freebsd/waagent.conf
@@ -105,3 +105,12 @@ OS.SudoersDir=/usr/local/etc/sudoers.d
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/gaia/waagent.conf b/config/gaia/waagent.conf
index 75550a67..9c28ba3d 100644
--- a/config/gaia/waagent.conf
+++ b/config/gaia/waagent.conf
@@ -104,3 +104,12 @@ AutoUpdate.Enabled=n
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/openbsd/waagent.conf b/config/openbsd/waagent.conf
index ec47a786..a39a9a57 100644
--- a/config/openbsd/waagent.conf
+++ b/config/openbsd/waagent.conf
@@ -103,3 +103,12 @@ OS.PasswordPath=/etc/master.passwd
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/suse/waagent.conf b/config/suse/waagent.conf
index b2e90a88..ba50be6d 100644
--- a/config/suse/waagent.conf
+++ b/config/suse/waagent.conf
@@ -107,3 +107,12 @@ OS.SshDir=/etc/ssh
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/ubuntu/waagent.conf b/config/ubuntu/waagent.conf
index f9145ca3..71f2c043 100644
--- a/config/ubuntu/waagent.conf
+++ b/config/ubuntu/waagent.conf
@@ -95,3 +95,12 @@ OS.SshDir=/etc/ssh
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/config/waagent.conf b/config/waagent.conf
index bd11d461..99f54d67 100644
--- a/config/waagent.conf
+++ b/config/waagent.conf
@@ -108,3 +108,8 @@ OS.SshDir=/etc/ssh
 # Allow fallback to HTTP if HTTPS is unavailable
 # Note: Allowing HTTP (vs. HTTPS) may cause security risks
 # OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y


Test patch:
diff --git a/tests/common/osutil/test_default.py b/tests/common/osutil/test_default.py
index 9a2628e9..3013f65f 100644
--- a/tests/common/osutil/test_default.py
+++ b/tests/common/osutil/test_default.py
@@ -488,5 +488,82 @@ Match host 192.168.1.2\n\
         print("WRITING TO {0}".format(waagent_sudoers))
         self.assertEqual(1, count)
 
+    @patch('os.getuid', return_value=42)
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output')
+    @patch('azurelinuxagent.common.utils.shellutil.run')
+    def test_enable_firewall(self, mock_run, mock_output, mock_uid):
+        util = osutil.DefaultOSUtil()
+
+        dst = '1.2.3.4'
+        uid = 42
+
+        mock_run.side_effect = [1, 1, 0, 0]
+        mock_output.return_value = 0
+        util.enable_firewall(dst_ip=dst, uid=uid)
+
+        mock_run.assert_has_calls([
+            call(osutil.FIREWALL_ACCEPT.format("C", dst, uid), chk_err=False),
+            call(osutil.FIREWALL_DROP.format("C", dst), chk_err=False),
+            call(osutil.FIREWALL_ACCEPT.format("A", dst, uid)),
+            call(osutil.FIREWALL_DROP.format("A", dst))
+        ])
+        mock_output.assert_called_with(osutil.FIREWALL_LIST)
+
+    @patch('os.getuid', return_value=42)
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output')
+    @patch('azurelinuxagent.common.utils.shellutil.run')
+    def test_enable_firewall_skips_if_accept_exists(self, mock_run, mock_output, mock_uid):
+        util = osutil.DefaultOSUtil()
+
+        dst = '1.2.3.4'
+        uid = 42
+
+        mock_run.side_effect = [0, 1, 0, 0]
+        mock_output.return_value = 0
+        util.enable_firewall(dst_ip=dst, uid=uid)
+
+        mock_run.assert_has_calls([
+            call(osutil.FIREWALL_ACCEPT.format("C", dst, uid), chk_err=False)
+        ])
+        mock_output.assert_not_called()
+
+    @patch('os.getuid', return_value=42)
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output')
+    @patch('azurelinuxagent.common.utils.shellutil.run')
+    def test_enable_firewall_skips_if_drop_exists(self, mock_run, mock_output, mock_uid):
+        util = osutil.DefaultOSUtil()
+
+        dst = '1.2.3.4'
+        uid = 42
+
+        mock_run.side_effect = [1, 0, 0, 0]
+        mock_output.return_value = 0
+        util.enable_firewall(dst_ip=dst, uid=uid)
+
+        mock_run.assert_has_calls([
+            call(osutil.FIREWALL_ACCEPT.format("C", dst, uid), chk_err=False),
+            call(osutil.FIREWALL_DROP.format("C", dst), chk_err=False),
+        ])
+        mock_output.assert_not_called()
+
+    @patch('os.getuid', return_value=42)
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output')
+    @patch('azurelinuxagent.common.utils.shellutil.run')
+    def test_enable_firewall_ignores_exceptions(self, mock_run, mock_output, mock_uid):
+        util = osutil.DefaultOSUtil()
+
+        dst = '1.2.3.4'
+        uid = 42
+
+        mock_run.side_effect = [1, 1, Exception]
+        util.enable_firewall(dst_ip=dst, uid=uid)
+
+        mock_run.assert_has_calls([
+            call(osutil.FIREWALL_ACCEPT.format("C", dst, uid), chk_err=False),
+            call(osutil.FIREWALL_DROP.format("C", dst), chk_err=False),
+            call(osutil.FIREWALL_ACCEPT.format("A", dst, uid))
+        ])
+        mock_output.assert_not_called()
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/common/test_conf.py b/tests/common/test_conf.py
index 6a0beb0d..93759de1 100644
--- a/tests/common/test_conf.py
+++ b/tests/common/test_conf.py
@@ -63,7 +63,8 @@ class TestConf(AgentTestCase):
         "AutoUpdate.Enabled" : True,
         "AutoUpdate.GAFamily" : "Prod",
         "EnableOverProvisioning" : False,
-        "OS.AllowHTTP" : False
+        "OS.AllowHTTP" : False,
+        "OS.EnableFirewall" : True
     }
 
     def setUp(self):
diff --git a/tests/data/test_waagent.conf b/tests/data/test_waagent.conf
index c7bcc9e3..edc36769 100644
--- a/tests/data/test_waagent.conf
+++ b/tests/data/test_waagent.conf
@@ -112,3 +112,12 @@ OS.SshDir=/notareal/path
 # handling until inVMArtifactsProfile.OnHold is false.
 # Default is disabled
 # EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of exising VMs
+OS.EnableFirewall=y
diff --git a/tests/test_agent.py b/tests/test_agent.py
index e6622647..77be07a7 100644
--- a/tests/test_agent.py
+++ b/tests/test_agent.py
@@ -39,6 +39,7 @@ Logs.Verbose = False
 OS.AllowHTTP = False
 OS.CheckRdmaDriver = False
 OS.EnableFIPS = True
+OS.EnableFirewall = True
 OS.EnableRDMA = False
 OS.HomeDir = /home
 OS.OpensslPath = /usr/bin/openssl
diff --git a/tests/utils/test_rest_util.py b/tests/utils/test_rest_util.py
index ae1d1353..75c75f89 100644
--- a/tests/utils/test_rest_util.py
+++ b/tests/utils/test_rest_util.py
@@ -23,7 +23,7 @@ from azurelinuxagent.common.exception import BadRequestError, \
 import azurelinuxagent.common.utils.restutil as restutil
 
 from azurelinuxagent.common.future import httpclient, ustr
-from tests.tools import AgentTestCase, patch, Mock, MagicMock
+from tests.tools import *
 
 
 class TestHttpOperations(AgentTestCase):
@@ -118,46 +118,100 @@ class TestHttpOperations(AgentTestCase):
             self.assertEqual("foo.com", h)
             self.assertEqual(80, p)
 
-
     @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection")
     @patch("azurelinuxagent.common.future.httpclient.HTTPConnection")
-    def test_http_request(self, HTTPConnection, HTTPSConnection):
-        mock_http_conn = MagicMock()
-        mock_http_resp = MagicMock()
-        mock_http_conn.getresponse = Mock(return_value=mock_http_resp)
-        HTTPConnection.return_value = mock_http_conn
-        HTTPSConnection.return_value = mock_http_conn
-
-        mock_http_resp.read = Mock(return_value="_(:3| <)_")
-
-        # Test http get
-        resp = restutil._http_request("GET", "foo", "bar")
-        self.assertNotEquals(None, resp)
-        self.assertEquals("_(:3| <)_", resp.read())
-
-        # Test https get
-        resp = restutil._http_request("GET", "foo", "bar", secure=True)
+    def test_http_request_direct(self, HTTPConnection, HTTPSConnection):
+        mock_conn = \
+            MagicMock(getresponse=\
+                Mock(return_value=\
+                    Mock(read=Mock(return_value="TheResults"))))
+
+        HTTPConnection.return_value = mock_conn
+
+        resp = restutil._http_request("GET", "foo", "/bar")
+
+        HTTPConnection.assert_has_calls([
+            call("foo", 80, timeout=10)
+        ])
+        HTTPSConnection.assert_not_called()
+        mock_conn.request.assert_has_calls([
+            call(method="GET", url="/bar", body=None, headers={})
+        ])
+        mock_conn.getresponse.assert_called_once()
         self.assertNotEquals(None, resp)
-        self.assertEquals("_(:3| <)_", resp.read())
+        self.assertEquals("TheResults", resp.read())
 
-        # Test http get with proxy
-        mock_http_resp.read = Mock(return_value="_(:3| <)_")
-        resp = restutil._http_request("GET", "foo", "bar", proxy_host="foo.bar",
-                                      proxy_port=23333)
+    @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection")
+    @patch("azurelinuxagent.common.future.httpclient.HTTPConnection")
+    def test_http_request_direct_secure(self, HTTPConnection, HTTPSConnection):
+        mock_conn = \
+            MagicMock(getresponse=\
+                Mock(return_value=\
+                    Mock(read=Mock(return_value="TheResults"))))
+
+        HTTPSConnection.return_value = mock_conn
+
+        resp = restutil._http_request("GET", "foo", "/bar", secure=True)
+
+        HTTPConnection.assert_not_called()
+        HTTPSConnection.assert_has_calls([
+            call("foo", 443, timeout=10)
+        ])
+        mock_conn.request.assert_has_calls([
+            call(method="GET", url="/bar", body=None, headers={})
+        ])
+        mock_conn.getresponse.assert_called_once()
         self.assertNotEquals(None, resp)
-        self.assertEquals("_(:3| <)_", resp.read())
+        self.assertEquals("TheResults", resp.read())
 
-        # Test https get
-        resp = restutil._http_request("GET", "foo", "bar", secure=True)
+    @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection")
+    @patch("azurelinuxagent.common.future.httpclient.HTTPConnection")
+    def test_http_request_proxy(self, HTTPConnection, HTTPSConnection):
+        mock_conn = \
+            MagicMock(getresponse=\
+                Mock(return_value=\
+                    Mock(read=Mock(return_value="TheResults"))))
+
+        HTTPConnection.return_value = mock_conn
+
+        resp = restutil._http_request("GET", "foo", "/bar",
+                            proxy_host="foo.bar", proxy_port=23333)
+
+        HTTPConnection.assert_has_calls([
+            call("foo.bar", 23333, timeout=10)
+        ])
+        HTTPSConnection.assert_not_called()
+        mock_conn.request.assert_has_calls([
+            call(method="GET", url="http://foo:80/bar", body=None, headers={})
+        ])
+        mock_conn.getresponse.assert_called_once()
         self.assertNotEquals(None, resp)
-        self.assertEquals("_(:3| <)_", resp.read())
+        self.assertEquals("TheResults", resp.read())
 
-        # Test https get with proxy
-        mock_http_resp.read = Mock(return_value="_(:3| <)_")
-        resp = restutil._http_request("GET", "foo", "bar", proxy_host="foo.bar",
-                                      proxy_port=23333, secure=True)
+    @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection")
+    @patch("azurelinuxagent.common.future.httpclient.HTTPConnection")
+    def test_http_request_proxy_secure(self, HTTPConnection, HTTPSConnection):
+        mock_conn = \
+            MagicMock(getresponse=\
+                Mock(return_value=\
+                    Mock(read=Mock(return_value="TheResults"))))
+
+        HTTPSConnection.return_value = mock_conn
+
+        resp = restutil._http_request("GET", "foo", "/bar",
+                            proxy_host="foo.bar", proxy_port=23333,
+                            secure=True)
+
+        HTTPConnection.assert_not_called()
+        HTTPSConnection.assert_has_calls([
+            call("foo.bar", 23333, timeout=10)
+        ])
+        mock_conn.request.assert_has_calls([
+            call(method="GET", url="https://foo:443/bar", body=None, headers={})
+        ])
+        mock_conn.getresponse.assert_called_once()
         self.assertNotEquals(None, resp)
-        self.assertEquals("_(:3| <)_", resp.read())
+        self.assertEquals("TheResults", resp.read())
 
     @patch("time.sleep")
     @patch("azurelinuxagent.common.utils.restutil._http_request")