Merge pull request #843 from brendandixon/master

Restrict access to Azure fabric services
This commit is contained in:
brendandixon
2017-08-17 13:24:47 -07:00
committed by GitHub
21 changed files with 364 additions and 65 deletions
+7 -3
View File
@@ -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 \
+38
View File
@@ -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
+6
View File
@@ -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):
+27 -30
View File
@@ -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
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.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
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:
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_host, conn_port = host, port
url = rel_uri
if secure:
conn = httpclient.HTTPSConnection(conn_host,
conn_port,
timeout=10)
if use_proxy:
conn.set_tunnel(host, port)
else:
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,
+13
View File
@@ -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):
+9
View File
@@ -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
+9
View File
@@ -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
+18 -1
View File
@@ -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
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
+9
View File
@@ -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
+5
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+5
View File
@@ -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
+77
View File
@@ -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()
+2 -1
View File
@@ -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):
+9
View File
@@ -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
+1
View File
@@ -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
+84 -30
View File
@@ -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_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("TheResults", resp.read())
@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
def test_http_request_direct_secure(self, HTTPConnection, HTTPSConnection):
mock_conn = \
MagicMock(getresponse=\
Mock(return_value=\
Mock(read=Mock(return_value="TheResults"))))
mock_http_resp.read = Mock(return_value="_(:3| <)_")
HTTPSConnection.return_value = mock_conn
# Test http get
resp = restutil._http_request("GET", "foo", "bar")
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)
self.assertNotEquals(None, resp)
self.assertEquals("_(:3| <)_", resp.read())
@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"))))
# 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)
self.assertNotEquals(None, resp)
self.assertEquals("_(:3| <)_", resp.read())
HTTPConnection.return_value = mock_conn
# Test https get
resp = restutil._http_request("GET", "foo", "bar", secure=True)
self.assertNotEquals(None, resp)
self.assertEquals("_(:3| <)_", resp.read())
resp = restutil._http_request("GET", "foo", "/bar",
proxy_host="foo.bar", proxy_port=23333)
# 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)
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())
@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("TheResults", resp.read())
@patch("time.sleep")
@patch("azurelinuxagent.common.utils.restutil._http_request")