The aws:ec2/securityGroup:SecurityGroup resource, part of the Pulumi AWS provider, defines the security group container itself: its name, VPC placement, and optionally inline rules. This guide focuses on four capabilities: creating security groups with separate rule resources, inline rule configuration, prefix list references, and explicit rule removal.
Security groups belong to a VPC and may reference VPC endpoints or other security groups. The documentation recommends using separate SecurityGroupIngressRule and SecurityGroupEgressRule resources instead of inline rules to avoid management issues with multiple CIDR blocks. The examples are intentionally small. Combine them with your own VPC infrastructure and rule definitions.
Create a security group with separate rule resources
Most deployments create a security group container, then attach ingress and egress rules as separate resources to avoid inline rule limitations.
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. The SecurityGroupIngressRule and SecurityGroupEgressRule resources attach rules separately, each referencing the security group ID. This pattern handles multiple CIDR blocks cleanly and provides unique IDs for each rule. The ipProtocol value “-1” means all protocols; specific protocols use “tcp”, “udp”, or protocol numbers.
Allow all outbound traffic with inline rules
AWS removes the default egress rule when you create security groups in a VPC. To restore allow-all behavior, define an inline egress rule.
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 argument defines rules inline rather than as separate resources. Setting protocol to “-1”, fromPort and toPort to 0, and including both IPv4 and IPv6 CIDR blocks recreates AWS’s default allow-all egress behavior. Without this rule, the security group blocks all outbound traffic.
Reference VPC endpoints with prefix lists
VPC endpoints export prefix list IDs representing AWS service IP ranges. Security groups can reference these to allow traffic to AWS services.
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 references the VPC endpoint’s prefix list instead of explicit CIDR blocks. This automatically tracks AWS service IP ranges as they change. Prefix lists work with both ingress and egress rules.
Remove all managed rules from a security group
The ingress and egress arguments use attributes-as-blocks mode. Removing them from configuration doesn’t destroy rules; you must explicitly set them to empty arrays.
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 explicitly removes all managed rules. This is necessary because the provider processes these arguments in attributes-as-blocks mode, where removing the argument doesn’t signal deletion.
Beyond these examples
These snippets focus on specific security group features: separate rule resources vs inline rules, prefix list references for AWS services, and explicit rule removal. They’re intentionally minimal rather than full network security configurations.
The examples may reference pre-existing infrastructure such as VPC with CIDR blocks configured, and VPC endpoints for prefix list examples. They focus on configuring the security group rather than provisioning the surrounding VPC infrastructure.
To keep things focused, common security group patterns are omitted, including:
- Security group rule resources (SecurityGroupRule) as alternative to inline rules
- Self-referencing rules for intra-group communication
- Cross-security-group references
- Rule descriptions and tags
- Deletion challenges and workarounds (revokeRulesOnDelete, provisioners)
These omissions are intentional: the goal is to illustrate how each security group feature is wired, not provide drop-in network security modules. See the Security Group resource reference for all available configuration options.
Let's create and Configure Security Groups
Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.
Try Pulumi Cloud for FREEFrequently Asked Questions
Rule Management & Best Practices
ingress and egress arguments) struggle with managing multiple CIDR blocks and lack support for unique IDs, tags, and descriptions. Use aws.vpc.SecurityGroupIngressRule and aws.vpc.SecurityGroupEgressRule resources instead, with one CIDR block per rule.aws.vpc.SecurityGroupIngressRule, aws.vpc.SecurityGroupEgressRule, or aws.ec2.SecurityGroupRule resources causes rule conflicts, perpetual differences, and overwritten rules. Use one approach or the other, never both.ingress: [] and egress: [] in your configuration. Due to attributes-as-blocks mode, simply removing these arguments won’t delete the managed rules.cidrBlocks and ipv6CidrBlocks are optional in ingress and egress blocks, but omitting them blocks all traffic.Deletion & Lifecycle Issues
revokeRulesOnDelete (default false) instructs Pulumi to revoke all attached ingress and egress rules before deleting the security group. Use this for services like Elastic Map Reduce that automatically add rules with cyclic dependencies.name, description, namePrefix, or vpcId forces recreation because these properties are immutable. This can fail due to dependencies with other resources like EC2 instances, creating the “Security Group Deletion Problem.”Immutable Properties & Configuration
name, description, namePrefix, and vpcId. Changing any of these forces recreation of the security group.description property is immutable and maps to AWS’s GroupDescription attribute, which has no Update API. Use tags for classification that needs to be updated.Networking & VPC
-1, CIDR 0.0.0.0/0 (IPv4), and ::/0 (IPv6).Using a different cloud?
Explore networking guides for other cloud providers: