IAM Role Mapping for GIS Workloads
Spatial platforms fail quietly when identity is an afterthought: a tile renderer that can list every bucket in the account, a database role that drifts out of sync with its cloud identity, or a cross-account trust policy with no external ID. Mapping cloud IAM identities onto the concrete actors of a geospatial stack — tile renderers, raster ingestion workers, vector pipelines, and read-only analytics services — is the work that keeps least-privilege real as the platform grows. This resource area sits inside the broader Network Security & Access Control framework and is tightly coupled to VPC Routing for Tile Servers and Security Group Hardening: identity, network path, and port exposure must enforce the same boundary or the weakest of the three defines your actual posture. Codifying role mapping in Terraform or Pulumi turns it from a console-clicking chore into a reviewable, versioned contract that renders consistent identities across development, staging, and production.
Environment parity and configuration drift mitigation
The recurring failure in spatial IAM is divergence between environments. A gis-tile-renderer role granted broad s3:* in development “to unblock a demo” is rarely tightened before it reaches production, and a permission added by hand during an incident never makes it back into source control. Both produce drift that no diff will show you until an audit — or an attacker — finds it.
Treat the role definition as a single parameterized module instantiated once per environment. The structural shape (trust policy, attached policy documents, condition keys) stays identical; only the inputs change — bucket ARNs, allowed CIDR ranges, session duration, and the environment-scoped object prefix. This guarantees that a tightening applied in development is structurally present in production, because both render from the same code path.
- Lock the trust boundary, not just the permissions. Environment parity means the
assume_role_policyprincipal,ExternalId, and source-condition keys are identical in shape across environments — only the values differ. A role that is assumable byecs-tasks.amazonaws.comin staging must not silently become assumable by a broad*principal in production. - Scope object access by environment prefix. Raster and tile reads should be bounded to
s3://bucket/rasters/${environment}/*so a staging role physically cannot read production imagery, mirroring the prefix discipline established in Object Storage for Raster/Vector Workloads. - Pin provider versions. A minor AWS provider bump can change default IAM behaviours (for example, inline vs managed policy handling). Pinning
required_providerskeepsplanoutput deterministic across the promotion cycle. - Detect drift on a schedule, not just on apply. A nightly
terraform plan -detailed-exitcodeagainst production state surfaces console-applied permission changes as a non-zero exit before they become a standing risk.
State itself is part of parity. IAM policy documents contain account IDs, role ARNs, and condition values that should never sit in plaintext, so role modules must run against a locked remote backend — see State Backend Selection and the locking mechanics in Managing Terraform State Locks for Spatial Data for why concurrent role mutations without a lock can corrupt the recorded trust relationship.
CI/CD validation and operational guardrails
Identity changes are among the highest-blast-radius edits in a spatial platform, so they belong behind the strictest pull-request gates. The objective is to make an over-permissive policy un-mergeable, not merely discouraged.
A practical gate sequence in the pull-request pipeline:
- Static policy analysis. Run
checkovandcfn-nagover the rendered plan, and feed the candidate policy to AWS IAM Access Analyzer’s policy validation API. These catch wildcardAction/Resourcepairs, missing condition keys, and trust policies that admit overly broad principals before any apply. - Plan diffing on the IAM graph. Surface
terraform plan(orpulumi preview) output as a PR comment, with reviewers required specifically when anaws_iam_role,aws_iam_role_policy, or trust policy changes. A privilege expansion should never merge on a silent diff. - Policy-as-code assertions. Encode organisational rules — “no
s3:*on raster buckets”, “every cross-account trust must carry anExternalId”, “session duration ≤ 1 hour for renderer roles” — as OPA/Conftest or Sentinel policies that fail the build deterministically. - Rollback triggers. Because IAM mutations can lock out the very pipeline that applies them, keep the previous known-good policy document tagged in state so a revert is a single
applyof the prior revision rather than an emergency console edit.
These guardrails are the identity-layer equivalent of the pre-apply integrity checks used elsewhere on the platform: the same PR workflow that validates a spatial index or a parameter group should refuse a role that widens the blast radius.
Resource architecture and service integration
IAM role mapping is the connective tissue between the platform’s compute, storage, and data tiers, and it never operates in isolation. Each actor in a spatial stack needs a role scoped to exactly its job:
- Tile renderers (ECS tasks, Lambda, or EC2 behind a WMS/WMTS endpoint) need read-only access to the raster/COG prefix and the tile cache, plus permission to emit metrics. They should never hold write access to the source archive.
- Ingestion workers that mosaic, reproject, or pyramid imagery need scoped write access to a staging prefix and read access to the landing zone — and nothing in the served tile cache.
- Spatial databases. Where a PostGIS Cluster Provisioning deployment uses IAM database authentication, the cloud identity must map to a PostgreSQL role with matching grants: ingestion identities map to a writer role, analytics identities to a read-only role. The cloud-side and database-side grants must be provisioned together or they will drift.
- Analytics and BI services that query vector tiles or run spatial joins need read-only retrieval, ideally through a VPC endpoint rather than the public internet.
Identity also has to agree with the network path. When a renderer role is scoped to a specific gateway VPC endpoint (for example com.amazonaws.us-east-1.s3), raster retrieval never touches the public internet — but only if the route table and endpoint policy described in VPC Routing for Tile Servers actually steer that traffic to the endpoint. Likewise, browser-facing spatial APIs depend on CORS & CSP Configuration to govern client access, while the backing role governs what the API can reach server-side. The deepest treatment of programmatic, prefix-scoped storage permissions lives in Pulumi IAM Policies for S3 Raster Access, which covers the incident-response and state-recovery angle of these same roles.
Runnable configuration
The module below renders a tile-renderer role with an environment-scoped trust policy and prefix-isolated raster access. Every input is parameterized so the same code produces a tightened production identity and a sandboxed development one. Provider versions are pinned so plan output stays deterministic across the promotion cycle.
# terraform/modules/gis_iam_role/main.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
}
variable "environment" {
type = string
description = "Deployment environment (dev | staging | prod); scopes the role name and object prefix."
}
variable "allowed_cidrs" {
type = list(string)
description = "Source CIDRs permitted to assume the role (e.g. the NAT/VPC egress range for renderer tasks)."
}
variable "raster_bucket_arn" {
type = string
description = "ARN of the bucket holding source rasters and COG imagery."
}
data "aws_region" "current" {}
# Identity for the tile-rendering tier. Assumable only by ECS tasks,
# only from the platform egress range, and only in the home region.
resource "aws_iam_role" "gis_tile_renderer" {
name = "gis-tile-renderer-${var.environment}"
max_session_duration = 3600 # 1 hour: short-lived credentials for a stateless renderer
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = { "aws:RequestedRegion" = data.aws_region.current.name }
IpAddress = { "aws:SourceIp" = var.allowed_cidrs }
}
}]
})
tags = { Component = "tile-renderer", Environment = var.environment }
}
# Read-only, prefix-isolated raster access. ListBucket is constrained
# to the environment prefix so a dev role cannot enumerate prod imagery.
resource "aws_iam_role_policy" "raster_read" {
name = "raster-prefix-access-${var.environment}"
role = aws_iam_role.gis_tile_renderer.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ReadScopedRasters"
Effect = "Allow"
Action = ["s3:GetObject"]
Resource = ["${var.raster_bucket_arn}/rasters/${var.environment}/*"]
},
{
Sid = "ListScopedPrefixOnly"
Effect = "Allow"
Action = ["s3:ListBucket"]
Resource = [var.raster_bucket_arn]
Condition = {
StringLike = { "s3:prefix" = ["rasters/${var.environment}/*"] }
}
}
]
})
}
The split between GetObject (scoped to the object path) and ListBucket (scoped via the s3:prefix condition on the bucket ARN) is deliberate: granting ListBucket on the object path silently does nothing, a common mistake that leaves renderers unable to enumerate tiles while operators assume the policy is correct.
Guardrails embedded in configuration
The strongest controls are the ones baked into the resource definition rather than bolted on by a runbook:
- State locking is non-negotiable. Run the role module against a locked remote backend so two pipelines cannot mutate the same trust policy concurrently and leave state describing a relationship that does not exist in the account.
- No long-lived secrets in roles. Roles deliver temporary STS credentials by design;
max_session_durationis capped at one hour for stateless renderers so a leaked credential expires fast. Where static secrets are unavoidable (a database password for non-IAM auth), reference them from Secrets Manager and mark Terraform outputssensitive/ encrypt Pulumi config — never inline them in policy JSON. - Network isolation reinforces identity. Scope storage permissions to a VPC endpoint and pair them with an endpoint policy so that even a valid credential used from outside the expected network path is denied. Identity and route must agree.
- Permission boundaries and SCPs cap the ceiling. Codify Service Control Policies and IAM permission boundaries alongside the role so that an explicit deny survives any manual console edit — the platform’s hard upper bound on what a role can ever do.
- External IDs on every cross-account trust. A
sts:AssumeRolefrom a partner or SaaS account must require anExternalIdcondition and, where appropriate, MFA, closing the confused-deputy path between analytics and production accounts.
Troubleshooting and failure modes
- S3 prefix scope mismatch. Renderers return empty tiles or
AccessDeniedbecauseListBucketwas granted on the object path instead of the bucket ARN with ans3:prefixcondition, or because the environment prefix in the policy does not match the prefix in the request. Confirm thes3:prefixcondition matches the renderer’s actual key layout; a CloudTrailAccessDeniedonListBucketwith an empty prefix is the signature. - VPC endpoint policy gap. Identity allows the read, but the gateway endpoint’s own policy does not, so requests fail with
AccessDeniedeven though the role looks correct. The IAM policy and the endpoint policy must both permit the action — check the endpoint policy whenever a role audit shows the grant is present but reads still fail. - Trust policy region/IP skew. Tasks cannot assume the role after a region migration or a NAT/egress IP change because the
aws:RequestedRegionoraws:SourceIpcondition no longer matches. The symptom isAssumeRolefailures in CloudTrail with no matchingAllow; re-render the module with the current CIDRs. - Cross-account confused deputy. A trust policy missing its
ExternalIdlets a third party assume the role on a victim’s behalf. Audit every cross-accountassume_role_policyfor asts:ExternalIdcondition; its absence is the finding. - IAM database auth role skew. Cloud identity exists but the matching PostgreSQL role was never granted (or was dropped), so connections authenticate at the cloud layer then fail at the database with a permission error. Provision the cloud role and the database grant in the same module so they cannot drift apart.
Related
- Network Security & Access Control — the parent framework for identity, network, and web-layer controls.
- Pulumi IAM Policies for S3 Raster Access — programmatic, prefix-scoped storage roles with incident-response and state recovery.
- VPC Routing for Tile Servers — aligning route tables and endpoint policies with IAM boundaries.
- Security Group Hardening — port-level controls that must agree with identity scope.
- CORS & CSP Configuration — governing browser-side access to spatial APIs.
For provider-specific behaviour, consult the Terraform AWS Provider IAM Role documentation and the AWS IAM policy evaluation logic reference, in which an explicit deny in a boundary policy always overrides a role permission.