Network Security and Access Control for Production Spatial Infrastructure

Network security and access control form the operational backbone of any production-grade geospatial platform. When map workloads — high-throughput tile caches, OGC service endpoints, batch vector pipelines, and spatial databases — are provisioned as part of the broader Spatial Infrastructure as Code discipline, the legacy perimeter model must be retired in favor of explicit, version-controlled boundaries. Terraform and Pulumi provide the mechanisms to codify those boundaries, but operational maturity arrives only when network topology, identity mapping, and ingress policy are treated as first-class spatial assets rather than console afterthoughts. This domain anchors a set of concrete practices — from VPC Routing for Tile Servers that keeps public map endpoints off the internal data plane, to IAM Role Mapping for Geospatial Workloads that binds ephemeral identities to specific raster buckets and PostGIS instances. The sections below cover why geospatial network security is uniquely demanding, the declarative and programmatic patterns that encode it, and the state, cost, and portability concerns that keep it reproducible at scale.

Network security and access control domain overview Map clients reach a public ALB or CDN that enforces CORS and CSP, which forwards only HTTPS 443 traffic to tile servers in a private subnet. Tile servers reach PostGIS on port 5432 through a security-group-to-security-group rule and read object storage over a gateway VPC endpoint instead of a NAT gateway. A dashed IAM role boundary scopes the data tier to a schema and bucket prefix. A surrounding dashed audit plane streams VPC flow logs, CloudTrail events, and security-group mutations to an immutable log sink in a dedicated security account. Audit plane — VPC flow logs · CloudTrail · security-group mutations Map client / browser Public subnet Public ALB / CDN 443 edge CORS + CSP at edge Private subnets — no 0.0.0.0/0 ingress Tile servers WMS · WFS · WMTS IAM role scope → schema · bucket prefix PostGIS · 5432 Object storage COG · raster reads 443 only 5432 · SG-to-SG VPC endpoint · no NAT Immutable log sink dedicated security account

Architectural Context: Why Geospatial Network Boundaries Are Hard

Geospatial traffic profiles are inherently asymmetric, and that asymmetry breaks the assumptions baked into general-purpose network templates. A public-facing tile server emits predictable, high-volume egress — 256×256 PNG or vector tiles fanned out across a pyramid of zoom levels — while the analytical layer behind it demands low-latency, strictly internal communication with PostGIS clusters and object storage. A flat network that treats both alike introduces lateral-movement risk and, just as damaging, unpredictable cross-AZ data-transfer charges as tile renders pull geometries across availability-zone boundaries on every cache miss.

Several characteristics make this domain distinct from a conventional three-tier web application:

  • OGC endpoints are public by contract. WMS, WFS, WMTS, and OGC API – Tiles services are designed to be consumed by third-party clients, embedded maps, and federated catalogs. The attack surface cannot simply be hidden behind a VPN; it must be exposed deliberately and then constrained at the request, header, and origin level rather than the network level alone.
  • Raster I/O dominates the data plane. Cloud Optimized GeoTIFF (COG) reads issue many small ranged GET requests against object storage. Routing that traffic over a NAT gateway instead of a gateway VPC endpoint silently converts internal reads into metered egress, inflating both latency and bill.
  • Spatial indexes change the blast radius math. A single compromised credential with read access to a PostGIS instance can exfiltrate an entire cadastral or imagery catalog in one COPY (SELECT ...) TO STDOUT. Identity scope must be narrowed to the table and schema level, not just the database.
  • Multi-tenant SaaS and agency boundaries overlap. A platform serving multiple agencies must isolate tenant data planes while sharing the same rendering tier, which pushes isolation logic into security groups, IAM conditions, and CORS allowlists simultaneously.

These constraints mean that network security for spatial platforms is not a single control but a layered system: a public edge governed by browser-level policy, a private data plane governed by routing and security groups, and an identity fabric that scopes every actor to the narrowest possible spatial resource. The remainder of this guide treats each layer as code.

Core IaC Patterns: Declarative and Programmatic Network Boundaries

The central decision in encoding network security is the same trade-off that runs through all of spatial IaC — declarative configuration versus programmatic orchestration. The broader analysis in Terraform vs Pulumi for GIS applies directly here: HCL gives you a static, reviewable resource graph with strong schema validation, which suits security groups and route tables where every rule should be obvious in a diff; a general-purpose language gives you loops, typed inputs, and conditional topology, which suits dynamically generated allowlists driven by service discovery.

In practice, the highest-leverage pattern is to reference resource identities rather than CIDR literals. A security group that allows the tile-rendering tier to reach PostGIS should reference the renderer’s security-group ID, not a hard-coded subnet range. This produces an implicit, self-healing allowlist: when the rendering tier scales or moves subnets, the rule still holds, and a reviewer reading Security Group Hardening practices can verify intent at a glance. Broad 0.0.0.0/0 ingress on any port other than the public 443 edge is treated as a policy violation, not a convenience.

