The aws:ec2/securityGroup:SecurityGroup resource, part of the Pulumi AWS provider, provisions the security group container itself: its name, VPC placement, and metadata. This guide focuses on four capabilities: creating security groups with companion rule resources, inline rule definitions, prefix list references, and rule removal patterns.
Security groups don’t define access control in isolation. They exist within VPCs and depend on companion SecurityGroupIngressRule and SecurityGroupEgressRule resources for rule definitions. Current best practice avoids inline ingress and egress blocks due to limitations with multiple CIDR blocks and rule metadata. The examples are intentionally small and show individual configuration patterns. Combine them with your own VPC infrastructure and access requirements.
Create a security group with separate rule resources
Most deployments define security groups as containers, then attach individual rules using companion resources. This avoids conflicts when managing multiple CIDR blocks.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const allowTls = new aws.ec2.SecurityGroup("allow_tls", {
name: "allow_tls",
description: "Allow TLS inbound traffic and all outbound traffic",
vpcId: main.id,
tags: {
Name: "allow_tls",
},
});
const allowTlsIpv4 = new aws.vpc.SecurityGroupIngressRule("allow_tls_ipv4", {
securityGroupId: allowTls.id,
cidrIpv4: main.cidrBlock,
fromPort: 443,
ipProtocol: "tcp",
toPort: 443,
});
const allowTlsIpv6 = new aws.vpc.SecurityGroupIngressRule("allow_tls_ipv6", {
securityGroupId: allowTls.id,
cidrIpv6: main.ipv6CidrBlock,
fromPort: 443,
ipProtocol: "tcp",
toPort: 443,
});
const allowAllTrafficIpv4 = new aws.vpc.SecurityGroupEgressRule("allow_all_traffic_ipv4", {
securityGroupId: allowTls.id,
cidrIpv4: "0.0.0.0/0",
ipProtocol: "-1",
});
const allowAllTrafficIpv6 = new aws.vpc.SecurityGroupEgressRule("allow_all_traffic_ipv6", {
securityGroupId: allowTls.id,
cidrIpv6: "::/0",
ipProtocol: "-1",
});
import pulumi
import pulumi_aws as aws
allow_tls = aws.ec2.SecurityGroup("allow_tls",
name="allow_tls",
description="Allow TLS inbound traffic and all outbound traffic",
vpc_id=main["id"],
tags={
"Name": "allow_tls",
})
allow_tls_ipv4 = aws.vpc.SecurityGroupIngressRule("allow_tls_ipv4",
security_group_id=allow_tls.id,
cidr_ipv4=main["cidrBlock"],
from_port=443,
ip_protocol="tcp",
to_port=443)
allow_tls_ipv6 = aws.vpc.SecurityGroupIngressRule("allow_tls_ipv6",
security_group_id=allow_tls.id,
cidr_ipv6=main["ipv6CidrBlock"],
from_port=443,
ip_protocol="tcp",
to_port=443)
allow_all_traffic_ipv4 = aws.vpc.SecurityGroupEgressRule("allow_all_traffic_ipv4",
security_group_id=allow_tls.id,
cidr_ipv4="0.0.0.0/0",
ip_protocol="-1")
allow_all_traffic_ipv6 = aws.vpc.SecurityGroupEgressRule("allow_all_traffic_ipv6",
security_group_id=allow_tls.id,
cidr_ipv6="::/0",
ip_protocol="-1")
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/vpc"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
allowTls, err := ec2.NewSecurityGroup(ctx, "allow_tls", &ec2.SecurityGroupArgs{
Name: pulumi.String("allow_tls"),
Description: pulumi.String("Allow TLS inbound traffic and all outbound traffic"),
VpcId: pulumi.Any(main.Id),
Tags: pulumi.StringMap{
"Name": pulumi.String("allow_tls"),
},
})
if err != nil {
return err
}
_, err = vpc.NewSecurityGroupIngressRule(ctx, "allow_tls_ipv4", &vpc.SecurityGroupIngressRuleArgs{
SecurityGroupId: allowTls.ID(),
CidrIpv4: pulumi.Any(main.CidrBlock),
FromPort: pulumi.Int(443),
IpProtocol: pulumi.String("tcp"),
ToPort: pulumi.Int(443),
})
if err != nil {
return err
}
_, err = vpc.NewSecurityGroupIngressRule(ctx, "allow_tls_ipv6", &vpc.SecurityGroupIngressRuleArgs{
SecurityGroupId: allowTls.ID(),
CidrIpv6: pulumi.Any(main.Ipv6CidrBlock),
FromPort: pulumi.Int(443),
IpProtocol: pulumi.String("tcp"),
ToPort: pulumi.Int(443),
})
if err != nil {
return err
}
_, err = vpc.NewSecurityGroupEgressRule(ctx, "allow_all_traffic_ipv4", &vpc.SecurityGroupEgressRuleArgs{
SecurityGroupId: allowTls.ID(),
CidrIpv4: pulumi.String("0.0.0.0/0"),
IpProtocol: pulumi.String("-1"),
})
if err != nil {
return err
}
_, err = vpc.NewSecurityGroupEgressRule(ctx, "allow_all_traffic_ipv6", &vpc.SecurityGroupEgressRuleArgs{
SecurityGroupId: allowTls.ID(),
CidrIpv6: pulumi.String("::/0"),
IpProtocol: pulumi.String("-1"),
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var allowTls = new Aws.Ec2.SecurityGroup("allow_tls", new()
{
Name = "allow_tls",
Description = "Allow TLS inbound traffic and all outbound traffic",
VpcId = main.Id,
Tags =
{
{ "Name", "allow_tls" },
},
});
var allowTlsIpv4 = new Aws.Vpc.SecurityGroupIngressRule("allow_tls_ipv4", new()
{
SecurityGroupId = allowTls.Id,
CidrIpv4 = main.CidrBlock,
FromPort = 443,
IpProtocol = "tcp",
ToPort = 443,
});
var allowTlsIpv6 = new Aws.Vpc.SecurityGroupIngressRule("allow_tls_ipv6", new()
{
SecurityGroupId = allowTls.Id,
CidrIpv6 = main.Ipv6CidrBlock,
FromPort = 443,
IpProtocol = "tcp",
ToPort = 443,
});
var allowAllTrafficIpv4 = new Aws.Vpc.SecurityGroupEgressRule("allow_all_traffic_ipv4", new()
{
SecurityGroupId = allowTls.Id,
CidrIpv4 = "0.0.0.0/0",
IpProtocol = "-1",
});
var allowAllTrafficIpv6 = new Aws.Vpc.SecurityGroupEgressRule("allow_all_traffic_ipv6", new()
{
SecurityGroupId = allowTls.Id,
CidrIpv6 = "::/0",
IpProtocol = "-1",
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.SecurityGroup;
import com.pulumi.aws.ec2.SecurityGroupArgs;
import com.pulumi.aws.vpc.SecurityGroupIngressRule;
import com.pulumi.aws.vpc.SecurityGroupIngressRuleArgs;
import com.pulumi.aws.vpc.SecurityGroupEgressRule;
import com.pulumi.aws.vpc.SecurityGroupEgressRuleArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var allowTls = new SecurityGroup("allowTls", SecurityGroupArgs.builder()
.name("allow_tls")
.description("Allow TLS inbound traffic and all outbound traffic")
.vpcId(main.id())
.tags(Map.of("Name", "allow_tls"))
.build());
var allowTlsIpv4 = new SecurityGroupIngressRule("allowTlsIpv4", SecurityGroupIngressRuleArgs.builder()
.securityGroupId(allowTls.id())
.cidrIpv4(main.cidrBlock())
.fromPort(443)
.ipProtocol("tcp")
.toPort(443)
.build());
var allowTlsIpv6 = new SecurityGroupIngressRule("allowTlsIpv6", SecurityGroupIngressRuleArgs.builder()
.securityGroupId(allowTls.id())
.cidrIpv6(main.ipv6CidrBlock())
.fromPort(443)
.ipProtocol("tcp")
.toPort(443)
.build());
var allowAllTrafficIpv4 = new SecurityGroupEgressRule("allowAllTrafficIpv4", SecurityGroupEgressRuleArgs.builder()
.securityGroupId(allowTls.id())
.cidrIpv4("0.0.0.0/0")
.ipProtocol("-1")
.build());
var allowAllTrafficIpv6 = new SecurityGroupEgressRule("allowAllTrafficIpv6", SecurityGroupEgressRuleArgs.builder()
.securityGroupId(allowTls.id())
.cidrIpv6("::/0")
.ipProtocol("-1")
.build());
}
}
resources:
allowTls:
type: aws:ec2:SecurityGroup
name: allow_tls
properties:
name: allow_tls
description: Allow TLS inbound traffic and all outbound traffic
vpcId: ${main.id}
tags:
Name: allow_tls
allowTlsIpv4:
type: aws:vpc:SecurityGroupIngressRule
name: allow_tls_ipv4
properties:
securityGroupId: ${allowTls.id}
cidrIpv4: ${main.cidrBlock}
fromPort: 443
ipProtocol: tcp
toPort: 443
allowTlsIpv6:
type: aws:vpc:SecurityGroupIngressRule
name: allow_tls_ipv6
properties:
securityGroupId: ${allowTls.id}
cidrIpv6: ${main.ipv6CidrBlock}
fromPort: 443
ipProtocol: tcp
toPort: 443
allowAllTrafficIpv4:
type: aws:vpc:SecurityGroupEgressRule
name: allow_all_traffic_ipv4
properties:
securityGroupId: ${allowTls.id}
cidrIpv4: 0.0.0.0/0
ipProtocol: '-1'
allowAllTrafficIpv6:
type: aws:vpc:SecurityGroupEgressRule
name: allow_all_traffic_ipv6
properties:
securityGroupId: ${allowTls.id}
cidrIpv6: ::/0
ipProtocol: '-1'
The SecurityGroup resource creates the container with a name and vpcId. Companion SecurityGroupIngressRule and SecurityGroupEgressRule resources attach to it via securityGroupId. This approach provides unique IDs for each rule, enabling tags and descriptions per rule.
Allow all outbound traffic using inline rules
AWS creates an allow-all egress rule by default for new VPC security groups. Pulumi removes this default, requiring explicit recreation.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.ec2.SecurityGroup("example", {egress: [{
fromPort: 0,
toPort: 0,
protocol: "-1",
cidrBlocks: ["0.0.0.0/0"],
ipv6CidrBlocks: ["::/0"],
}]});
import pulumi
import pulumi_aws as aws
example = aws.ec2.SecurityGroup("example", egress=[{
"from_port": 0,
"to_port": 0,
"protocol": "-1",
"cidr_blocks": ["0.0.0.0/0"],
"ipv6_cidr_blocks": ["::/0"],
}])
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ec2.NewSecurityGroup(ctx, "example", &ec2.SecurityGroupArgs{
Egress: ec2.SecurityGroupEgressArray{
&ec2.SecurityGroupEgressArgs{
FromPort: pulumi.Int(0),
ToPort: pulumi.Int(0),
Protocol: pulumi.String("-1"),
CidrBlocks: pulumi.StringArray{
pulumi.String("0.0.0.0/0"),
},
Ipv6CidrBlocks: pulumi.StringArray{
pulumi.String("::/0"),
},
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var example = new Aws.Ec2.SecurityGroup("example", new()
{
Egress = new[]
{
new Aws.Ec2.Inputs.SecurityGroupEgressArgs
{
FromPort = 0,
ToPort = 0,
Protocol = "-1",
CidrBlocks = new[]
{
"0.0.0.0/0",
},
Ipv6CidrBlocks = new[]
{
"::/0",
},
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.SecurityGroup;
import com.pulumi.aws.ec2.SecurityGroupArgs;
import com.pulumi.aws.ec2.inputs.SecurityGroupEgressArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var example = new SecurityGroup("example", SecurityGroupArgs.builder()
.egress(SecurityGroupEgressArgs.builder()
.fromPort(0)
.toPort(0)
.protocol("-1")
.cidrBlocks("0.0.0.0/0")
.ipv6CidrBlocks("::/0")
.build())
.build());
}
}
resources:
example:
type: aws:ec2:SecurityGroup
properties:
egress:
- fromPort: 0
toPort: 0
protocol: '-1'
cidrBlocks:
- 0.0.0.0/0
ipv6CidrBlocks:
- ::/0
The egress block uses protocol “-1” to represent all protocols. The cidrBlocks and ipv6CidrBlocks arrays allow traffic to all IPv4 and IPv6 destinations. Setting fromPort and toPort to 0 with protocol “-1” allows all port ranges.
Reference AWS-managed prefix lists in rules
AWS services publish prefix lists representing IP ranges for services like S3 or CloudFront. VPC endpoints expose prefix list IDs that can be referenced in rules.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const myEndpoint = new aws.ec2.VpcEndpoint("my_endpoint", {});
const example = new aws.ec2.SecurityGroup("example", {egress: [{
fromPort: 0,
toPort: 0,
protocol: "-1",
prefixListIds: [myEndpoint.prefixListId],
}]});
import pulumi
import pulumi_aws as aws
my_endpoint = aws.ec2.VpcEndpoint("my_endpoint")
example = aws.ec2.SecurityGroup("example", egress=[{
"from_port": 0,
"to_port": 0,
"protocol": "-1",
"prefix_list_ids": [my_endpoint.prefix_list_id],
}])
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
myEndpoint, err := ec2.NewVpcEndpoint(ctx, "my_endpoint", nil)
if err != nil {
return err
}
_, err = ec2.NewSecurityGroup(ctx, "example", &ec2.SecurityGroupArgs{
Egress: ec2.SecurityGroupEgressArray{
&ec2.SecurityGroupEgressArgs{
FromPort: pulumi.Int(0),
ToPort: pulumi.Int(0),
Protocol: pulumi.String("-1"),
PrefixListIds: pulumi.StringArray{
myEndpoint.PrefixListId,
},
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var myEndpoint = new Aws.Ec2.VpcEndpoint("my_endpoint");
var example = new Aws.Ec2.SecurityGroup("example", new()
{
Egress = new[]
{
new Aws.Ec2.Inputs.SecurityGroupEgressArgs
{
FromPort = 0,
ToPort = 0,
Protocol = "-1",
PrefixListIds = new[]
{
myEndpoint.PrefixListId,
},
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.VpcEndpoint;
import com.pulumi.aws.ec2.SecurityGroup;
import com.pulumi.aws.ec2.SecurityGroupArgs;
import com.pulumi.aws.ec2.inputs.SecurityGroupEgressArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var myEndpoint = new VpcEndpoint("myEndpoint");
var example = new SecurityGroup("example", SecurityGroupArgs.builder()
.egress(SecurityGroupEgressArgs.builder()
.fromPort(0)
.toPort(0)
.protocol("-1")
.prefixListIds(myEndpoint.prefixListId())
.build())
.build());
}
}
resources:
example:
type: aws:ec2:SecurityGroup
properties:
egress:
- fromPort: 0
toPort: 0
protocol: '-1'
prefixListIds:
- ${myEndpoint.prefixListId}
myEndpoint:
type: aws:ec2:VpcEndpoint
name: my_endpoint
The prefixListIds property accepts a list of managed prefix list IDs. Here, a VPC endpoint’s prefixListId provides the AWS service ranges. This avoids hardcoding IP ranges that AWS manages and updates.
Remove all rules from an existing security group
The ingress and egress properties are processed in attributes-as-blocks mode. Removing them from configuration won’t destroy managed rules; set them to empty arrays explicitly.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.ec2.SecurityGroup("example", {
name: "sg",
vpcId: exampleAwsVpc.id,
ingress: [],
egress: [],
});
import pulumi
import pulumi_aws as aws
example = aws.ec2.SecurityGroup("example",
name="sg",
vpc_id=example_aws_vpc["id"],
ingress=[],
egress=[])
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ec2.NewSecurityGroup(ctx, "example", &ec2.SecurityGroupArgs{
Name: pulumi.String("sg"),
VpcId: pulumi.Any(exampleAwsVpc.Id),
Ingress: ec2.SecurityGroupIngressArray{},
Egress: ec2.SecurityGroupEgressArray{},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var example = new Aws.Ec2.SecurityGroup("example", new()
{
Name = "sg",
VpcId = exampleAwsVpc.Id,
Ingress = new[] {},
Egress = new[] {},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.SecurityGroup;
import com.pulumi.aws.ec2.SecurityGroupArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var example = new SecurityGroup("example", SecurityGroupArgs.builder()
.name("sg")
.vpcId(exampleAwsVpc.id())
.ingress()
.egress()
.build());
}
}
resources:
example:
type: aws:ec2:SecurityGroup
properties:
name: sg
vpcId: ${exampleAwsVpc.id}
ingress: []
egress: []
Setting ingress and egress to empty arrays ([]) removes all managed rules while preserving the security group. This is necessary because the attributes-as-blocks processing mode doesn’t treat property removal as destruction.
Beyond These Examples
These snippets focus on specific security group features: security group creation with companion rule resources, inline rule definitions for egress, prefix list references for AWS services, and rule removal patterns. They’re intentionally minimal rather than full network security configurations.
The examples may reference pre-existing infrastructure such as VPCs with subnets, and VPC endpoints for prefix list examples. They focus on security group configuration rather than provisioning the underlying network.
To keep things focused, common security group patterns are omitted, including:
- Ingress rules for specific protocols (SSH, HTTP, HTTPS)
- Self-referencing security group rules
- Cross-security-group references
- IPv6-only rules
- Description and tags for individual rules
- Rule priority and conflict resolution
These omissions are intentional: the goal is to illustrate how each security group feature is wired, not provide drop-in security modules. See the Security Group resource reference for all available configuration options.
Frequently Asked Questions
Rule Management & Best Practices
ingress and egress arguments struggle with managing multiple CIDR blocks and lack unique IDs, tags, and descriptions. Use aws.vpc.SecurityGroupIngressRule and aws.vpc.SecurityGroupEgressRule resources instead, with one CIDR block per rule.ingress/egress arguments with aws.vpc.SecurityGroupIngressRule, aws.vpc.SecurityGroupEgressRule, or aws.ec2.SecurityGroupRule resources causes rule conflicts, perpetual differences, and overwritten rules. Choose one approach and stick with it.ingress: [] and egress: [] in your configuration. Simply removing these arguments won’t destroy the managed rules due to attributes-as-blocks mode.Deletion & Lifecycle Issues
name, description, namePrefix, or vpcId forces recreation since these properties are immutable. This can cause the Security Group Deletion Problem if the group is attached to other resources.revokeRulesOnDelete to true to revoke all attached ingress and egress rules before deletion. This is useful when AWS services like Elastic Map Reduce automatically add rules with cyclic dependencies.Egress & Default Behavior
cidrIpv4: '0.0.0.0/0' (or cidrIpv6: '::/0' for IPv6) and ipProtocol: '-1', as shown in the egress examples.cidrBlocks or ipv6CidrBlocks in ingress or egress blocks, traffic will be blocked by default.Advanced Configuration
prefixListIds argument in egress or ingress blocks. Prefix list IDs are exported on VPC Endpoints, or you can find them using the aws.ec2.getPrefixList data source.tags for classification that needs updating. The description property is immutable and maps to AWS GroupDescription, which has no Update API.Ready to get started?
Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.
Create free account