Provisioning KMS Keys for Encrypted Raster Buckets
Encrypting a raster archive is one line of configuration and two decisions that are much harder to reverse: which key, and who may use it. Get the first wrong and you have an archive that cannot be shared, replicated or restored into another account. Get the second wrong and you have either an archive nobody can read or a key that grants far more than the bucket does. There is also a cost dimension specific to raster work — Cloud Optimized GeoTIFF access issues many small range reads, and a naive configuration charges a key-service request for each one. This guide extends Encryption and Key Management for Spatial Data within Network Security and Access Control.
Choosing the key granularity
Three arrangements are common and only one of them is usually right.
The provider-default key. Free, invisible, and disqualifying for anything you may need to move: an object encrypted with the provider’s default key cannot be shared with another account, and a snapshot encrypted with it cannot be shared at all. It also has no policy of yours to show an auditor and no key-usage trail attributable to your principals. Acceptable for genuinely disposable scratch data and nothing else.
One customer-managed key per bucket. Feels tidy and produces a key estate nobody can audit: dozens of keys, each with its own policy, and eventually one that nobody can prove is unused but nobody dares disable.
One key per data domain. Raster archive, operational geometry, backups. Three or four keys for a platform. The blast radius of a compromise is a domain rather than everything, the grant list on each key is short enough to review in a meeting, and revocation is possible without an outage. This is the arrangement to default to.
Prerequisites and environment assumptions
Terraform 1.6 or later with hashicorp/aws pinned at ~> 5.60. Distinct principals for key administration and key use, because the whole value of a key policy comes from separating them. A list of the workloads that read the archive — renderers, batch jobs, functions, analyst roles — since each needs a decrypt grant and each will produce a confusing access-denied error if it is missed.
Understand the two-permission structure before writing anything. Reading an encrypted object requires s3:GetObject on the bucket and kms:Decrypt on the key, and these live in different policies with different owners. An access-denied error on a raster read is therefore ambiguous by default, which is why the triage habit is to check the key policy immediately after the bucket policy.
Step-by-step provisioning
-
Create the domain key with automatic rotation and a long deletion window. Automatic rotation keeps previous key material, so objects encrypted years ago stay readable — the property that makes rotation safe. Thirty days of deletion window is not bureaucracy; it is the interval in which a quarterly job that still reads the archive fails loudly while the key is still recoverable.
-
Write a policy that separates administration from use. The admin statement can disable and schedule deletion but must not include
Decrypt. The workload statement can decrypt but must not include any policy-modifying action. Collapsing them into one statement is the most common key-policy mistake and it removes the point of having a policy. -
Bind the workload grant to the service with
kms:ViaService. Without it, a compromised workload role can use the key against any resource type in the account rather than only the bucket it was provisioned for. -
Enable bucket keys on the encryption configuration. This is the raster-specific detail. Cloud Optimized GeoTIFF reads are many small range requests against large objects, and without bucket keys each one is a separate key-service call. Enabling them collapses those into far fewer calls, which is both a cost and a latency improvement on exactly the access pattern raster serving depends on.
-
Deny unencrypted transport and unencrypted writes at the bucket. A
Denyons3:PutObjectwhere the encryption header is absent or names a different key prevents an object being written outside the domain key by a misconfigured client, which is how an archive ends up with a mixture nobody notices until a restore.
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.60" }
}
}
variable "key_admin_role_arn" { type = string }
variable "reader_role_arns" { type = list(string) }
resource "aws_kms_key" "raster" {
description = "Domain key: raster archive"
# Automatic rotation retains previous key material, so objects written years
# ago stay readable. Manual rotation does not, and that difference is what
# makes historical archives unreadable after a well-meaning cleanup.
enable_key_rotation = true
deletion_window_in_days = 30
tags = {
Domain = "raster-archive"
RotationModel = "automatic-annual"
}
}
data "aws_iam_policy_document" "raster_key" {
# Administration WITHOUT decrypt. An admin who can also read the data is not
# a separation of duties, it is one role wearing two names.
statement {
sid = "Administration"
effect = "Allow"
actions = [
"kms:Describe*", "kms:List*", "kms:Get*",
"kms:EnableKeyRotation", "kms:PutKeyPolicy", "kms:TagResource",
"kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion", "kms:DisableKey",
]
resources = ["*"]
principals {
type = "AWS"
identifiers = [var.key_admin_role_arn]
}
}
# Use WITHOUT administration, and scoped to the one service it is for.
statement {
sid = "WorkloadUse"
effect = "Allow"
actions = ["kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey"]
resources = ["*"]
principals {
type = "AWS"
identifiers = var.reader_role_arns
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["s3.${data.aws_region.current.name}.amazonaws.com"]
}
}
}
resource "aws_kms_key_policy" "raster" {
key_id = aws_kms_key.raster.id
policy = data.aws_iam_policy_document.raster_key.json
}
resource "aws_s3_bucket_server_side_encryption_configuration" "raster" {
bucket = aws_s3_bucket.raster.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.raster.arn
}
# THE raster-specific line. COG range reads are many small requests against
# large objects; without this, each one is a separate key-service call and
# the key bill can exceed the storage bill.
bucket_key_enabled = true
}
}
data "aws_iam_policy_document" "raster_bucket" {
# Stop an object being written outside the domain key by a misconfigured
# client — that is how an archive ends up with a mixture nobody notices
# until a restore fails.
statement {
sid = "DenyWrongKeyWrites"
effect = "Deny"
actions = ["s3:PutObject"]
resources = ["${aws_s3_bucket.raster.arn}/*"]
principals {
type = "*"
identifiers = ["*"]
}
condition {
test = "StringNotEqualsIfExists"
variable = "s3:x-amz-server-side-encryption-aws-kms-key-id"
values = [aws_kms_key.raster.arn]
}
}
}
data "aws_region" "current" {}
Verification
Read an object as each workload role and confirm success. Then confirm the separation actually holds: attempt a decrypt as the key administrator and require it to fail, and attempt a PutKeyPolicy as a workload role and require that to fail too. Both must fail, and if either succeeds the two statements have been collapsed somewhere.
Confirm the bucket keys optimisation is active by comparing key-service request counts against object read counts over a day — with bucket keys enabled the ratio should be dramatically below one, and a ratio near one means the setting did not apply. Finally, write an object with a deliberately different key and confirm the bucket policy denies it, which is the check that keeps the archive homogeneous.
Preventing recurrence
- Require a customer-managed key in policy-as-code. A rule failing any bucket or database created without an explicit key removes the provider-default option entirely, which is the outcome you want.
- Include the key in the restore drill. Encryption failures are silent until you read the ciphertext, and only a restore proves the grants still work.
- Record the rotation model on the key. An operator deciding whether an old key is safe to disable needs to know whether prior material is retained, and that research is exactly what nobody does under time pressure.
- Review grant lists when a workload is decommissioned. A decrypt grant for a service that no longer exists is a permission nobody is watching.
Frequently Asked Questions
Why can I list objects but not download them?
Almost always a missing kms:Decrypt on the key rather than a missing s3:GetObject on the bucket. Listing does not touch the key; reading does. Check the key policy second, immediately after the bucket policy.
Do bucket keys weaken the encryption?
No. They change how often a data key is requested from the key service, not the strength of the encryption applied to the object. For a workload issuing many range reads against large rasters, they are the difference between a modest key bill and one that exceeds storage.
Can I share an encrypted archive with a partner account?
Yes, provided the key is customer-managed and its policy grants the partner decrypt. An archive on the provider-default key cannot be shared at all, which is the concrete reason the default key is unacceptable for anything you may want to publish or replicate.
Should the tile cache use the same key as the archive?
Yes, if the tiles are derived from restricted geometry. Derived data contains the source data, quantised but legible, and keeping the derived copy inside the same audit and revocation boundary is what makes an answer to “who could read this” true rather than approximately true.
Related
- Encryption and Key Management for Spatial Data — the parent topic covering all encryption boundaries
- Rotating KMS Keys Without Breaking Tile Pipelines — what happens to this key over time
- Object Storage for Raster and Vector Data — the bucket this key protects
- Debugging Access Denied on Cross-Account Raster Buckets — the full triage for the ambiguous error