The following Terraform configuration is the representative pattern for this domain: a hardened data-plane security group plus the route table that keeps tile traffic off the public path. Note the pinned provider, the SG-to-SG reference, and the explicit gateway VPC endpoint route that prevents raster reads from leaking onto a NAT gateway.

terraform {
  required_version = ">= 1.10.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.60"
    }
  }
}

# Security group for the PostGIS data tier — reachable ONLY from the
# tile-rendering tier, never from the public internet.
resource "aws_security_group" "postgis" {
  name_prefix = "postgis-${var.environment}-"
  vpc_id      = var.vpc_id
  description = "PostGIS spatial DB — tile renderers only"

  ingress {
    description     = "PostgreSQL/PostGIS from renderer tier"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.tile_renderer.id] # implicit allowlist
  }

  egress {
    description = "Stateful return traffic only"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = [var.vpc_cidr] # confined to the VPC, no public egress
  }

  tags = { ManagedBy = "IaC", Tier = "data", Environment = var.environment }
}

# Private route table: S3 raster reads stay on the gateway endpoint,
# not the NAT gateway — avoids converting COG reads into metered egress.
resource "aws_route_table" "private" {
  vpc_id = var.vpc_id
  tags   = { ManagedBy = "IaC", Tier = "private", Environment = var.environment }
}

resource "aws_vpc_endpoint_route_table_association" "s3" {
  route_table_id  = aws_route_table.private.id
  vpc_endpoint_id = var.s3_gateway_endpoint_id
}

The same intent expressed programmatically in Pulumi trades HCL’s flat clarity for typed inputs and explicit dependency edges, which matters when ingress rules are generated from a list of trusted peer services discovered at deploy time:

// Pin the provider in package.json: "@pulumi/aws": "6.45.0"
import * as aws from "@pulumi/aws";

const postgisSg = new aws.ec2.SecurityGroup("postgis", {
  vpcId: vpcId,
  description: "PostGIS spatial DB — tile renderers only",
  ingress: [{
    description: "PostgreSQL/PostGIS from renderer tier",
    fromPort: 5432,
    toPort: 5432,
    protocol: "tcp",
    securityGroups: [tileRendererSg.id], // SG reference, not a CIDR literal
  }],
  egress: [{
    fromPort: 0, toPort: 0, protocol: "-1",
    cidrBlocks: [vpcCidr], // VPC-confined, no public egress
  }],
  tags: { ManagedBy: "IaC", Tier: "data" },
}, { dependsOn: [tileRendererSg] }); // explicit ordering: renderer SG first

Browser-level controls belong in this same codified layer. Public tile and feature APIs must ship their CORS & CSP Configuration alongside the load balancer or CDN that fronts them, so that WMS/WFS responses and Mapbox GL JS clients only accept requests from authorized origins. Provisioning those headers as code — rather than hand-editing a listener rule — keeps the allowlist in the same review flow as the network rules it complements.

State, Security, and the Access Dependency Graph

Network and identity resources produce some of the most sensitive state in any spatial deployment: VPC peering IDs, IAM role ARNs, KMS key references, and endpoint policy documents. That state must be isolated per environment and encrypted at rest, which is exactly the concern addressed by State Backend Selection. A staging tile-server network change should never be able to read or overwrite production security-group state; environment-scoped backends with mandatory locking enforce that boundary. HashiCorp’s own state lifecycle guidance is the baseline — state files must never be stored in plaintext or shared across untrusted CI runners.

Identity boundaries are the other half of the security graph. GIS environments routinely process proprietary imagery, cadastral records, and real-time sensor streams, each carrying a distinct classification. Embedding static credentials in an application layer is a critical anti-pattern that both fractures reproducibility and violates least privilege. Instead, infrastructure code should bind ephemeral service roles directly to spatial resource scopes, the approach detailed under IAM Role Mapping for Geospatial Workloads. When those policies are codified, drift detection becomes deterministic: any unauthorized permission escalation surfaces during terraform plan or pulumi preview rather than during an incident.

Ordering matters because these resources are mutually dependent. A VPC must exist before its subnets; subnets before route tables and endpoints; security groups before the instances that attach them; IAM roles before the policies that bind them to buckets. Implicit dependency resolution will usually infer this, but for cross-cutting edges — a security group rule that references a peered VPC, or a role whose trust policy names an OIDC provider created in the same apply — explicit depends_on (Terraform) or dependsOn (Pulumi) prevents out-of-order races during parallel provisioning.

The topology below shows how public ingress is isolated from the private data plane, with every boundary expressed as version-controlled routing and security-group rules:

Public ingress isolated from the private data plane A map client reaches an internet gateway, which connects to a public ALB or reverse proxy inside the public subnet. The ALB forwards only HTTPS 443 traffic into the private subnets, where tile servers serving WMS and WFS sit. Tile servers connect to PostGIS on port 5432 over a private path and read object storage through a gateway VPC endpoint. PostGIS and object storage have no public ingress. Map client / browser Internet gateway Public subnet Public ALB / proxy Private subnets — no public ingress Tile servers WMS / WFS PostGIS · 5432 Object storage 443 only 5432 private VPC endpoint

