Scoping Permission Boundaries for GIS Analyst Roles
A GIS analyst needs a genuinely awkward set of permissions. They must read imagery archives that cost real money to transfer, query a spatial database that holds authoritative geometry, launch compute large enough to reproject a national coverage, and write results somewhere colleagues can find. Granting that as a policy attached directly to their role produces something close to administrator; granting less produces a ticket queue and an analyst who works around the platform instead of on it. A permission boundary resolves this by separating what a role may be granted from what it is granted, which lets you delegate the granting safely. This guide extends IAM Role Mapping for GIS within Network Security and Access Control.
How a boundary differs from a policy
A permission boundary is a ceiling, not a grant. Attaching one to a role grants nothing; it constrains the maximum any identity policy on that role can achieve. Effective permissions are the intersection of the identity policy and the boundary, so a role with an administrator policy and a narrow boundary has narrow permissions.
That indirection is what makes delegation safe. A platform team can allow analysts — or an analyst-facing self-service tool — to create and modify their own roles, provided every role they create carries the boundary. The boundary then guarantees that no matter what policy is attached, the resulting role cannot delete a production database, cannot write to the authoritative geometry bucket, and cannot escalate its own privileges by editing the boundary itself.
The escalation clause is the part most often missed. A boundary that permits iam:* allows an analyst to detach their own boundary, at which point it protects nothing. Every boundary must explicitly deny the IAM actions that would let a principal modify boundaries, policies or roles.
Prerequisites and environment assumptions
Terraform 1.6 or later with hashicorp/aws pinned at ~> 5.60. A clear inventory of what analysts legitimately need, which is harder than it sounds and is best assembled from access logs rather than from a meeting. Tagged resources, because the most useful boundary conditions are tag-based — a boundary that permits writes only to resources tagged as a scratch environment depends entirely on those tags being reliable, which is the argument in FinOps Tagging Strategies for Geospatial Resources.
Decide the analyst’s write territory before writing any policy. The clean answer for a spatial platform is: read broadly across published imagery and published geometry, write only into a scratch prefix and a scratch schema that carry no retention guarantee, and never touch the authoritative store. That single sentence generates most of the boundary.
Step-by-step implementation
-
Enumerate the allowed services at the boundary. A boundary is a ceiling, so start by listing the services an analyst may ever use — object storage, the database, the compute service, the metrics service — and permit nothing outside that list. Everything not enumerated is denied by omission, which is the correct default here.
-
Constrain writes by prefix and by tag. Reads can be broad; writes must be narrow. Permit
s3:PutObjectonly under the scratch prefix, and permit compute actions only on resources carrying the analyst’s team tag. This is where a boundary earns its keep, because it survives whatever policy someone attaches later. -
Deny the escalation paths explicitly.
iam:DeleteRolePermissionsBoundary,iam:PutRolePermissionsBoundaryon their own role,iam:CreatePolicyVersion, andiam:AttachRolePolicyall belong in an explicit deny. An explicit deny cannot be overridden by any grant. -
Deny the expensive irreversible actions regardless of tags. Deleting a database cluster, deleting a bucket, disabling a key — none of these should be reachable by an analyst role even in a scratch environment, because scratch environments are where the tag is most likely to be wrong.
-
Require the boundary on any role the analyst can create. If you are delegating role creation, the delegating policy must include a condition requiring the boundary be attached, or the delegation immediately defeats itself.
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.60" }
}
}
variable "imagery_bucket_arn" { type = string }
variable "scratch_bucket_arn" { type = string }
data "aws_iam_policy_document" "analyst_boundary" {
# Read broadly across published spatial data. Reads are the analyst's job.
statement {
sid = "ReadPublishedSpatialData"
effect = "Allow"
actions = ["s3:GetObject", "s3:ListBucket", "s3:GetObjectVersion"]
resources = [var.imagery_bucket_arn, "${var.imagery_bucket_arn}/*"]
}
# Write narrowly: only into scratch, which carries no retention guarantee.
statement {
sid = "WriteScratchOnly"
effect = "Allow"
actions = ["s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload"]
resources = ["${var.scratch_bucket_arn}/analysts/*"]
}
# Compute, but only on resources carrying the team tag. This is where the
# tag contract stops being bookkeeping and becomes access control.
statement {
sid = "TaggedComputeOnly"
effect = "Allow"
actions = [
"batch:SubmitJob", "batch:DescribeJobs", "batch:TerminateJob",
"logs:GetLogEvents", "logs:FilterLogEvents",
"cloudwatch:GetMetricData", "cloudwatch:ListMetrics",
]
resources = ["*"]
condition {
test = "StringEquals"
variable = "aws:ResourceTag/Team"
values = ["geospatial-analysis"]
}
}
# Explicit deny beats every grant, in any policy, forever. Without this the
# analyst can detach the boundary and the ceiling disappears.
statement {
sid = "DenySelfEscalation"
effect = "Deny"
actions = [
"iam:DeleteRolePermissionsBoundary",
"iam:PutRolePermissionsBoundary",
"iam:CreatePolicyVersion",
"iam:SetDefaultPolicyVersion",
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"organizations:*",
"account:*",
]
resources = ["*"]
}
# Irreversible destruction stays out of reach even inside scratch, because
# scratch is exactly where a tag is most likely to be wrong.
statement {
sid = "DenyIrreversibleDestruction"
effect = "Deny"
actions = [
"rds:DeleteDBInstance", "rds:DeleteDBCluster", "rds:DeleteDBSnapshot",
"s3:DeleteBucket", "s3:PutBucketPolicy",
"kms:ScheduleKeyDeletion", "kms:DisableKey",
]
resources = ["*"]
}
}
resource "aws_iam_policy" "analyst_boundary" {
name = "gis-analyst-boundary"
description = "Ceiling for analyst roles. Grants nothing on its own."
policy = data.aws_iam_policy_document.analyst_boundary.json
}
resource "aws_iam_role" "analyst" {
name = "gis-analyst"
permissions_boundary = aws_iam_policy.analyst_boundary.arn
assume_role_policy = data.aws_iam_policy_document.analyst_trust.json
}
# Delegation: analysts may create their own roles, but ONLY with the boundary
# attached. Without this condition the delegation defeats itself immediately.
data "aws_iam_policy_document" "analyst_role_delegation" {
statement {
effect = "Allow"
actions = ["iam:CreateRole", "iam:PutRolePolicy"]
resources = ["arn:aws:iam::*:role/analyst-scoped/*"]
condition {
test = "StringEquals"
variable = "iam:PermissionsBoundary"
values = [aws_iam_policy.analyst_boundary.arn]
}
}
}
Verification
Test the ceiling by trying to exceed it. Assume the analyst role and attempt, in order: a read from the imagery bucket (must succeed), a write to the scratch prefix (must succeed), a write to the imagery bucket (must fail), a database cluster deletion (must fail), and an attempt to attach an administrator policy to the role (must fail). The last one is the important test, because it proves the boundary cannot be removed by the identity it constrains.
Then verify the boundary is genuinely a ceiling rather than a grant: attach a deliberately over-broad identity policy to a test role carrying the boundary and confirm the effective permissions are still narrow. The policy simulator will show the boundary as the deny reason, which is the confirmation that the intersection is working as intended.
Preventing recurrence
- Require the boundary in policy-as-code. A rule that fails any plan creating an analyst-namespace role without the boundary makes the delegation self-enforcing, using the gate described in Policy as Code for Spatial Resources.
- Review the deny list when a new service is adopted. A boundary enumerating allowed services is safe by default but becomes an obstacle as the platform grows; schedule the review rather than handling it as a series of urgent exceptions.
- Keep scratch genuinely disposable. A lifecycle rule expiring the scratch prefix is what makes the “no retention guarantee” claim true, and what stops scratch quietly becoming a second authoritative store.
- Build the inventory from access logs. The permissions people actually use differ substantially from the ones they say they need, in both directions.
Frequently Asked Questions
Does a permission boundary grant anything by itself?
No. It is purely a ceiling. A role with a boundary and no identity policy can do nothing at all. This surprises people the first time, and it is precisely the property that makes the boundary safe to attach broadly.
Why deny irreversible actions if the tag condition already limits scope?
Because tags are data and data is sometimes wrong. A scratch environment mistagged as production, or a production database mistagged as scratch, turns a tag-scoped grant into a production deletion. Irreversible actions deserve a deny that does not depend on a tag being correct.
Can analysts create their own roles safely?
Yes, with two conditions: the delegating policy must require the boundary on any role created, and it must confine creation to a dedicated path or namespace. Without the boundary condition the delegation grants role creation, which is equivalent to granting whatever the analyst is willing to write into a policy.
How does this relate to the roles a pipeline uses?
Differently, and they should not share a boundary. A pipeline role is machine-driven, narrowly scoped by design and reviewed as code; an analyst role is human-driven and needs latitude within a ceiling. The pipeline identity patterns are covered in GitHub Actions OIDC for Terraform Spatial Deploys.
Related
- IAM Role Mapping for GIS — the parent topic on identity design for spatial workloads
- Debugging Access Denied on Cross-Account Raster Buckets — triaging the denials a boundary produces
- Encryption and Key Management for Spatial Data — the key policy that also governs what an analyst can decrypt
- FinOps Tagging Strategies for Geospatial Resources — the tag contract the boundary’s conditions depend on