Passing Credentials to Remote Raster Workers
The best credential is one the worker gets from its own environment — an IAM role:
import rasterio
from rasterio.session import AWSSession
import boto3
# On a worker with an instance profile or task role: no keys anywhere in code
with rasterio.Env(AWSSession(boto3.Session(), requester_pays=True)):
with rasterio.open("s3://private-bucket/scenes/T33UVP/B04.tif") as src:
block = src.read(1, window=((0, 512), (0, 512)))
Code that reads private COGs on a laptop often fails on a cluster, because the laptop’s credentials live in files and environment variables that remote workers do not have. This page belongs to distributed processing on Coiled and AWS Batch in Cloud Execution & Orchestration.
Three Ways Workers Get Credentials
On AWS Batch, attach a job role to the job definition; on Coiled, Kubernetes or EC2-based Dask clusters, attach an instance profile or service-account role. The AWS SDK and GDAL both discover these automatically through the instance metadata service or the container credentials endpoint, and refresh them before they expire. No secret ever appears in code, configuration or the task graph.
Environment & Setup
| Package | Version pin | Used for |
|---|---|---|
boto3 |
>=1.34 |
Sessions and STS |
rasterio |
>=1.3.0 |
AWSSession for GDAL reads |
dask[distributed] |
>=2023.1 |
Worker plugins |
coiled |
>=1.0 |
Optional: managed clusters with credential forwarding |
pip install "boto3>=1.34" "rasterio>=1.3.0" "dask[distributed]>=2023.1" "coiled>=1.0"
Complete Working Example
import os
import boto3
from dask.distributed import WorkerPlugin
GDAL_OPTS = {
"AWS_REGION": "eu-central-1",
"AWS_REQUEST_PAYER": "requester",
"GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR",
"CPL_VSIL_CURL_ALLOWED_EXTENSIONS": ".tif,.tiff,.TIF",
}
class GDALEnv(WorkerPlugin):
"""Apply GDAL options on every worker. No secrets here."""
def setup(self, worker):
os.environ.update(GDAL_OPTS)
class TemporaryAWSCredentials(WorkerPlugin):
"""Fallback when workers cannot have a role: forward short-lived STS credentials."""
def __init__(self, duration_s: int = 3600):
creds = boto3.client("sts").get_session_token(DurationSeconds=duration_s)["Credentials"]
self._env = {
"AWS_ACCESS_KEY_ID": creds["AccessKeyId"],
"AWS_SECRET_ACCESS_KEY": creds["SecretAccessKey"],
"AWS_SESSION_TOKEN": creds["SessionToken"],
}
self.expires = creds["Expiration"]
def setup(self, worker):
os.environ.update(self._env)
def configure(client, forward_credentials: bool = False):
client.register_plugin(GDALEnv())
if forward_credentials:
plugin = TemporaryAWSCredentials()
client.register_plugin(plugin)
return plugin.expires
The plugin approach keeps credentials out of the task graph. Passing keys as function arguments embeds them in every serialised task, where they can end up in the scheduler’s memory, the dashboard, performance reports and error tracebacks. A plugin sends them once per worker and sets the environment that GDAL and boto3 read. Coiled offers built-in forwarding of the client’s temporary credentials, which does the same thing with automatic refresh.
GDAL Needs to Know More than Keys
A 403 from S3 looks like a permissions problem but frequently is not. Requester-pays buckets reject requests that do not declare the requester pays; public datasets read without credentials need AWS_NO_SIGN_REQUEST=YES, and mixing public and private buckets in one job requires setting that per read with rasterio.Env rather than globally. When a long job starts failing after about an hour, the cause is almost always an expired forwarded session token — the strongest argument for roles, which refresh themselves.
Least Privilege for Workers
Workers need to read inputs and write outputs, nothing more. Scope the role to the specific buckets and prefixes, grant s3:GetObject on inputs and s3:PutObject on the output prefix, and avoid wildcard actions. Separate roles for development and production clusters keep an experiment from overwriting a published product. For cross-account data, prefer a bucket policy granting the worker role access over copying keys between accounts. These controls are also what make it safe to run third-party or experimental code on the cluster.
Other Clouds and Signed URLs
The same principles apply on Google Cloud and Azure: workload identity or managed identities play the role of IAM roles, and GDAL reads GOOGLE_APPLICATION_CREDENTIALS or Azure environment variables when a role is not available. Signed URLs are the right tool when sharing a small number of objects with compute you do not control, such as a partner’s cluster or a tile server; GDAL reads them through /vsicurl/ like any HTTPS file. Never log them — a signed URL is a working credential for its lifetime.
Credentials in Container Images and Notebooks
Two leaks are common enough to name. The first is baking credentials into a container image through an ENV line or a copied ~/.aws directory: every layer of an image is readable by anyone who can pull it, and deleting the file in a later layer does not remove it from the earlier one. Images should contain configuration, never secrets, and get credentials at run time from the platform. The second is notebooks: a key pasted into a cell survives in the saved output and the version history long after the cell is edited. Load credentials from the environment or a profile instead, and add a pre-commit hook that scans for key patterns so a mistake is caught before it leaves the laptop. If a key does leak, rotate it at once — removing it from a repository does not remove it from clones and caches.
Verification
import boto3, rasterio
def probe(path="s3://private-bucket/scenes/T33UVP/B04.tif"):
ident = boto3.client("sts").get_caller_identity()["Arn"]
with rasterio.open(path) as src:
src.read(1, window=((0, 16), (0, 16)))
return ident
print(client.run(probe)) # every worker should report the expected role ARN
Common Errors
Works locally, 403 on the cluster
Workers lack the laptop’s credentials. Attach a role, or forward temporary credentials with a plugin.
Fails after about an hour
Forwarded session tokens expired. Use roles, or refresh the credentials with a new plugin registration.
Requester-pays reads fail with 403
GDAL was not told the requester pays. Set AWS_REQUEST_PAYER=requester.
Keys appear in the dashboard or tracebacks
They were passed as task arguments. Move them into a worker plugin or, better, a role.
Frequently Asked Questions
Q: How should Dask workers authenticate to S3? With an IAM role or instance profile attached to the workers. The SDK and GDAL obtain and refresh credentials automatically, and no secret is transmitted.
Q: Is it safe to pass AWS keys as task arguments? No. They are serialised into every task and can appear in the dashboard, logs and tracebacks. Use a worker plugin or a role.
Q: Why do my reads fail after an hour? Forwarded session credentials expire, typically after an hour. Roles refresh automatically; forwarded credentials must be renewed.
Q: How do I read public and private buckets in one job? Set AWS_NO_SIGN_REQUEST only for the public reads, using rasterio.Env around those reads, and let the role sign the private ones.
Related
- Distributed Processing on Coiled and AWS Batch — the parent topic.
- Launching a Coiled Cluster for STAC Processing — a cluster that forwards credentials.
- Sizing AWS Batch Jobs for Tile Workloads — the jobs that need this access.
- Caching PROJ Data and GDAL Config in Containers — where the non-secret settings belong.