IBM Cloud VPC Security Groups for Three-Tier VSI Workloads: A Stateful Allow-List Pattern
Learn to secure a three-tier IBM Cloud VPC VSI workload with stateful security-group references instead of CIDRs, including CLI configuration, validation tests, and common pitfalls.
04 Jun 2026, 22:05 UTC

The problem with flat VPC networks
A three-tier VSI workload in IBM Cloud VPC often starts with a flat network: every instance can reach every other instance because rules use broad CIDR blocks or a single default security group. The useful fix is to treat security groups as a stateful allow-list. Put each tier in its own group, and allow inbound traffic only from the upstream tier's security-group ID rather than from an IP range.
How security-group references work
In IBM Cloud VPC, a security group acts as a stateful allow-list attached to a VSI network interface. Rules are evaluated against the packet's remote address or, when you use a remote security-group reference, against the source interface's group membership. Because the control is stateful, an allowed inbound flow does not need a separate rule for the response traffic.
The default posture is deny inbound unless a rule allows it. Do not assume outbound is denied; if a template starts with an explicit outbound allow, narrow it to DNS, NTP, and package repositories rather than all destinations.
Worked example: lb, app, and db tiers
Assume a non-production VPC with three VSIs: a public entry point, an application server, and a database. Create three security groups in the same VPC.
# Run in a shell with the IBM Cloud CLI installed and logged in.
# Confirm current flags and parameters with command help before use.
# Create three security groups in the same VPC (illustrative; check help for exact syntax)
ibmcloud is security-group-create lb-sg --vpc my-vpc
ibmcloud is security-group-create app-sg --vpc my-vpc
ibmcloud is security-group-create db-sg --vpc my-vpc
# Capture the group IDs for use in rules
LB_SG_ID=$(ibmcloud is security-groups --output json | jq -r '.[] | select(.name=="lb-sg") | .id')
APP_SG_ID=$(ibmcloud is security-groups --output json | jq -r '.[] | select(.name=="app-sg") | .id')
DB_SG_ID=$(ibmcloud is security-groups --output json | jq -r '.[] | select(.name=="db-sg") | .id')
# Allow the app tier to receive TCP 8080 only from the lb tier
ibmcloud is security-group-rule-add $APP_SG_ID inbound tcp \
--remote $LB_SG_ID \
--port-min 8080 --port-max 8080
# Allow the db tier to receive TCP 5432 only from the app tier
ibmcloud is security-group-rule-add $DB_SG_ID inbound tcp \
--remote $APP_SG_ID \
--port-min 5432 --port-max 5432
Attach lb-sg to the public entry point's network interface, app-sg to the application VSI, and db-sg to the database VSI. The exact CLI syntax for attaching a security group to an existing interface varies by generation and resource state; check ibmcloud is instance-network-interface-update --help or the VPC API reference for your region.
Validating the allow-list
Test the allowed path from the app VSI to the database private IP on port 5432. Then test the denied path from an unauthorized VSI in the same VPC.
# From the app VSI (expected: connection succeeds)
nc -vz <db-private-ip> 5432
# From an unauthorized VSI (expected: timeout)
nc -vz <db-private-ip> 5432
List the rules and attachments to confirm the configuration matches intent. Use the VPC console or the ibmcloud is list commands to confirm the rule set and the instances or interfaces attached to each security group.
If flow logs are enabled for the VPC, check the collector for the test window. The database port should show accepted entries only from the app tier and rejected entries from other sources.
Limits and edge cases
- Quota-based limits: Security groups per VPC, rules per group, interfaces per group, and instances per group are region- and account-specific quotas. Check the VPC quotas in the IBM Cloud console or CLI before designing a large mesh.
- VPC-scoped: Security groups are scoped to a single VPC. Referencing another security group does not create cross-VPC reachability; you still need Transit Gateway or another connectivity mechanism.
- IPv6: An IPv4-only allow rule will not match IPv6 sources. If the workload uses IPv6, create rules for the correct IP version.
- Stateful, not stateless: Security groups are not network ACLs. Do not expect them to provide subnet-level stateless filtering or logging in the same way.
- Not a substitute for other controls: Security groups control network reachability only. They do not replace IAM policies, host-based firewalls, or application authentication.
Common mistakes
- Public IP on the database tier: A public address on a database VSI defeats the east-west allow-list. Keep the database on a private subnet with no public gateway route.
- 0.0.0.0/0 for management ports: Using the internet-wide CIDR for SSH or RDP exposes the tier to the public internet. Restrict management access to a bastion security group or specific corporate CIDRs.
- Assuming outbound is denied: The default inbound posture is deny, but outbound behavior depends on the rules present. An overly broad outbound allow in a template can permit data exfiltration.
- Changing rules without checking attachments: Tightening a rule can break load-balancer health checks or orchestration agents. Confirm which instances and interfaces are attached before editing.
- Forgetting new instances: New VSIs inherit only the security groups attached at creation. Repeat the attachment check after scaling events or image changes.
Rolling back the configuration
Because the commands above change state, remove the rules and groups if the pattern is not needed:
# Remove rules first (illustrative; confirm syntax with command help)
ibmcloud is security-group-rule-delete <rule-id> --security-group $DB_SG_ID
ibmcloud is security-group-rule-delete <rule-id> --security-group $APP_SG_ID
# Detach from interfaces, then delete groups
ibmcloud is security-group-delete $DB_SG_ID
ibmcloud is security-group-delete $APP_SG_ID
ibmcloud is security-group-delete $LB_SG_ID
Deleting a security group fails if it is still attached to a network interface. Detach first, then delete.
A practical way to check the result
After applying the pattern, run the two nc tests and compare them with the flow log entries. The allowed path should succeed quickly; the denied path should time out. If the denied path succeeds, check whether the unauthorized VSI shares a security group with the app tier or whether an ACL is permitting the traffic. If the allowed path fails, verify that the app VSI's interface is actually attached to app-sg and that the database is listening on the private interface.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.