Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(provider/aws): Enable optional AWS Shield protection on ELB & ALBs #2089

Merged
merged 2 commits into from
Nov 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class UpsertAmazonLoadBalancerDescription extends AbstractAmazonCredentia
private List<String> securityGroups;
private Map<String, List<String>> availabilityZones;

private boolean shieldProtectionEnabled = true;

public AmazonLoadBalancerType getLoadBalancerType() {
return loadBalancerType;
}
Expand Down Expand Up @@ -96,4 +98,12 @@ public Map<String, List<String>> getAvailabilityZones() {
public void setAvailabilityZones(Map<String, List<String>> availabilityZones) {
this.availabilityZones = availabilityZones;
}

public boolean getShieldProtectionEnabled() {
return shieldProtectionEnabled;
}

public void setShieldProtectionEnabled(boolean shieldProtectionEnabled) {
this.shieldProtectionEnabled = shieldProtectionEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,31 @@ package com.netflix.spinnaker.clouddriver.aws.deploy.handlers

import com.amazonaws.AmazonServiceException
import com.amazonaws.services.elasticloadbalancingv2.AmazonElasticLoadBalancing
import com.amazonaws.services.elasticloadbalancingv2.model.*
import com.netflix.spinnaker.clouddriver.aws.data.ArnUtils
import com.amazonaws.services.elasticloadbalancingv2.model.Action
import com.amazonaws.services.elasticloadbalancingv2.model.CreateListenerRequest
import com.amazonaws.services.elasticloadbalancingv2.model.CreateListenerResult
import com.amazonaws.services.elasticloadbalancingv2.model.CreateLoadBalancerRequest
import com.amazonaws.services.elasticloadbalancingv2.model.CreateRuleRequest
import com.amazonaws.services.elasticloadbalancingv2.model.CreateTargetGroupRequest
import com.amazonaws.services.elasticloadbalancingv2.model.CreateTargetGroupResult
import com.amazonaws.services.elasticloadbalancingv2.model.DeleteListenerRequest
import com.amazonaws.services.elasticloadbalancingv2.model.DeleteTargetGroupRequest
import com.amazonaws.services.elasticloadbalancingv2.model.DescribeListenersRequest
import com.amazonaws.services.elasticloadbalancingv2.model.DescribeRulesRequest
import com.amazonaws.services.elasticloadbalancingv2.model.DescribeTargetGroupsRequest
import com.amazonaws.services.elasticloadbalancingv2.model.Listener
import com.amazonaws.services.elasticloadbalancingv2.model.ListenerNotFoundException
import com.amazonaws.services.elasticloadbalancingv2.model.LoadBalancer
import com.amazonaws.services.elasticloadbalancingv2.model.Matcher
import com.amazonaws.services.elasticloadbalancingv2.model.ModifyTargetGroupAttributesRequest
import com.amazonaws.services.elasticloadbalancingv2.model.ModifyTargetGroupRequest
import com.amazonaws.services.elasticloadbalancingv2.model.ResourceInUseException
import com.amazonaws.services.elasticloadbalancingv2.model.Rule
import com.amazonaws.services.elasticloadbalancingv2.model.RuleCondition
import com.amazonaws.services.elasticloadbalancingv2.model.SetSecurityGroupsRequest
import com.amazonaws.services.elasticloadbalancingv2.model.TargetGroup
import com.amazonaws.services.elasticloadbalancingv2.model.TargetGroupAttribute
import com.amazonaws.services.elasticloadbalancingv2.model.TargetGroupNotFoundException
import com.netflix.spinnaker.clouddriver.aws.deploy.description.UpsertAmazonLoadBalancerV2Description
import com.netflix.spinnaker.clouddriver.data.task.Task
import com.netflix.spinnaker.clouddriver.data.task.TaskRepository
Expand Down Expand Up @@ -326,7 +349,7 @@ class LoadBalancerV2UpsertHandler {
}
}

static String createLoadBalancer(AmazonElasticLoadBalancing loadBalancing, String loadBalancerName, boolean isInternal,
static LoadBalancer createLoadBalancer(AmazonElasticLoadBalancing loadBalancing, String loadBalancerName, boolean isInternal,
Collection<String> subnetIds, Collection<String> securityGroups,
List<UpsertAmazonLoadBalancerV2Description.TargetGroup> targetGroups,
List<UpsertAmazonLoadBalancerV2Description.Listener> listeners) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import com.amazonaws.services.elasticloadbalancing.model.Listener
import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerAttributes
import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription
import com.amazonaws.services.elasticloadbalancing.model.ModifyLoadBalancerAttributesRequest
import com.amazonaws.services.shield.AWSShield
import com.amazonaws.services.shield.model.CreateProtectionRequest
import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.frigga.Names
import com.netflix.spinnaker.clouddriver.aws.AwsConfiguration.DeployDefaults
Expand All @@ -49,8 +51,8 @@ import groovy.transform.InheritConstructors
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired

import static com.netflix.spinnaker.clouddriver.aws.deploy.ops.securitygroup.SecurityGroupLookupFactory.*

import static com.netflix.spinnaker.clouddriver.aws.deploy.ops.securitygroup.SecurityGroupLookupFactory.SecurityGroupLookup
import static com.netflix.spinnaker.clouddriver.aws.deploy.ops.securitygroup.SecurityGroupLookupFactory.SecurityGroupUpdater
/**
* An AtomicOperation for creating an Elastic Load Balancer from the description of {@link UpsertAmazonLoadBalancerClassicDescription}.
*
Expand Down Expand Up @@ -167,6 +169,24 @@ class UpsertAmazonLoadBalancerAtomicOperation implements AtomicOperation<UpsertA
}

dnsName = LoadBalancerUpsertHandler.createLoadBalancer(loadBalancing, loadBalancerName, isInternal, availabilityZones, subnetIds, listeners, securityGroups)

// Enable AWS shield. We only do this on creation. The ELB must be external, the account must be enabled with
// AWS Shield Protection and the description must not opt out of protection.
if (!description.isInternal && description.credentials.shieldEnabled && description.shieldProtectionEnabled) {
task.updateStatus BASE_PHASE, "Configuring AWS Shield for ${loadBalancerName} in ${region}..."
try {
AWSShield shieldClient = amazonClientProvider.getAmazonShield(description.credentials, region)
shieldClient.createProtection(
new CreateProtectionRequest()
.withName(loadBalancerName)
.withResourceArn(loadBalancerArn(description.credentials.accountId, region, loadBalancerName))
)
task.updateStatus BASE_PHASE, "AWS Shield configured for ${loadBalancerName} in ${region}."
} catch (Exception e) {
log.error("Failed to enable AWS Shield protection on $loadBalancerName", e)
task.updateStatus BASE_PHASE, "Failed to configure AWS Shield for ${loadBalancerName} in ${region}."
}
}
} else {
dnsName = loadBalancer.DNSName
LoadBalancerUpsertHandler.updateLoadBalancer(loadBalancing, loadBalancer, listeners, securityGroups)
Expand Down Expand Up @@ -347,6 +367,10 @@ class UpsertAmazonLoadBalancerAtomicOperation implements AtomicOperation<UpsertA
permissionsToAdd.removeIf { permission -> !permission.userIdGroupPairs }
}

private static String loadBalancerArn(String accountId, String region, String name) {
return "arn:aws:elasticloadbalancing:$accountId:$region:loadbalancer/$name"
}

@InheritConstructors
static class FailedSecurityGroupIngressException extends Exception {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package com.netflix.spinnaker.clouddriver.aws.deploy.ops.loadbalancer

import com.amazonaws.AmazonServiceException
import com.amazonaws.services.elasticloadbalancingv2.model.*
import com.amazonaws.services.shield.AWSShield
import com.amazonaws.services.shield.model.CreateProtectionRequest
import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.clouddriver.aws.deploy.description.UpsertAmazonLoadBalancerDescription
import com.netflix.spinnaker.clouddriver.aws.deploy.description.UpsertAmazonLoadBalancerV2Description
Expand Down Expand Up @@ -94,7 +96,26 @@ class UpsertAmazonLoadBalancerV2AtomicOperation implements AtomicOperation<Upser
subnetIds = regionScopedProvider.subnetAnalyzer.getSubnetIdsForZones(availabilityZones,
description.subnetType, SubnetTarget.ELB, 1)
}
dnsName = LoadBalancerV2UpsertHandler.createLoadBalancer(loadBalancing, loadBalancerName, isInternal, subnetIds, securityGroups, description.targetGroups, description.listeners)
loadBalancer = LoadBalancerV2UpsertHandler.createLoadBalancer(loadBalancing, loadBalancerName, isInternal, subnetIds, securityGroups, description.targetGroups, description.listeners)
dnsName = loadBalancer.DNSName

// Enable AWS shield. We only do this on creation. The ELB must be external, the account must be enabled with
// AWS Shield Protection and the description must not opt out of protection.
if (!description.isInternal && description.credentials.shieldEnabled && description.shieldProtectionEnabled) {
task.updateStatus BASE_PHASE, "Configuring AWS Shield for ${loadBalancerName} in ${region}..."
try {
AWSShield shieldClient = amazonClientProvider.getAmazonShield(description.credentials, region)
shieldClient.createProtection(
new CreateProtectionRequest()
.withName(loadBalancerName)
.withResourceArn(loadBalancer.getLoadBalancerArn())
)
task.updateStatus BASE_PHASE, "AWS Shield configured for ${loadBalancerName} in ${region}."
} catch (Exception e) {
log.error("Failed to enable AWS Shield protection on $loadBalancerName", e)
task.updateStatus BASE_PHASE, "Failed to configure AWS Shield for ${loadBalancerName} in ${region}."
}
}
} else {
task.updateStatus BASE_PHASE, "Found existing load balancer named ${loadBalancerName} in ${region}... Using that."
dnsName = loadBalancer.DNSName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import com.amazonaws.services.route53.AmazonRoute53ClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.shield.AWSShield;
import com.amazonaws.services.shield.AWSShieldClientBuilder;
import com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflow;
import com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClientBuilder;
import com.amazonaws.services.sns.AmazonSNS;
Expand Down Expand Up @@ -417,4 +419,12 @@ public AmazonIdentityManagement getAmazonIdentityManagement(NetflixAmazonCredent
public AmazonIdentityManagement getAmazonIdentityManagement(String accountName, AWSCredentialsProvider awsCredentialsProvider, String region) {
return awsSdkClientSupplier.getClient(AmazonIdentityManagementClientBuilder.class, AmazonIdentityManagement.class, accountName, awsCredentialsProvider, region);
}

public AWSShield getAmazonShield(NetflixAmazonCredentials amazonCredentials, String region) {
return proxyHandlerBuilder.getProxyHandler(AWSShield.class, AWSShieldClientBuilder.class, amazonCredentials, region, true);
}

public AWSShield getAmazonShield(String accountName, AWSCredentialsProvider awsCredentialsProvider, String region) {
return awsSdkClientSupplier.getClient(AWSShieldClientBuilder.class, AWSShield.class, accountName, awsCredentialsProvider, region);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class NetflixAmazonCredentials extends AmazonCredentials {
private final boolean front50Enabled;
private final String bastionHost;
private final boolean bastionEnabled;
private final boolean shieldEnabled;

public NetflixAmazonCredentials(@JsonProperty("name") String name,
@JsonProperty("environment") String environment,
Expand All @@ -53,7 +54,8 @@ public NetflixAmazonCredentials(@JsonProperty("name") String name,
@JsonProperty("front50") String front50,
@JsonProperty("front50Enabled") Boolean front50Enabled,
@JsonProperty("bastionHost") String bastionHost,
@JsonProperty("bastionEnabled") Boolean bastionEnabled) {
@JsonProperty("bastionEnabled") Boolean bastionEnabled,
@JsonProperty("shieldEnabled") Boolean shieldEnabled) {
this(name,
environment,
accountType,
Expand All @@ -73,7 +75,8 @@ public NetflixAmazonCredentials(@JsonProperty("name") String name,
front50,
front50Enabled,
bastionHost,
bastionEnabled);
bastionEnabled,
shieldEnabled);
}

private static boolean flagValue(String serviceUrl, Boolean flag) {
Expand All @@ -100,7 +103,8 @@ public NetflixAmazonCredentials(NetflixAmazonCredentials copy, AWSCredentialsPro
copy.getFront50(),
copy.getFront50Enabled(),
copy.getBastionHost(),
copy.getBastionEnabled());
copy.getBastionEnabled(),
copy.getShieldEnabled());
}

NetflixAmazonCredentials(String name,
Expand All @@ -122,7 +126,8 @@ public NetflixAmazonCredentials(NetflixAmazonCredentials copy, AWSCredentialsPro
String front50,
Boolean front50Enabled,
String bastionHost,
Boolean bastionEnabled) {
Boolean bastionEnabled,
Boolean shieldEnabled) {
super(name,
environment,
accountType,
Expand All @@ -143,6 +148,7 @@ public NetflixAmazonCredentials(NetflixAmazonCredentials copy, AWSCredentialsPro
this.front50Enabled = flagValue(front50, front50Enabled);
this.bastionHost = bastionHost;
this.bastionEnabled = flagValue(bastionHost, bastionEnabled);
this.shieldEnabled = (shieldEnabled == null) ? false : shieldEnabled;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaults to false, good.

}

public String getEdda() {
Expand Down Expand Up @@ -176,4 +182,8 @@ public boolean getFront50Enabled() {
public boolean getBastionEnabled() {
return bastionEnabled;
}

public boolean getShieldEnabled() {
return shieldEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public NetflixAssumeRoleAmazonCredentials(@JsonProperty("name") String name,
@JsonProperty("front50Enabled") Boolean front50Enabled,
@JsonProperty("bastionHost") String bastionHost,
@JsonProperty("bastionEnabled") Boolean bastionEnabled,
@JsonProperty("shieldEnabled") Boolean shieldEnabled,
@JsonProperty("assumeRole") String assumeRole,
@JsonProperty("sessionName") String sessionName) {

Expand All @@ -76,6 +77,7 @@ public NetflixAssumeRoleAmazonCredentials(@JsonProperty("name") String name,
front50Enabled,
bastionHost,
bastionEnabled,
shieldEnabled,
assumeRole,
sessionName);
}
Expand All @@ -101,6 +103,7 @@ public NetflixAssumeRoleAmazonCredentials(NetflixAssumeRoleAmazonCredentials cop
copy.getFront50Enabled(),
copy.getBastionHost(),
copy.getBastionEnabled(),
copy.getShieldEnabled(),
copy.getAssumeRole(),
copy.getSessionName());
}
Expand All @@ -125,6 +128,7 @@ public NetflixAssumeRoleAmazonCredentials(NetflixAssumeRoleAmazonCredentials cop
Boolean front50Enabled,
String bastionHost,
Boolean bastionEnabled,
Boolean shieldEnabled,
String assumeRole,
String sessionName) {
super(name,
Expand All @@ -149,7 +153,8 @@ public NetflixAssumeRoleAmazonCredentials(NetflixAssumeRoleAmazonCredentials cop
front50,
front50Enabled,
bastionHost,
bastionEnabled);
bastionEnabled,
shieldEnabled);
this.assumeRole = assumeRole;
this.sessionName = sessionName == null ? AssumeRoleAmazonCredentials.DEFAULT_SESSION_NAME : sessionName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import com.amazonaws.services.elasticloadbalancing.model.ListenerDescription
import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerAttributes
import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription
import com.amazonaws.services.elasticloadbalancing.model.ModifyLoadBalancerAttributesRequest
import com.amazonaws.services.shield.AWSShield
import com.amazonaws.services.shield.model.CreateProtectionRequest
import com.netflix.spinnaker.clouddriver.aws.AwsConfiguration
import com.netflix.spinnaker.clouddriver.aws.deploy.ops.securitygroup.SecurityGroupLookupFactory
import com.netflix.spinnaker.clouddriver.aws.security.AmazonClientProvider
Expand All @@ -49,6 +51,7 @@ import com.netflix.spinnaker.clouddriver.aws.services.RegionScopedProviderFactor
import com.netflix.spinnaker.clouddriver.aws.services.SecurityGroupService
import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Unroll

import static com.netflix.spinnaker.clouddriver.aws.deploy.ops.securitygroup.SecurityGroupLookupFactory.*

Expand All @@ -74,8 +77,10 @@ class UpsertAmazonLoadBalancerClassicAtomicOperationSpec extends Specification {
healthCheckPort: 7001
)
AmazonElasticLoadBalancing loadBalancing = Mock(AmazonElasticLoadBalancing)
AWSShield awsShield = Mock(AWSShield)
def mockAmazonClientProvider = Stub(AmazonClientProvider) {
getAmazonElasticLoadBalancing(_, _, true) >> loadBalancing
getAmazonShield(_, _) >> awsShield
}
def mockSecurityGroupService = Stub(SecurityGroupService) {
getSecurityGroupIds(["foo"], null) >> ["foo": "sg-1234"]
Expand Down Expand Up @@ -557,4 +562,34 @@ class UpsertAmazonLoadBalancerClassicAtomicOperationSpec extends Specification {

1 * loadBalancing.createLoadBalancer(_ as CreateLoadBalancerRequest) >> new CreateLoadBalancerResult(dNSName: 'dnsName1')
}

@Unroll
void "should enable AWS Shield protection if external ELB"() {
given:
description.credentials = TestCredential.named('bar', [shieldEnabled: shieldEnabled])
description.shieldProtectionEnabled = descriptionOverride
description.vpcId = "vpcId"

when:
operation.operate([])

then:
1 * securityGroupLookup.getSecurityGroupByName('bar', 'kato-elb', 'vpcId') >> Optional.of(elbSecurityGroupUpdater)
1 * securityGroupLookup.getSecurityGroupByName('bar', 'kato', 'vpcId') >> Optional.of(appSecurityGroupUpdater)
1 * elbSecurityGroupUpdater.getSecurityGroup() >> elbSecurityGroup
1 * appSecurityGroupUpdater.getSecurityGroup() >> applicationSecurityGroup

1 * loadBalancing.createLoadBalancer(_ as CreateLoadBalancerRequest) >> new CreateLoadBalancerResult(dNSName: 'dnsName1')
(shouldProtect ? 1 : 0) * awsShield.createProtection(new CreateProtectionRequest(
name: 'kato-main-frontend',
resourceArn: 'arn:aws:elasticloadbalancing:123456789012bar:us-east-1:loadbalancer/kato-main-frontend'
))

where:
shieldEnabled | descriptionOverride || shouldProtect
false | false || false
false | true || false
true | false || false
true | true || true
}
}
Loading