The aws:ec2/securityGroup:SecurityGroup resource, part of the Pulumi AWS provider, defines the security group container itself: its name, VPC placement, and optional inline rules. This guide focuses on four capabilities: companion rule resources (the recommended pattern), inline rule configuration, prefix list references, and rule removal.
Security groups belong to VPCs and may reference VPC endpoints for prefix lists. The examples are intentionally small. The documentation strongly recommends using companion SecurityGroupIngressRule and SecurityGroupEgressRule resources instead of inline rules to avoid conflicts when managing multiple CIDR blocks or rule descriptions.
Create a security group with separate rule resources
Most deployments create security groups as containers, then attach ingress and egress rules separately to avoid conflicts.
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 defines the container with a name and VPC. The SecurityGroupIngressRule and SecurityGroupEgressRule resources attach rules by referencing the security group’s ID. This pattern avoids the limitations of inline rules: each rule can have its own description, tags, and CIDR block without conflicts. The ipProtocol value “-1” means all protocols.
Allow all outbound traffic with inline rules
AWS creates a default egress rule allowing all outbound traffic when you create a security group. Pulumi removes this default, requiring you to recreate it explicitly.
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 inline rules. Setting protocol to “-1” allows all protocols, and the CIDR blocks “0.0.0.0/0” and “::/0” allow all IPv4 and IPv6 destinations. This recreates AWS’s default egress behavior, which Pulumi removes automatically.
Reference AWS-managed prefix lists for service endpoints
VPC endpoints expose prefix list IDs representing AWS service IP ranges, which security groups can reference instead of hardcoding CIDR blocks.
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, which AWS maintains automatically. When AWS updates service IP ranges, your security group rules update automatically without configuration changes.
Remove all rules by setting empty arrays
The ingress and egress arguments use attribute-as-blocks mode. Removing them from configuration doesn’t destroy managed rules; you must explicitly set 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 removes all managed rules. Without this explicit configuration, removing the arguments leaves existing rules in place.
Beyond these examples
These snippets focus on specific security group features: companion rule resources vs inline rules, prefix list references, and rule removal patterns. They’re intentionally minimal rather than full network security configurations.
The examples may reference pre-existing infrastructure such as VPC with CIDR blocks, and VPC endpoints for prefix list examples. They focus on configuring the security group rather than provisioning the surrounding network.
To keep things focused, common security group patterns are omitted, including:
- Security group rule descriptions and tags
- Self-referencing rules for intra-group traffic
- Cross-security-group references
- Rule priority and evaluation order
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 configure AWS 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
aws.vpc.SecurityGroupIngressRule and aws.vpc.SecurityGroupEgressRule) with one CIDR block per rule. Inline ingress and egress arguments struggle with multiple CIDR blocks and lack unique IDs, tags, and descriptions. Never mix inline rules with separate rule resources, as this causes rule conflicts, perpetual diffs, and rules being overwritten.ingress/egress arguments) with separate rule resources (aws.vpc.SecurityGroupIngressRule, aws.vpc.SecurityGroupEgressRule, or aws.ec2.SecurityGroupRule). This combination causes conflicts and perpetual differences. Choose one approach and stick with it.ingress: [] and egress: [] in your configuration. Due to attributes-as-blocks mode, simply removing these arguments won’t destroy the managed rules.cidrBlocks and ipv6CidrBlocks parameters are optional in ingress and egress blocks, but omitting them blocks all traffic.Resource Lifecycle & Deletion
revokeRulesOnDelete: true when using AWS services like Elastic MapReduce that automatically add rules to your security groups. These auto-added rules can create cyclic dependencies that prevent deletion. This option revokes all attached rules before deleting the security group itself. Default is false.Configuration & Immutability
name, description, namePrefix, or vpcId forces Pulumi to destroy and recreate the security group. These properties are immutable in AWS and cannot be updated in place.description field maps to AWS’s GroupDescription attribute, which has no Update API. It’s immutable and defaults to “Managed by Pulumi”. Use tags instead if you need updatable classification for your security groups.prefixListIds in your egress or ingress blocks. You can reference prefix lists from VPC endpoints (using vpcEndpoint.prefixListId) or find specific prefix lists using the aws.ec2.getPrefixList data source.Default Behaviors
protocol: "-1", cidrBlocks: ["0.0.0.0/0"], and ipv6CidrBlocks: ["::/0"].Using a different cloud?
Explore networking guides for other cloud providers: