Responding to an Exposed PostGIS Port Incident
You have just discovered that a security group left PostGIS 5432 open to 0.0.0.0/0, and the clock is now running on a spatial database that holds cadastral records, tenant boundaries, or survey geometry reachable from the open internet. This runbook walks the full incident lifecycle — detection, containment through code rather than the console, credential rotation, access auditing, and permanent closure — as the emergency companion to Security Group Hardening within the broader Network Security & Access Control framework. The stakes are specific to geospatial estates: an exposed PostGIS instance is not just a data-loss risk but a route to poisoning the authoritative geometry that every downstream tile renderer and OGC endpoint trusts.
Symptom Identification and Triage
An exposed-port incident surfaces from one of three sources, and each carries a different confidence level about whether the port was actually reached, not just reachable:
- AWS Config rule flag: the managed rule
restricted-common-portsorvpc-sg-open-only-to-authorized-portsreports aNON_COMPLIANTsecurity group with an ingress rule pairing5432with0.0.0.0/0or::/0. This proves reachability from the config plane but says nothing about traffic. - GuardDuty finding: a
Recon:EC2/PortProbeUnprotectedPortorUnauthorizedAccess:EC2/RDPBruteForce-style finding names the database ENI, confirming that external hosts are actively probing the open port. Treat any GuardDuty finding on5432as a live-traffic incident, not a theoretical one. - External scan confirmation: an
nmap -Pn -p 5432 <public-ip>from outside the VPC returnsopenand a PostgreSQL banner. This is the definitive proof the port answers to the internet.
Triage classifies the exposure by whether the data plane was touched. Cross-reference the security group’s AuthorizeSecurityGroupIngress event in CloudTrail (who added the rule, when, from which principal) against RDS connection metrics and VPC flow logs for the database ENI. Three outcomes follow, and the branch you land on sets the containment urgency.
Prerequisites and Environment Assumptions
Before you touch anything, confirm the operating context this runbook assumes:
- Tooling. Terraform
>= 1.6with thehashicorp/awsprovider pinned at~> 5.60, the same pinning used across Security Group Hardening. The AWS CLI v2 for CloudTrail and flow-log queries. Contain through the pipeline, not the console — a console hotfix desynchronizes state and is exactly the class of change that opened the port in the first place. - State backend. A locked remote backend so the emergency
applycannot race a scheduled pipeline run. Concurrent applies during an incident are how a half-authorized ruleset lingers. - IAM. A human-gated break-glass role holding
ec2:RevokeSecurityGroupIngress,ec2:AuthorizeSecurityGroupIngress,secretsmanager:UpdateSecret/PutSecretValue,rds:ModifyDBInstance, and read access to CloudTrail and flow logs. The default CI/CD runner identity must never carry revoke rights — that boundary is defined in IAM Role Mapping for GIS. - Log coverage. VPC flow logs enabled on the database subnet and CloudTrail management events retained — without them you cannot distinguish “latent” from “confirmed breach,” and you must then treat the incident as a breach by default.
Step-by-Step Remediation
Contain first, investigate second, harden last. The order matters: leaving the port open while you assemble a perfect audit trail extends the exposure window with no benefit.
-
Contain through code, not the console. The instinct to click “Edit inbound rules” and delete the offending rule is wrong — it fixes the account while leaving the codebase describing an open port, so the next
applyre-opens it. Instead, correct the rule in Terraform and apply immediately with a targeted plan. If the pipeline is too slow for the blast radius, use the break-glass role to revoke via CLI and then land the same change in code in the same hour, treating the CLI action as a stopgap that state must catch up to.# Stopgap only if the pipeline cannot land in time — mirror into IaC immediately after. aws ec2 revoke-security-group-ingress \ --group-id sg-0abc123postgis \ --protocol tcp --port 5432 --cidr 0.0.0.0/0 -
Rotate every credential the exposed database could authenticate. Whether the classification is “probed” or “confirmed,” rotate the PostGIS master password and any application role passwords, because a probe you cannot fully rule out is a probe you must assume succeeded. Rotate in the secrets store so the new value propagates to consumers rather than editing the database by hand.
-
Audit who connected. Reconstruct the access history from
pg_stat_activity(for live sessions), the PostgreSQL log exports in CloudWatch (for authentication attempts and source IPs), and VPC flow logs (for the packet-level ACCEPT records). Any source IP outside your VPC CIDR or egress NAT range on5432is an external connection that must be accounted for. -
Close the rule permanently and restore tiered trust. Replace the open rule with a security-group-ID reference from the rendering tier plus a scoped analytics CIDR, exactly as the tiered model in Security Group Hardening prescribes. The database should accept
5432only from workload identities, never from a raw internet range. -
Rotate credentials without downtime for consumers. Coordinate the password change with connected tile renderers so the rotation does not itself cause an outage — the dual-secret pattern in Rotating PostGIS Credentials in Terraform Without Downtime lets old and new credentials overlap during the cutover.
The corrected, containment-grade ingress rule in Terraform — an explicit revoke of the open range expressed as its absence, and a scoped replacement:
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}
variable "rendering_sg_id" { type = string }
variable "analytics_subnet_cidr" { type = string }
# The open 0.0.0.0/0 rule is DELETED by removing it from code — do not keep it.
# PostGIS 5432 is now reachable only from the rendering tier's workload identity.
resource "aws_vpc_security_group_ingress_rule" "postgis_from_rendering" {
security_group_id = aws_security_group.data_plane.id
description = "PostGIS 5432 from rendering tier only (post-incident)"
ip_protocol = "tcp"
from_port = 5432
to_port = 5432
referenced_security_group_id = var.rendering_sg_id
}
resource "aws_vpc_security_group_ingress_rule" "postgis_from_analytics" {
security_group_id = aws_security_group.data_plane.id
description = "PostGIS 5432 from scoped analytics subnet only"
ip_protocol = "tcp"
from_port = 5432
to_port = 5432
cidr_ipv4 = var.analytics_subnet_cidr
}
resource "aws_security_group" "data_plane" {
name = "gis-postgis-prod"
description = "PostGIS - rendering + analytics only, never public"
vpc_id = var.vpc_id
lifecycle { prevent_destroy = true }
}
variable "vpc_id" { type = string }
Verification
Do not close the incident on the assumption that the apply worked. Prove each layer is contained:
- Port closed from outside. Re-run
nmap -Pn -p 5432 <public-ip>from a host outside the VPC and confirmfilteredorclosed, notopen. This is the single most important check — it proves the internet can no longer reach the port. - No open rule remains.
aws ec2 describe-security-groups --group-ids sg-0abc123postgisshould show no ingress rule with0.0.0.0/0or::/0on5432. Aterraform planmust reportNo changes, confirming code and account agree. - Config compliant. The
restricted-common-portsrule should re-evaluate toCOMPLIANTfor the group; force an evaluation rather than waiting for the next scheduled sweep. - Credentials rotated and live. Confirm the new secret version is
AWSCURRENTand that a tile renderer authenticates with it — aSELECT postgis_full_version();through the rendering path proves the PostGIS Cluster Provisioning baseline still answers on the new credential. - No lingering external sessions.
SELECT client_addr, usename, state FROM pg_stat_activity;returns only internal addresses.
Preventing Recurrence
Encode the fix so a 0.0.0.0/0 rule on 5432 cannot merge again:
- Policy-as-code gate. Add an OPA/Conftest or
checkovrule to the pull-request pipeline that fails any plan pairing a data-plane port (5432,6379) with0.0.0.0/0or::/0. This blocks the highest-frequency regression before it reaches the account, the same gating philosophy detailed in Hardening Security Groups for PostGIS Ports. - Auto-remediating Config rule. Wire the
restricted-common-portsrule to an SSM automation document that revokes the offending rule on detection, so a console edit is closed in minutes even before a human responds. - Scheduled drift detection. A nightly
terraform plan -detailed-exitcodesurfaces any out-of-band rule as a non-zero exit and pages the owning team, rather than waiting for the next deploy. - Least-privilege revoke rights. Keep
AuthorizeSecurityGroupIngress/RevokeSecurityGroupIngressoff the CI/CD runner and behind the human-gated break-glass role, and route every security-group mutation to a central audit log so the next incident has a clean trail from the first minute.
Frequently Asked Questions
Should I fix the open port in the console first to stop the bleeding?
Only as a break-glass stopgap, and only if the pipeline genuinely cannot land the change in time. A console revoke leaves the codebase still describing an open port, so the next apply re-opens it. If you must use the CLI to contain immediately, land the identical change in Terraform within the same hour so state catches up and the rule cannot reappear.
Do I have to rotate credentials if flow logs show no successful connection?
If you have complete flow-log and PostgreSQL authentication coverage and both show no external success, rotation is precautionary rather than mandatory. If log coverage has any gap, treat the exposure as a breach and rotate — you cannot prove a negative without the logs.
How do I rotate the PostGIS password without dropping live tile renderers?
Use a dual-secret overlap: publish the new credential alongside the old, let consumers pick it up, then retire the old value. The full pattern is in Rotating PostGIS Credentials in Terraform Without Downtime, and it is what keeps the rotation itself from becoming a second outage.
Why reference the rendering security group by ID instead of a CIDR?
A security-group-ID reference anchors trust to the workload, so re-IPing or scaling the rendering tier never accidentally opens the database to a stale range. CIDR is reserved for the one tightly scoped analytics subnet that has no security group of its own.
Related
- Security Group Hardening — the parent guide on codifying network boundaries around spatial workloads.
- Hardening Security Groups for PostGIS Ports — the port-level deep dive whose rules prevent this incident.
- Rotating PostGIS Credentials in Terraform Without Downtime — the zero-downtime rotation this runbook calls in step five.
- IAM Role Mapping for GIS — least-privilege boundaries for the break-glass revoke role.
- PostGIS Cluster Provisioning — the database whose exposure this runbook contains.