Security
What the provider does for you
IMDSv2 is required on every launch.
HttpTokens: "required"is set in the launch template and on every directRunInstancespath, so the metadata service rejects the unauthenticated IMDSv1GET— the request an SSRF-ed application can be tricked into making to read the instance’s role credentials.HttpEndpointstays enabled because the SSM agent needs it. The hop limit stays at EC2’s default of 2, since a container’s request costs a hop and a limit of 1 would make metadata unreachable from inside one.No long-lived credentials on instances. Workers get an IAM instance profile; nothing writes access keys into user data.
No network resources are created or deleted. Since v0.7.0 the VPC, subnet, and security group are yours. The provider cannot widen your network posture, and cannot delete a security group you supplied — a real hazard before v0.7.0, when
ServerlessMode.cleanup_infrastructure()calleddelete_security_groupon a user-supplied ID with no ownership check.Every resource is tagged
ParslResource=trueandParslWorkflowId=<provider_id>, so an orphan is findable and attributable.Instances terminate rather than stop.
InstanceInitiatedShutdownBehavior="terminate"means a finished worker leaves no billed EBS volume behind.
Least-privilege IAM policy
The authoritative action set is generated from the code, so it cannot drift from what the provider actually calls:
import json
from parsl_ephemeral_provider import EphemeralComputeProvider
print(json.dumps(EphemeralComputeProvider.minimum_iam_policy(), indent=2))
print(
json.dumps(EphemeralComputeProvider.minimum_iam_policy(include_ecr=True), indent=2)
)
This is a @staticmethod — you do not need to construct a provider to call it,
and it applies to EphemeralProvider just as much as to the Globus subclass.
It returns five statements: SessionValidation, EC2Management,
SSMCommandsAndParameters, SpotInterruptionWarning, and IAMInstanceProfile,
plus ECRContainerImages when include_ecr=True.
Note what it does not grant: no ec2:CreateVpc, CreateSubnet,
CreateSecurityGroup, CreateNatGateway, or RequestSpotFleet. Older versions
of this document asked for all of them. The provider creates no network resources
(#69) and uses CreateFleet rather than the legacy Spot Fleet API (#86). Nor any
Session Manager action — ssm:StartSession and its four companions were granted
for a tunnel this package does not have, and were removed in
#195.
The IAM statement grants both halves of the instance-profile lifecycle. The
teardown grants are not symmetry for its own sake: cleanup_infrastructure()
deletes the role and profile it created (#132), and cleanup logs rather than
raises, so a policy granting only the creates leaks a standing privileged
principal on every run without ever reporting an error. That is exactly how 94
orphaned roles accumulated in a real account, and the policy reproduced it until
#195.
Every statement uses Resource: "*". Most of these EC2 and IAM actions do not
support resource-level permissions, but you should scope what you can with
condition keys — ec2:ResourceTag/ParslResource on the terminate and tag actions,
and iam:PassedToService: ec2.amazonaws.com on iam:PassRole.
Additional permissions by mode
Detached mode — the bastion runs from a CloudFormation stack:
{
"Effect": "Allow",
"Action": [
"cloudformation:CreateStack",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackEvents",
"cloudformation:DeleteStack",
"iam:PassRole"
],
"Resource": "*"
}
Serverless mode (Lambda):
{
"Effect": "Allow",
"Action": [
"lambda:CreateFunction",
"lambda:InvokeFunction",
"lambda:DeleteFunction",
"lambda:GetFunction",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"iam:PassRole"
],
"Resource": "*"
}
Serverless mode (ECS/Fargate):
{
"Effect": "Allow",
"Action": [
"ecs:CreateCluster",
"ecs:DeleteCluster",
"ecs:RegisterTaskDefinition",
"ecs:DeregisterTaskDefinition",
"ecs:RunTask",
"ecs:StopTask",
"ecs:ListTasks",
"ecs:DescribeTasks",
"iam:PassRole"
],
"Resource": "*"
}
State backends — Parameter Store:
{
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:GetParameter",
"ssm:DeleteParameter"
],
"Resource": "arn:aws:ssm:*:*:parameter/parsl/*"
}
S3:
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-parsl-state-bucket",
"arn:aws:s3:::my-parsl-state-bucket/*"
]
}
No s3:CreateBucket — the bucket must already exist.
Instance profiles
Worker instance profile
With auto_create_instance_profile=True the provider creates a role carrying
AmazonSSMManagedInstanceCore and nothing else. That is the minimum for SSM
SendCommand, which the warm-pool and one-shot paths need.
If your workload needs more — reading S3, writing CloudWatch Logs — create the
role yourself and pass iam_instance_profile_arn:
provider = EphemeralProvider(
# ... network and compute options ...
iam_instance_profile_arn="arn:aws:iam::123456789012:instance-profile/parsl-worker",
)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::your-data-bucket",
"arn:aws:s3:::your-data-bucket/*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
Attach AmazonSSMManagedInstanceCore as well if you use the warm pool or one-shot
mode.
A role created with auto_create_instance_profile=True is deleted on shutdown
since v0.8.0 (#132),
along with its instance profile. Deletion is gated on ownership, so a profile you
supplied through iam_instance_profile_arn is never deleted. Your policy must
grant the teardown actions listed above — cleanup logs rather than raises, so
without them the roles accumulate silently
(#195).
Bastion instance profile (detached mode)
The CloudFormation template creates this role, carrying
AmazonSSMManagedInstanceCore plus EC2 and Parameter Store access — the bastion
launches and terminates workers itself, so it needs RunInstances,
TerminateInstances, DescribeInstances, CreateTags, and read/write on
/parsl/* parameters. The bastion is a compute host with real authority; treat
its role as the most sensitive part of the deployment.
Network security
The provider requires an existing VPC, subnet, and security group — there is no
create_vpc option, and passing one raises ProviderConfigurationError. See
network-prerequisites.md for the required rules.
Scope the security group tightly
Workers need outbound to reach the interchange, AWS APIs, and any package index
worker_init uses. Inbound is where the exposure is.
import boto3
ec2 = boto3.client("ec2", region_name="us-east-1")
sg = ec2.create_security_group(
GroupName="parsl-workers",
Description="Parsl ephemeral workers",
VpcId="vpc-0123456789abcdef0",
)["GroupId"]
# Workers dial the interchange outbound, so no inbound rule is needed for Parsl
# itself. Add one only for a client that must reach the workers directly.
ec2.authorize_security_group_ingress(
GroupId=sg,
IpPermissions=[
{
"IpProtocol": "tcp",
"FromPort": 54000,
"ToPort": 55000,
"IpRanges": [{"CidrIp": "203.0.113.10/32"}], # your client, /32
}
],
)
provider = EphemeralProvider(
region="us-east-1",
vpc_id="vpc-0123456789abcdef0",
subnet_id="subnet-0123456789abcdef0",
security_group_id=sg,
)
No SSH rule appears above deliberately. Reach instances with SSM Session Manager instead:
aws ssm start-session --target i-0123456789abcdef0
That needs no inbound rule, no key pair, and no public IP, and every session is
logged in CloudTrail. key_name remains available if you want SSH, but it is not
required for access.
Private subnets
provider = EphemeralProvider(
# ... other options ...
subnet_id="subnet-private0123456789",
use_public_ips=False,
)
Instances then need a NAT gateway, or VPC endpoints for ssm, ssmmessages,
ec2messages, and s3, to reach AWS APIs and register with SSM. The provider
does not create either.
Data security
Encryption in transit
Traffic to AWS APIs is HTTPS. Worker-to-interchange ZMQ traffic is a different
matter: set encrypted=False on HighThroughputExecutor for now. Parsl’s
CurveZMQ certificates are generated in the client’s run_dir, which workers
cannot read, so leaving encryption on makes workers fail to register. Same-VPC
deployments rely on VPC isolation; certificate distribution for cross-VPC and
internet paths is
#62.
Encryption at rest
S3 state store — configure default encryption on the bucket itself. There is no provider option for this; the provider does not create the bucket, so bucket policy is where it belongs:
aws s3api put-bucket-encryption --bucket my-parsl-state-bucket \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
Parameter Store — ParameterStoreState supports SecureString via
use_secure_string=True, but the provider always constructs it with the default
False and exposes no option. To use it, replace the store after construction and
before the first submit():
from parsl_ephemeral_provider.state.parameter_store import ParameterStoreState
provider.state_store = ParameterStoreState(
provider=provider,
prefix="/parsl/my-workflow",
use_secure_string=True,
)
State documents hold resource IDs and job commands, not credentials — assess
whether that warrants SecureString for your environment.
EBS volumes — there is no block_device_mappings option. Set
EBS encryption by default
for the account and region, which covers every volume the provider launches
without needing an option at all.
Credentials
Never put credentials in code. The provider takes no aws_access_key_id or
aws_secret_access_key — it resolves credentials through botocore, so
environment variables, ~/.aws/credentials, and instance profiles all work.
Select a named profile with profile_name:
provider = EphemeralProvider(
profile_name="parsl-profile",
# ... other options ...
)
Prefer an instance profile or IAM Identity Center over static keys. If you must use keys, rotate them on your usual schedule.
Auditing and monitoring
Tagging
provider = EphemeralProvider(
# ... other options ...
additional_tags={
"Project": "MyDataScience",
"Environment": "Development",
"Owner": "someone@example.com",
"CostCenter": "12345",
},
)
The option is additional_tags, not tags. These are applied alongside
ParslResource and ParslWorkflowId, so cost allocation and orphan sweeps both
work.
Audit logging
parsl_ephemeral_provider.security.audit provides AuditLogger and
SecurityMonitor. ParameterStoreState emits STATE_ACCESS events when the
provider has an audit_logger attribute — but the provider takes no such
constructor argument and sets no such attribute, so you must attach one yourself:
from parsl_ephemeral_provider.security.audit import AuditLogger
provider.audit_logger = AuditLogger(log_file="parsl-audit.jsonl")
Only the Parameter Store backend consults it today. Broader adoption is not yet implemented.
CloudTrail and CloudWatch
CloudTrail records every API call the provider makes, including each
RunInstances,SendCommand, and IAM role creation and deletion. This is your real audit trail.Budgets and CloudWatch alarms on EC2 spend are worth setting up: the provider has no cost controls of its own beyond
max_blocks,auto_shutdown, and the warm-pool cap. Reclaiming idle-but-running instances is Parsl’smax_idletime, not a provider setting.CloudWatch Logs hold cloud-init output only if you configure the agent in
worker_init; the provider does not install it.
Compliance
If your workflows touch regulated data:
VPC endpoints for
ssm,ssmmessages,ec2messages,s3, andlogskeep traffic off the public internet. Create them yourself — there is no provider option.AWS Config rules can assert that launched instances match your baseline.
Security Hub aggregates findings across accounts.
Note the default AMI is a public Amazon Linux 2023 image resolved from SSM. If you need a hardened or approved base, pass
image_idexplicitly, or usebake_amiwithworker_initdoing the hardening.
Emergency shutdown
To tear down everything a provider owns:
provider.cleanup_all() # terminate compute, keep the provider usable
provider.shutdown() # ...and delete the launch template, AMI, and state
Neither takes a force argument. To reach the resources of a workflow started by
another process, construct a provider against the same state location — the
persisted provider_id is adopted automatically:
provider = EphemeralProvider(
mode="detached",
region="us-east-1",
vpc_id="vpc-0123456789abcdef0",
subnet_id="subnet-0123456789abcdef0",
security_group_id="sg-0123456789abcdef0",
state_store_type="parameter_store",
parameter_store_path="/parsl/compromised-workflow",
)
provider.shutdown()
If the state is gone, sweep by tag instead:
parsl-ephemeral-cleanup --region us-east-1 # --dry-run first
Reporting a vulnerability
Open a security advisory rather than a public issue.
SPDX-License-Identifier: Apache-2.0 SPDX-FileCopyrightText: 2025-2026 Scott Friedman and Project Contributors