Beyond state and ordering, audit is non-negotiable. Every VPC flow log, IAM role assumption, and security-group mutation should land in a centralized, immutable log store, ideally routed through a dedicated security account with lifecycle policies and automated alerting. Pairing centralized logging — as described in the AWS CloudTrail security best practices — with anomaly detection is what keeps a deployment aligned with frameworks like NIST SP 800-53 and FedRAMP as the network topology evolves.

Cost Governance and Drift Management

Network design is a cost-control discipline as much as a security one, because the most expensive mistakes in a spatial platform are usually pathing mistakes. Three patterns dominate the bill:

  • Cross-AZ data transfer on cache misses. When a tile renderer in one availability zone reads geometries from a PostGIS primary in another, every miss incurs inter-AZ transfer. Pinning the read replica and renderer to the same zone, or adding a zone-aware cache, turns a variable cost into a flat one.
  • NAT gateway egress for object reads. As noted above, raster and vector reads against object storage should traverse a gateway VPC endpoint. A missing endpoint route is the single most common cause of a surprise NAT bill on a COG-heavy workload.
  • Idle public load balancers and orphaned Elastic IPs. Per-environment teardown left incomplete leaves billable network edges running with no traffic.

These patterns are tractable when network changes pass through the cost gates described in Cost Estimation Frameworks. A pre-apply estimate that flags a new NAT gateway or a cross-region peering link before merge prevents the spend rather than discovering it on the next invoice; the practical wiring of that gate is covered in Cost Tracking Spatial Infrastructure with Infracost.

Drift is the security analogue of cost overrun. A security-group rule widened by hand during an incident, a route added to debug connectivity, or an IAM policy temporarily loosened — all silently diverge the running network from its declared state. Scheduled drift detection (a periodic terraform plan -detailed-exitcode or pulumi preview --expect-no-changes in CI) converts these from invisible liabilities into actionable diffs. For network resources specifically, drift detection should run often enough that an out-of-band 0.0.0.0/0 rule cannot survive more than one cycle.

Modularization and Portability

A production network baseline should be a reusable module with a narrow, well-documented interface — not copy-pasted per service. The discipline of Module Design Patterns applies directly: a spatial-network module exposes typed inputs (VPC CIDR, trusted origin list, environment) and typed outputs (private route table ID, data-tier security-group ID, S3 gateway endpoint ID) that downstream provisioning consumes. This keeps the security contract in one audited place while Geospatial Resource Provisioning modules attach to it by reference.

# Consuming the network baseline by interface, not by copy-paste.
module "spatial_network" {
  source         = "./modules/spatial-network"
  environment    = var.environment
  vpc_cidr       = "10.20.0.0/16"
  trusted_origins = ["https://maps.example.gov", "https://atlas.example.gov"]
}

# Downstream tiles/DB attach to the baseline's exported identities.
module "tile_tier" {
  source                = "./modules/tile-tier"
  data_security_group_id = module.spatial_network.data_sg_id
  private_route_table_id = module.spatial_network.private_rt_id
}

Portability is the other reason to keep the boundary modular. Vendor lock-in is a real risk for agencies managing petabyte-scale catalogs, and the network layer is where cloud-specific primitives concentrate. Abstracting VPCs, firewall rules, and private endpoints behind a portable module interface — and standardizing the data plane on open formats like GeoParquet and COG accessed through OGC API standards — lets the same logical topology target a different provider without rewriting every consumer. The cloud becomes an interchangeable execution environment rather than an architectural anchor, while the security contract that callers depend on stays stable.

Operational Maturity Checklist

  • State security: Network and IAM state lives in environment-scoped, encrypted remote backends with mandatory locking; no plaintext ARNs or peering IDs in shared state or CI logs.
  • Identity scope: Every actor binds to the narrowest spatial resource — bucket prefix, schema, or table — via codified IAM roles, with zero long-lived shared keys in application layers.
  • Network hygiene: Data-tier security groups reference peer SG IDs rather than CIDR literals; 0.0.0.0/0 exists only on the deliberate public 443 edge; raster reads route over a gateway VPC endpoint.
  • Policy enforcement: Pre-apply cost gates flag new NAT gateways and cross-region links; policy-as-code rejects broad ingress before merge.
  • Drift and audit: Scheduled drift detection runs often enough to catch out-of-band rule changes within one cycle; VPC flow logs, CloudTrail, and SG mutations stream to an immutable, alerted log store.
  • Portability: The network baseline is a reusable module with typed inputs and outputs, decoupled from provider-specific resource names wherever the data plane allows.

When network topology, identity, ingress policy, and audit are all treated as version-controlled spatial assets, a geospatial platform gains the reproducibility, isolation, and cost transparency that enterprise and agency deployments require. The boundary stops being a manual liability and becomes a deterministic, reviewable part of the same pipeline that ships the maps.