Introduction to Step Functions Security
AWS Step Functions is a serverless orchestration service that lets you coordinate distributed applications and microservices through visual workflows. Because Step Functions often sits at the center of critical business processes—triggering Lambda functions, calling APIs, querying databases, and interacting with dozens of other AWS services—securing it is essential. A misconfigured state machine can become a wide-open gateway for attackers to escalate privileges, exfiltrate data, or disrupt operations.
This tutorial covers the two foundational pillars of Step Functions security: IAM policies (which control who can create, execute, and manage state machines) and network security (which controls how Step Functions communicates with resources inside private networks). By the end, you will understand how to lock down your workflows using least-privilege IAM, service integration policies, VPC endpoints, and private networking patterns.
Why Step Functions Security Matters
Step Functions is powerful precisely because it can call so many AWS services on your behalf. Every state in your state machine can invoke a different integration—Lambda, ECS, DynamoDB, SNS, SQS, Glue, SageMaker, and over 200 others. Each of those calls uses an IAM role assumed by Step Functions. If that role is overly permissive, a single compromised state machine definition can lead to catastrophic consequences.
Common Security Risks
- Overly broad execution roles: Using
*permissions instead of scoping to specific resources. - Unrestricted state machine management: Allowing anyone in the account to modify or delete production workflows.
- Plaintext sensitive data in state machine input/output: Passing secrets through execution input without encryption or parameter store references.
- Uncontrolled network access: Step Functions calling private resources over the public internet instead of through VPC endpoints.
- Cross-account execution without conditions: Allowing external accounts to start executions without proper trust policies.
IAM Policies for Step Functions
IAM policies for Step Functions fall into two categories: management policies (who can create, update, delete, and describe state machines) and execution policies (what a running state machine is allowed to do). Understanding the difference is critical because they serve entirely different purposes and should be assigned to different principals.
1. Management Policies: Controlling Access to State Machines
Management policies are attached to IAM users, roles, or groups who administer Step Functions. They control operations like CreateStateMachine, UpdateStateMachine, DeleteStateMachine, StartExecution, and DescribeExecution. The goal here is least privilege: developers should only manage the state machines they own, and operators should only be able to start executions in their environments.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListStateMachines",
"Effect": "Allow",
"Action": [
"states:ListStateMachines",
"states:ListExecutions",
"states:ListActivities"
],
"Resource": "*"
},
{
"Sid": "ManageOwnStateMachines",
"Effect": "Allow",
"Action": [
"states:CreateStateMachine",
"states:UpdateStateMachine",
"states:DeleteStateMachine",
"states:DescribeStateMachine",
"states:StartExecution",
"states:StopExecution",
"states:DescribeExecution",
"states:GetExecutionHistory"
],
"Resource": "arn:aws:states:us-east-1:123456789012:stateMachine:OrderProcessing-*"
}
]
}
Notice how the resource ARN uses a prefix pattern (OrderProcessing-*) to scope permissions to a specific team's state machines. This is a simple but effective way to enforce team-level isolation within a shared AWS account.
2. Execution Roles: What a Running State Machine Can Do
Every state machine has an associated IAM role that Step Functions assumes when it executes the workflow. This role's permissions determine what the state machine can actually do—invoke a Lambda function, write to DynamoDB, publish to SNS, and so on. This is where most security mistakes happen, because developers often copy a broad role like lambda:InvokeFunction on * resources.
Here is a properly scoped execution role trust policy and permissions policy for a state machine that processes orders:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TrustStepFunctions",
"Effect": "Allow",
"Principal": {
"Service": "states.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "123456789012"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:states:us-east-1:123456789012:stateMachine:OrderProcessing-*"
}
}
}
]
}
The Condition block is essential. Without it, the confused deputy problem could allow a different state machine (or even one in another account, if your role is misconfigured) to assume this role. By restricting aws:SourceArn and aws:SourceAccount, you ensure only your specific state machine can assume the role.
Now the permissions policy attached to that role:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeOrderProcessingLambda",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessOrder"
},
{
"Sid": "WriteToOrderTable",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/Orders"
},
{
"Sid": "PublishToNotificationTopic",
"Effect": "Allow",
"Action": "sns:Publish",
"Resource": "arn:aws:sns:us-east-1:123456789012:OrderNotifications"
},
{
"Sid": "WriteLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogDelivery",
"logs:GetLogDelivery",
"logs:UpdateLogDelivery",
"logs:DeleteLogDelivery",
"logs:ListLogDeliveries",
"logs:PutResourcePolicy",
"logs:DescribeResourcePolicies",
"logs:DescribeLogGroups"
],
"Resource": "*"
}
]
}
Each statement is scoped to a specific resource ARN. There are no wildcards on the resource side for service integrations. The only * appears in the logging statement, which AWS requires because CloudWatch Logs log delivery uses service-level permissions.
3. Service Integration Patterns and IAM
Step Functions supports two integration patterns: Request Response (the default, which just calls the API) and Run a Job (.sync) (which waits for the task to complete). The .sync pattern requires additional IAM permissions because Step Functions polls for completion. For example, an ECS task with .sync requires ecs:RunTask, ecs:DescribeTasks, and events:PutTargets plus events:PutRule so Step Functions can register a callback.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RunEcsTaskSync",
"Effect": "Allow",
"Action": [
"ecs:RunTask",
"ecs:StopTask",
"ecs:DescribeTasks"
],
"Resource": "arn:aws:ecs:us-east-1:123456789012:task/OrderProcessingCluster/*"
},
{
"Sid": "RegisterCallback",
"Effect": "Allow",
"Action": [
"events:PutTargets",
"events:PutRule",
"events:DescribeRule"
],
"Resource": "arn:aws:events:us-east-1:123456789012:rule/StepFunctionsGetEventsForECSTaskRule"
}
]
}
Failing to include the EventBridge permissions for .sync integrations is one of the most common errors developers encounter. The state machine will fail at runtime with a permissions error, even though the primary service call (like ecs:RunTask) succeeds.
4. Cross-Account Execution
Sometimes a state machine in Account A needs to invoke a Lambda function in Account B. This requires a carefully coordinated trust relationship. The execution role in Account B must trust the state machine ARN in Account A, and the state machine's role in Account A must have lambda:InvokeFunction on the cross-account function ARN.
// Account B (Lambda owner) - Lambda execution role trust policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "states.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:states:us-east-1:111111111111:stateMachine:CrossAccountWorkflow"
}
}
}
]
}
Always use aws:SourceArn and aws:SourceAccount conditions in cross-account scenarios to prevent the confused deputy attack.
Network Security for Step Functions
While IAM controls what Step Functions can do, network security controls how it communicates with your resources. By default, Step Functions communicates with AWS services over the public AWS network. For most workloads this is fine because traffic stays within the AWS backbone. However, if you have resources in a private VPC—such as a private API Gateway VPC endpoint, an RDS database, or an ECS service behind a private load balancer—you need to think carefully about how Step Functions reaches them.
1. Understanding Step Functions Networking Model
Unlike Lambda, Step Functions does not run inside your VPC. It is a fully managed regional service that orchestrates calls to other services. This means you cannot attach a VPC configuration directly to a state machine. Instead, you control network access through two mechanisms:
- VPC Endpoints (PrivateLink): Keep traffic between Step Functions and AWS services within your VPC.
- Lambda functions as network proxies: Use a Lambda function configured with VPC access to make calls to private resources on behalf of Step Functions.
2. VPC Endpoints for Step Functions
If you want to prevent Step Functions API calls (like StartExecution or DescribeExecution) from traversing the public internet, you can create an interface VPC endpoint for Step Functions. This is especially important in regulated environments where all AWS API traffic must remain within a private network.
AWSTemplateFormatVersion: '2010-09-09'
Description: VPC Endpoint for Step Functions
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Resources:
StepFunctionsSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTPS to Step Functions VPC endpoint
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 10.0.0.0/16
StepFunctionsVPCEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VpcId
ServiceName: com.amazonaws.us-east-1.states
VpcEndpointType: Interface
SubnetIds: !Ref SubnetIds
SecurityGroupIds:
- !Ref StepFunctionsSecurityGroup
PrivateDnsEnabled: true
With PrivateDnsEnabled: true, the public DNS name for Step Functions (states.us-east-1.amazonaws.com) resolves to the private IP addresses of your VPC endpoint. This means any client inside the VPC calling Step Functions will use the private endpoint automatically, without code changes.
3. VPC Endpoints for Downstream Services
When your state machine calls services like DynamoDB, S3, or Secrets Manager, you may also want those calls to stay within your VPC. While Step Functions itself runs outside your VPC, the services it calls often support VPC endpoints. The key insight is that Step Functions calls AWS service APIs directly, not through your VPC, so VPC endpoints on those services primarily benefit resources inside your VPC (like Lambda functions or ECS tasks) that also call those services.
For DynamoDB, a gateway endpoint is the simplest option:
Resources:
DynamoDBEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VpcId
ServiceName: com.amazonaws.us-east-1.dynamodb
VpcEndpointType: Gateway
RouteTableIds:
- !Ref PrivateRouteTableA
- !Ref PrivateRouteTableB
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal: '*'
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:Query
- dynamodb:Scan
Resource:
- arn:aws:dynamodb:us-east-1:123456789012:table/Orders
- arn:aws:dynamodb:us-east-1:123456789012:table/Orders/index/*
The endpoint policy above restricts which DynamoDB actions and tables are accessible through the endpoint. Even if an IAM role had broader DynamoDB permissions, the endpoint policy would block access to other tables when traffic flows through this VPC endpoint.
4. Accessing Private Resources via Lambda Proxy
The most common pattern for accessing private resources from Step Functions is to use a Lambda function configured with VPC access. The Lambda function runs inside your private subnets and can reach RDS databases, private API Gateway endpoints, internal load balancers, and ECS services. Step Functions invokes the Lambda function, which then makes the private network call.
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda function in VPC for Step Functions
Resources:
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: SG for Lambda VPC access
VpcId: !Ref VpcId
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 10.0.2.0/24
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
Policies:
- PolicyName: DatabaseAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: arn:aws:secretsmanager:us-east-1:123456789012:secret:OrderDbCredentials-*
PrivateLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Runtime: python3.12
Role: !GetAtt LambdaExecutionRole.Arn
VpcConfig:
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroupIds:
- !Ref LambdaSecurityGroup
Code:
ZipFile: |
import json
import boto3
import psycopg2
def handler(event, context):
# Retrieve DB credentials from Secrets Manager
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='OrderDbCredentials')
creds = json.loads(response['SecretString'])
# Connect to private RDS instance
conn = psycopg2.connect(
host='10.0.2.10',
dbname='orders',
user=creds['username'],
password=creds['password']
)
cursor = conn.cursor()
cursor.execute("SELECT status FROM orders WHERE id = %s", (event['orderId'],))
result = cursor.fetchone()
conn.close()
return {'orderId': event['orderId'], 'status': result[0]}
The security group egress rule only allows outbound traffic on port 5432 (PostgreSQL) to the database subnet CIDR. This is a defense-in-depth measure: even if the Lambda function were compromised, it could not reach the internet or other internal services.
5. VPC Endpoint Policies for Step Functions
When you create a VPC endpoint for Step Functions, you can attach an endpoint policy that controls which state machines can be managed or executed through that endpoint. This is a powerful network-level control that complements your IAM policies.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowExecutionOnly",
"Effect": "Allow",
"Principal": "*",
"Action": [
"states:StartExecution",
"states:DescribeExecution",
"states:GetExecutionHistory",
"states:StopExecution"
],
"Resource": "arn:aws:states:us-east-1:123456789012:stateMachine:OrderProcessing-*"
},
{
"Sid": "DenyStateMachineModification",
"Effect": "Deny",
"Principal": "*",
"Action": [
"states:CreateStateMachine",
"states:UpdateStateMachine",
"states:DeleteStateMachine"
],
"Resource": "*"
}
]
}
This policy allows execution operations through the VPC endpoint but blocks any state machine modifications. Combined with IAM policies, this creates a layered defense: even if an IAM principal has states:DeleteStateMachine permissions, the VPC endpoint policy will block the call if it originates from within the VPC.
Encryption and Data Protection
Step Functions supports customer-managed KMS keys for encrypting execution data, including input, output, and state. By default, Step Functions uses an AWS-owned key, but for compliance requirements you should use a customer-managed key (CMK).
Configuring a Customer-Managed KMS Key
AWSTemplateFormatVersion: '2010-09-09'
Resources:
StepFunctionsKMSKey:
Type: AWS::KMS::Key
Properties:
Description: KMS key for Step Functions execution data
EnableKeyRotation: true
KeyPolicy:
Version: '2012-10-17'
Statement:
- Sid: Enable IAM permissions
Effect: Allow
Principal:
AWS: arn:aws:iam::123456789012:root
Action: kms:*
Resource: '*'
- Sid: AllowStepFunctionsService
Effect: Allow
Principal:
Service: states.amazonaws.com
Action:
- kms:GenerateDataKey
- kms:Decrypt
Resource: '*'
Condition:
StringEquals:
aws:SourceAccount: '123456789012'
StepFunctionsKeyAlias:
Type: AWS::KMS::Alias
Properties:
AliasName: alias/stepfunctions-order-processing
TargetKeyId: !Ref StepFunctionsKMSKey
Once the key is created, you reference it when creating or updating the state machine:
aws stepfunctions create-state-machine \
--name OrderProcessing \
--definition file://definition.json \
--role-arn arn:aws:iam::123456789012:role/StepFunctionsExecutionRole \
--encryption-configuration EncryptionType=CUSTOMER_MANAGED_KMS_KEY,KmsKeyArn=arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv
Best Practices
IAM Best Practices
- Always use
aws:SourceArnandaws:SourceAccountconditions in execution role trust policies to prevent confused deputy attacks. - Scope resource ARNs precisely. Never use
"Resource": "*"for service integration permissions likelambda:InvokeFunctionordynamodb:PutItem. - Separate management and execution roles. Developers who manage state machines should not have the execution role, and vice versa.
- Use permission boundaries for developer teams. This prevents teams from creating overly permissive execution roles.
- Audit execution roles regularly. Use IAM Access Analyzer to identify unused permissions and over-privileged roles.
- Tag state machines and use tag-based conditions. For example, require
aws:ResourceTag/Environmentto match the principal's environment tag.
Network Security Best Practices
- Use VPC endpoints for all AWS service calls in regulated environments to keep traffic private.
- Apply endpoint policies to restrict which API calls are allowed through each VPC endpoint.
- Use Lambda VPC proxy functions for accessing private resources like RDS, private APIs, and internal load balancers.
- Restrict security group egress rules on Lambda proxy functions to only the ports and CIDRs they need.
- Enable VPC Flow Logs on subnets where Step Functions-related resources operate for audit and troubleshooting.
- Use private hosted zones for DNS resolution of internal services to avoid relying on public DNS.
Data Protection Best Practices
- Use customer-managed KMS keys for state machines that process sensitive data.
- Enable key rotation on all customer-managed KMS keys.
- Never pass secrets in state machine input. Instead, reference Secrets Manager ARNs and have downstream tasks retrieve secrets directly.
- Use CloudTrail to monitor all Step Functions API calls and set up alerts for suspicious activity like
DeleteStateMachineorUpdateStateMachinefrom unexpected principals. - Enable execution history logging to CloudWatch Logs for audit purposes, but be careful not to log sensitive payload data.
Putting It All Together: A Secure State Machine
Here is a complete CloudFormation snippet that creates a secure state machine with a properly scoped execution role, customer-managed encryption, and CloudWatch logging:
AWSTemplateFormatVersion: '2010-09-09'
Description: Secure Order Processing State Machine
Parameters:
ProcessOrderLambdaArn:
Type: String
KmsKeyArn:
Type: String
Resources:
StateMachineExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: states.amazonaws.com
Action: sts:AssumeRole
Condition:
StringEquals:
aws:SourceAccount: !Ref 'AWS::AccountId'
ArnLike:
aws:SourceArn: !Sub 'arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:OrderProcessing'
Policies:
- PolicyName: OrderProcessingPermissions
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !Ref ProcessOrderLambdaArn
- Effect: Allow
Action:
- logs:CreateLogDelivery
- logs:GetLogDelivery
- logs:UpdateLogDelivery
- logs:DeleteLogDelivery
- logs:ListLogDeliveries
- logs:PutResourcePolicy
- logs:DescribeResourcePolicies
- logs:DescribeLogGroups
Resource: '*'
- Effect: Allow
Action:
- kms:GenerateDataKey
- kms:Decrypt
Resource: !Ref KmsKeyArn
Condition:
StringEquals:
kms:ViaService: !Sub 'states.${AWS::Region}.amazonaws.com'
OrderProcessingStateMachine:
Type: AWS::StepFunctions::StateMachine
Properties:
StateMachineName: OrderProcessing
RoleArn: !GetAtt StateMachineExecutionRole.Arn
EncryptionConfiguration:
EncryptionType: CUSTOMER_MANAGED_KMS_KEY
KmsKeyArn: !Ref KmsKeyArn
LoggingConfiguration:
Destinations:
- CloudWatchLogsLogGroup:
LogGroupArn: !GetAtt StateMachineLogGroup.Arn
IncludeExecutionData: false
Level: ERROR
Definition:
Comment: Order processing workflow
StartAt: ProcessOrder
States:
ProcessOrder:
Type: Task
Resource: !Ref ProcessOrderLambdaArn
End: true
StateMachineLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/states/OrderProcessing
RetentionInDays: 90
KmsKeyId: !Ref KmsKeyArn
Note several important security decisions in this template: IncludeExecutionData is set to false to prevent sensitive input/output from being written to CloudWatch Logs; the logging level is ERROR to minimize log volume; the KMS key policy condition restricts key usage to the Step Functions service; and the log group itself is encrypted with the same customer-managed key.
Conclusion
Securing AWS Step Functions requires a defense-in-depth approach that spans IAM policies, network configuration, and data encryption. By scoping execution roles to specific resources with aws:SourceArn conditions, using VPC endpoints and endpoint policies to control network traffic, leveraging Lambda proxy functions for private resource access, and encrypting execution data with customer-managed KMS keys, you can build workflows that are both powerful and secure. The key principle throughout is least privilege: every role, every policy, every security group rule, and every endpoint policy should grant only the minimum access needed for the state machine to function. Start with these patterns, audit regularly with IAM Access Analyzer and CloudTrail, and iterate as your workflows evolve. Security is not a one-time configuration but an ongoing practice that grows alongside your Step Functions usage.