← Back to DevBytes

Step Functions Security: IAM Policies and Network Security

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

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:

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

Network Security Best Practices

Data Protection Best Practices

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.

🛠 Tools from DevBytes

Inventory Tracker Pro — Excel inventory system, low-stock alerts · $19
AI Dev Kit for Mac — local AI dev environment templates · $9.99
KeyMapper for Mac — custom keyboard shortcut toolkit · $7.99

← Back to all articles