Running code that comes from users, customers, plugins, or automated systems is one of the harder problems in cloud application security. The code may need CPU, memory, networking, and temporary storage, but giving it access to the same container environment as the main application can create a much larger security risk.
This is especially important for applications that execute user-submitted scripts, AI-generated code, build steps, data-processing jobs, or custom plugins.
A safer approach is to isolate the untrusted workload and give it only the resources and permissions it actually needs. AWS provides several building blocks for this, including containers, IAM, networking controls, and sandboxing technologies.
The goal is not simply to run the code in another container. The goal is to make sure that a compromised workload has as little opportunity as possible to affect the rest of the system.
Why Running Untrusted Code Is Different
Normal application code is controlled by the development team. Untrusted code is not.
For example, imagine an application that allows users to submit Python or JavaScript code for execution. A normal request might look like this:
User
|
v
API
|
v
Code Execution Service
|
v
SandboxThe execution service receives the code and starts an isolated environment.
The problem begins if the execution environment has unnecessary access to:
- Application secrets
- IAM credentials
- Internal services
- Production databases
- Private network resources
- Host-level resources
- Container metadata
- Shared storage
Even if the application itself is secure, an attacker may use the code execution feature as a path toward other resources.
For example, code such as this should never have access to your application's environment variables:
import os
for key, value in os.environ.items():
print(key, value)If the execution environment contains database credentials or API keys, those values can potentially become exposed.
Use a Separate Execution Boundary
The first design decision should be to separate the execution environment from the application that receives the request.
A typical architecture can look like this:
+----------------+
| Client |
+-------+--------+
|
v
+----------------+
| API |
+-------+--------+
|
Queue / Job
|
v
+-------------------+
| Execution Service |
+---------+---------+
|
v
+-------------------+
| Isolated Sandbox |
| |
| Untrusted Code |
+---------+---------+
|
Limited access
|
+-----------+-----------+
| Temporary Storage |
| Specific API Access |
+----------------------+The important part is the boundary between the application and the code being executed.
The sandbox should not be treated as an extension of the main application container.
Why a Normal Container Is Not Always Enough
Containers provide process and filesystem isolation, but they should not automatically be treated as a complete security boundary for hostile workloads.
A container running arbitrary code may still have access to resources that the application does not intend to expose.
For example, a poorly configured workload might receive:
Environment variables
IAM credentials
Network access
Shared volumes
Service endpoints
Container metadataThe security model should therefore assume that the code running inside the sandbox may attempt to escape its intended boundaries.
A better approach is defense in depth.
This means combining:
- Workload isolation
- Least-privilege IAM
- Network restrictions
- Short execution timeouts
- CPU and memory limits
- Ephemeral storage
- Minimal runtime images
- Logging and monitoring
- Input and output controls
No single control should be responsible for the entire security model.
Isolate the Workload With AWS Nitro-Based Sandboxing
For workloads that require a stronger isolation boundary, AWS Nitro-based sandboxing can be used to isolate untrusted workloads from the surrounding application environment.
The basic idea is that the untrusted workload runs inside a strongly isolated execution environment instead of sharing the same application runtime.
Conceptually:
Main Application
|
| Job
v
Execution Manager
|
v
+----------------------+
| Nitro-based Sandbox |
| |
| Untrusted Workload |
| Temporary Files |
| Limited Resources |
+----------------------+The exact implementation depends on the AWS service and sandboxing technology being used, but the architectural principle remains the same: reduce the amount of trust between the host application and the code being executed.
This is particularly useful for workloads where the code itself cannot be trusted.
Give the Sandbox a Separate IAM Role
IAM permissions should be designed around the workload rather than the application.
For example, suppose a code execution job only needs to read a specific object from S3.
Do not give the execution environment:
AmazonS3FullAccessInstead, create a dedicated IAM role with only the required permission.
A simplified policy might look like:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-execution-inputs/jobs/*"
}
]
}The principle is simple:
Sandbox permissions != Application permissionsThe application may need access to databases, queues, configuration services, and other AWS resources.
The sandbox usually does not.
Keeping those permissions separate limits the damage if the untrusted workload is compromised.
Avoid Passing Application Secrets Into the Sandbox
One common mistake is to pass secrets through environment variables because it is convenient.
For example:
DATABASE_URL=...
API_KEY=...
JWT_SECRET=...
AWS_ACCESS_KEY_ID=...
Join the conversation! Your thoughts help the community grow.