Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 78083200B76 for ; Tue, 30 Aug 2016 19:13:44 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id 7674A160ABA; Tue, 30 Aug 2016 17:13:44 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 4C8DE160AAF for ; Tue, 30 Aug 2016 19:13:43 +0200 (CEST) Received: (qmail 68266 invoked by uid 500); 30 Aug 2016 17:13:42 -0000 Mailing-List: contact commits-help@cloudstack.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@cloudstack.apache.org Delivered-To: mailing list commits@cloudstack.apache.org Received: (qmail 68257 invoked by uid 99); 30 Aug 2016 17:13:42 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 30 Aug 2016 17:13:42 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 59CF0E0200; Tue, 30 Aug 2016 17:13:42 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: bhaisaab@apache.org To: commits@cloudstack.apache.org Date: Tue, 30 Aug 2016 17:13:42 -0000 Message-Id: X-Mailer: ASF-Git Admin Mailer Subject: [1/2] git commit: updated refs/heads/4.9 to 3e6f49d archived-at: Tue, 30 Aug 2016 17:13:44 -0000 Repository: cloudstack Updated Branches: refs/heads/4.9 0671a8097 -> 3e6f49d9e CLOUDSTACK-6432: Prevent DNS reflection attacks DNS on VR should not be publically accessible as it may be prone to DNS amplification/reflection attacks. This fixes the issue by only allowing VR DNS (port 53) to be accessible from guest network cidr, as per the fix in: https://issues.apache.org/jira/browse/CLOUDSTACK-6432 - Only allows guest network cidrs to query VR DNS on port 53. - Includes marvin smoke test that checks the VR DNS accessibility checks from guest and non-guest network. - Fixes Marvin sshClient to avoid using ssh agent when password is provided, previous some environments may have seen 'No existing session' exception without this fix. - Adds a new dnspython dependency that is used to perform dns resolutions in the tests. Signed-off-by: Rohit Yadav Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/14504dc7 Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/14504dc7 Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/14504dc7 Branch: refs/heads/4.9 Commit: 14504dc7e3ef6ba5f63b39ecd7454498263cf66a Parents: b9801ef Author: Rohit Yadav Authored: Mon Aug 22 10:31:41 2016 +0100 Committer: Rohit Yadav Committed: Tue Aug 30 22:39:33 2016 +0530 ---------------------------------------------------------------------- .../debian/config/opt/cloud/bin/cs/CsAddress.py | 24 +- test/integration/smoke/test_router_dns.py | 268 +++++++++++++++++++ tools/marvin/marvin/sshClient.py | 3 +- tools/marvin/setup.py | 1 + 4 files changed, 282 insertions(+), 14 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cloudstack/blob/14504dc7/systemvm/patches/debian/config/opt/cloud/bin/cs/CsAddress.py ---------------------------------------------------------------------- diff --git a/systemvm/patches/debian/config/opt/cloud/bin/cs/CsAddress.py b/systemvm/patches/debian/config/opt/cloud/bin/cs/CsAddress.py index f78ec4c..64ddb26 100755 --- a/systemvm/patches/debian/config/opt/cloud/bin/cs/CsAddress.py +++ b/systemvm/patches/debian/config/opt/cloud/bin/cs/CsAddress.py @@ -394,12 +394,13 @@ class CsIP: self.fw.append(["filter", "", "-A INPUT -i lo -j ACCEPT"]) if self.get_type() in ["guest"]: + guestNetworkCidr = self.address['network'] self.fw.append( ["filter", "", "-A INPUT -i %s -p udp -m udp --dport 67 -j ACCEPT" % self.dev]) self.fw.append( - ["filter", "", "-A INPUT -i %s -p udp -m udp --dport 53 -j ACCEPT" % self.dev]) + ["filter", "", "-A INPUT -i %s -p udp -m udp --dport 53 -s %s -j ACCEPT" % (self.dev, guestNetworkCidr)]) self.fw.append( - ["filter", "", "-A INPUT -i %s -p tcp -m tcp --dport 53 -j ACCEPT" % self.dev]) + ["filter", "", "-A INPUT -i %s -p tcp -m tcp --dport 53 -s %s -j ACCEPT" % (self.dev, guestNetworkCidr)]) self.fw.append( ["filter", "", "-A INPUT -i %s -p tcp -m tcp --dport 80 -m state --state NEW -j ACCEPT" % self.dev]) self.fw.append( @@ -436,8 +437,9 @@ class CsIP: self.fw.append(["filter", "", "-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT"]) if self.get_type() in ["guest"]: + guestNetworkCidr = self.address['network'] self.fw.append(["filter", "", "-A FORWARD -d %s -o %s -j ACL_INBOUND_%s" % - (self.address['network'], self.dev, self.dev)]) + (guestNetworkCidr, self.dev, self.dev)]) self.fw.append( ["filter", "front", "-A ACL_INBOUND_%s -d 224.0.0.18/32 -j ACCEPT" % self.dev]) self.fw.append( @@ -452,9 +454,9 @@ class CsIP: self.fw.append( ["filter", "", "-A INPUT -i %s -p udp -m udp --dport 67 -j ACCEPT" % self.dev]) self.fw.append( - ["filter", "", "-A INPUT -i %s -p udp -m udp --dport 53 -j ACCEPT" % self.dev]) + ["filter", "", "-A INPUT -i %s -p udp -m udp --dport 53 -s %s -j ACCEPT" % (self.dev, guestNetworkCidr)]) self.fw.append( - ["filter", "", "-A INPUT -i %s -p tcp -m tcp --dport 53 -j ACCEPT" % self.dev]) + ["filter", "", "-A INPUT -i %s -p tcp -m tcp --dport 53 -s %s -j ACCEPT" % (self.dev, guestNetworkCidr)]) self.fw.append( ["filter", "", "-A INPUT -i %s -p tcp -m tcp --dport 80 -m state --state NEW -j ACCEPT" % self.dev]) @@ -462,20 +464,16 @@ class CsIP: ["filter", "", "-A INPUT -i %s -p tcp -m tcp --dport 8080 -m state --state NEW -j ACCEPT" % self.dev]) self.fw.append(["mangle", "", "-A PREROUTING -m state --state NEW -i %s -s %s ! -d %s/32 -j ACL_OUTBOUND_%s" % - (self.dev, self.address[ - 'network'], self.address['gateway'], self.dev) - ]) + (self.dev, guestNetworkCidr, self.address['gateway'], self.dev)]) self.fw.append(["", "front", "-A NETWORK_STATS_%s -i %s -d %s" % - ("eth1", "eth1", self.address['network'])]) + ("eth1", "eth1", guestNetworkCidr)]) self.fw.append(["", "front", "-A NETWORK_STATS_%s -o %s -s %s" % - ("eth1", "eth1", self.address['network'])]) + ("eth1", "eth1", guestNetworkCidr)]) self.fw.append(["nat", "front", "-A POSTROUTING -s %s -o %s -j SNAT --to-source %s" % - (self.address['network'], self.dev, - self.address['public_ip']) - ]) + (guestNetworkCidr, self.dev, self.address['public_ip'])]) if self.get_type() in ["public"]: self.fw.append(["", "front", http://git-wip-us.apache.org/repos/asf/cloudstack/blob/14504dc7/test/integration/smoke/test_router_dns.py ---------------------------------------------------------------------- diff --git a/test/integration/smoke/test_router_dns.py b/test/integration/smoke/test_router_dns.py new file mode 100644 index 0000000..ef77224 --- /dev/null +++ b/test/integration/smoke/test_router_dns.py @@ -0,0 +1,268 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import logging +import dns.resolver + +from nose.plugins.attrib import attr +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.utils import cleanup_resources +from marvin.lib.base import (ServiceOffering, + VirtualMachine, + Account, + NATRule, + FireWallRule, + NetworkOffering, + Network) +from marvin.lib.common import (get_zone, + get_template, + get_domain, + list_routers, + list_nat_rules, + list_publicIP) + + +class TestRouterDns(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.logger = logging.getLogger('TestRouterDns') + cls.stream_handler = logging.StreamHandler() + cls.logger.setLevel(logging.DEBUG) + cls.logger.addHandler(cls.stream_handler) + + cls.testClient = super(TestRouterDns, cls).getClsTestClient() + cls.api_client = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + + cls.domain = get_domain(cls.api_client) + cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) + cls.services['mode'] = cls.zone.networktype + cls.template = get_template( + cls.api_client, + cls.zone.id, + cls.services["ostype"] + ) + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + + cls.logger.debug("Creating Admin Account for domain %s on zone %s" % (cls.domain.id, cls.zone.id)) + cls.account = Account.create( + cls.api_client, + cls.services["account"], + admin=True, + domainid=cls.domain.id + ) + + cls.logger.debug("Creating Service Offering on zone %s" % (cls.zone.id)) + cls.service_offering = ServiceOffering.create( + cls.api_client, + cls.services["service_offering"] + ) + + cls.logger.debug("Creating Network Offering on zone %s" % (cls.zone.id)) + cls.services["isolated_network_offering"]["egress_policy"] = "true" + cls.network_offering = NetworkOffering.create(cls.api_client, + cls.services["isolated_network_offering"], + conservemode=True) + cls.network_offering.update(cls.api_client, state='Enabled') + + cls.logger.debug("Creating Network for Account %s using offering %s" % (cls.account.name, cls.network_offering.id)) + cls.network = Network.create(cls.api_client, + cls.services["network"], + accountid=cls.account.name, + domainid=cls.account.domainid, + networkofferingid=cls.network_offering.id, + zoneid=cls.zone.id) + + cls.logger.debug("Creating guest VM for Account %s using offering %s" % (cls.account.name, cls.service_offering.id)) + cls.vm = VirtualMachine.create(cls.api_client, + cls.services["virtual_machine"], + templateid=cls.template.id, + accountid=cls.account.name, + domainid=cls.domain.id, + serviceofferingid=cls.service_offering.id, + networkids=[str(cls.network.id)]) + cls.vm.password = "password" + + cls.services["natrule1"] = { + "privateport": 22, + "publicport": 22, + "protocol": "TCP" + } + + cls.services["configurableData"] = { + "host": { + "password": "password", + "username": "root", + "port": 22 + }, + "input": "INPUT", + "forward": "FORWARD" + } + + cls._cleanup = [ + cls.vm, + cls.network, + cls.network_offering, + cls.service_offering, + cls.account + ] + + + @classmethod + def tearDownClass(cls): + try: + cleanup_resources(cls.api_client, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.cleanup = [] + + + def tearDown(self): + try: + cleanup_resources(self.apiclient, self.cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + + def test_router_common(self): + """Performs common router tests and returns router public_ips""" + + routers = list_routers( + self.apiclient, + account=self.account.name, + domainid=self.account.domainid + ) + + self.assertEqual( + isinstance(routers, list), + True, + "Check for list routers response return valid data" + ) + + self.assertTrue( + len(routers) >= 1, + "Check list router response" + ) + + router = routers[0] + + self.assertEqual( + router.state, + 'Running', + "Check list router response for router state" + ) + + public_ips = list_publicIP( + self.apiclient, + account=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id + ) + + self.assertEqual( + isinstance(public_ips, list), + True, + "Check for list public IPs response return valid data" + ) + + self.assertTrue( + len(public_ips) >= 1, + "Check public IP list has at least one IP" + ) + + return public_ips + + + @attr(tags=["advanced", "advancedns", "ssh"], required_hardware="true") + def test_router_dns_externalipquery(self): + """Checks that non-guest network IPs cannot access VR DNS""" + + self.logger.debug("Starting test_router_dns_externalips...") + + public_ip = self.test_router_common()[0] + + self.logger.debug("Querying VR DNS IP: " + public_ip.ipaddress) + resolver = dns.resolver.Resolver() + resolver.namerservers = [public_ip.ipaddress] + try: + resolver.query('google.com', 'A') + self.fail("Non-guest network IPs are able to access VR DNS, failing.") + except: + self.logger.debug("VR DNS query failed from non-guest network IP as expected") + + + @attr(tags=["advanced", "advancedns", "ssh"], required_hardware="true") + def test_router_dns_guestipquery(self): + """Checks that guest VM can query VR DNS""" + + self.logger.debug("Starting test_router_dns_guestipquery...") + public_ip = self.test_router_common()[0] + + self.logger.debug("Creating Firewall rule for VM ID: %s" % self.vm.id) + FireWallRule.create( + self.apiclient, + ipaddressid=public_ip.id, + protocol=self.services["natrule1"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["natrule1"]["publicport"], + endport=self.services["natrule1"]["publicport"] + ) + + self.logger.debug("Creating NAT rule for VM ID: %s" % self.vm.id) + nat_rule1 = NATRule.create( + self.apiclient, + self.vm, + self.services["natrule1"], + public_ip.id + ) + nat_rules = list_nat_rules( + self.apiclient, + id=nat_rule1.id + ) + self.assertEqual( + isinstance(nat_rules, list), + True, + "Check for list NAT rules response return valid data" + ) + self.assertTrue( + len(nat_rules) >= 1, + "Check for list NAT rules to have at least one rule" + ) + self.assertEqual( + nat_rules[0].state, + 'Active', + "Check list port forwarding rules" + ) + + result = None + try: + self.logger.debug("SSH into guest VM with IP: %s" % nat_rule1.ipaddress) + ssh = self.vm.get_ssh_client(ipaddress=nat_rule1.ipaddress, port=self.services['natrule1']["publicport"], retries=8) + result = str(ssh.execute("nslookup google.com")) + except Exception as e: + self.fail("Failed to SSH into VM - %s due to exception: %s" % (nat_rule1.ipaddress, e)) + + if not result: + self.fail("Did not to receive any response from the guest VM, failing.") + + self.assertTrue("google.com" in result and "#53" in result, + "VR DNS should serve requests from guest network, unable to get valid nslookup result from guest VM.") http://git-wip-us.apache.org/repos/asf/cloudstack/blob/14504dc7/tools/marvin/marvin/sshClient.py ---------------------------------------------------------------------- diff --git a/tools/marvin/marvin/sshClient.py b/tools/marvin/marvin/sshClient.py index e481109..e872256 100644 --- a/tools/marvin/marvin/sshClient.py +++ b/tools/marvin/marvin/sshClient.py @@ -118,7 +118,8 @@ class SshClient(object): port=self.port, username=self.user, password=self.passwd, - timeout=self.timeout) + timeout=self.timeout, + allow_agent=False) else: self.ssh.connect(hostname=self.host, port=self.port, http://git-wip-us.apache.org/repos/asf/cloudstack/blob/14504dc7/tools/marvin/setup.py ---------------------------------------------------------------------- diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py index 265d3c6..7aa7582 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -53,6 +53,7 @@ setup(name="Marvin", "ddt >= 0.4.0", "pyvmomi >= 5.5.0", "netaddr >= 0.7.14", + "dnspython", "ipmisim >= 0.7" ], py_modules=['marvin.marvinPlugin'],