0Pricing
AI Agents · Lesson

Security Policies for Code Execution

Capability restrictions, network isolation, file system limits, and timeouts.

Security Policies for Code Execution is a free AI Agents lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Agents learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Policies Aren't Enough by Themselves

Telling the LLM 'don't access the network' is not a security control — it is a hint. The LLM might ignore it, be jailbroken, or the generated code might indirectly violate the rule.

Real security policies must be enforced at the execution layer, not only in the prompt.

Capability Restrictions Overview

Capability restrictions limit what a sandboxed process can do at the OS level. Linux capabilities split root privilege into fine-grained abilities that can be dropped individually.

Dropping all capabilities except the minimum needed is called principle of least privilege.

import docker
client = docker.from_env()

# Drop all Linux capabilities
output = client.containers.run(
    'python:3.12-slim',
    'python -c "print(\"safe run\")"',
    cap_drop=['ALL'],
    network_disabled=True,
    mem_limit='256m',
    auto_remove=True
)
print(output.decode())

Blocking Disk Writes Outside /tmp

Use a read-only root filesystem and mount only /tmp as writable. This prevents the code from modifying system files or persisting data outside the designated scratch area.

import tempfile, docker
client = docker.from_env()

with tempfile.TemporaryDirectory() as tmpdir:
    output = client.containers.run(
        'python:3.12-slim',
        'python -c "open(\"/tmp/ok.txt\",\"w\").write(\"ok\"); print(open(\"/tmp/ok.txt\").read())"',
        read_only=True,
        tmpfs={'/tmp': 'size=64m,mode=1777'},
        network_disabled=True,
        mem_limit='128m',
        auto_remove=True
    )
    print(output.decode())  # 'ok' — /tmp write allowed
    # Writing anywhere else raises ReadOnlyFileSystem

seccomp Profiles: Filtering Syscalls

seccomp (secure computing mode) lets you specify an allowlist of permitted syscalls. Any syscall not on the list causes the process to be killed with SIGSYS.

Docker ships a default seccomp profile. You can provide a custom JSON profile to be more restrictive.

import json, docker
client = docker.from_env()

# Minimal seccomp profile: only allow read, write, open, close, exit
seccomp_profile = json.dumps({
    'defaultAction': 'SCMP_ACT_ERRNO',
    'architectures': ['SCMP_ARCH_X86_64'],
    'syscalls': [
        {'names': ['read', 'write', 'open', 'openat', 'close',
                   'fstat', 'mmap', 'mprotect', 'munmap', 'brk',
                   'exit', 'exit_group', 'rt_sigaction',
                   'rt_sigprocmask', 'futex'],
         'action': 'SCMP_ACT_ALLOW'}
    ]
})

output = client.containers.run(
    'python:3.12-slim',
    'python -c "print(1+1)"',
    security_opt=[f'seccomp={seccomp_profile}'],
    auto_remove=True
)
print(output.decode())

Resource Limits: CPU, Memory, Time

Three resource limits every sandbox should enforce:

  • Memory: mem_limit — prevent OOM attacks
  • CPU: nano_cpus or cpu_quota — prevent CPU exhaustion
  • Time: execution timeout — prevent infinite loops
output = client.containers.run(
    'python:3.12-slim',
    'python -c "import time; time.sleep(100)"',
    mem_limit='128m',
    nano_cpus=500_000_000,   # 0.5 CPU
    network_disabled=True,
    auto_remove=True,
    timeout=10               # kill after 10 seconds
)
# Raises ReadTimeout after 10 seconds

Allowlisting Python Imports

Before executing code, scan the AST for import statements. Reject any import not on the allowlist. This is a defense-in-depth layer — not a replacement for OS-level controls, but useful for catching obvious policy violations early.

import ast

ALLOWED_IMPORTS = {'math', 'statistics', 'json', 'csv', 'datetime', 'collections', 're'}

def check_imports(code: str) -> list[str]:
    tree = ast.parse(code)
    blocked = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                if alias.name.split('.')[0] not in ALLOWED_IMPORTS:
                    blocked.append(alias.name)
        elif isinstance(node, ast.ImportFrom):
            if node.module and node.module.split('.')[0] not in ALLOWED_IMPORTS:
                blocked.append(node.module)
    return blocked

print(check_imports('import socket; print(1)'))   # ['socket']
print(check_imports('import math; print(math.pi)'))  # []

Detecting Dangerous Code Patterns

Beyond imports, check for patterns that bypass import restrictions: __import__, exec(), eval(), compile(), open() with paths outside /tmp.

import ast

DANGEROUS_CALLS = {'exec', 'eval', 'compile', '__import__', 'breakpoint'}

def check_dangerous_calls(code: str) -> list[str]:
    tree = ast.parse(code)
    found = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name):
                if node.func.id in DANGEROUS_CALLS:
                    found.append(node.func.id)
            elif isinstance(node.func, ast.Attribute):
                if node.func.attr in DANGEROUS_CALLS:
                    found.append(node.func.attr)
    return found

print(check_dangerous_calls('exec("import os")'))  # ['exec']

Output Size Limits

A runaway print loop can generate gigabytes of output. Limit stdout/stderr to a reasonable size (e.g., 1MB) and truncate or kill the container if exceeded.

MAX_OUTPUT_BYTES = 1 * 1024 * 1024  # 1 MB

def run_with_output_limit(client, code: str) -> dict:
    container = client.containers.run(
        'python:3.12-slim',
        f'python -c "{code}"',
        mem_limit='256m',
        network_disabled=True,
        detach=True
    )
    try:
        container.wait(timeout=30)
        logs = container.logs(stdout=True, stderr=True)
        if len(logs) > MAX_OUTPUT_BYTES:
            return {'success': False, 'error': 'Output too large'}
        return {'success': True, 'output': logs.decode(errors='replace')}
    finally:
        container.remove(force=True)

if __name__ == '__main__':
    class FakeContainer:
        def wait(self, timeout=30): pass
        def logs(self, stdout=True, stderr=True): return b'Analysis complete: 42 rows processed.'
        def remove(self, force=True): pass
    class FakeContainers:
        def run(self, *args, **kwargs): return FakeContainer()
    class FakeClient:
        containers = FakeContainers()

    result = run_with_output_limit(FakeClient(), 'print(42)')
    print('Success:', result['success'])
    print('Output :', result['output'])

No-New-Privileges Flag

The no-new-privileges security option prevents the process from gaining additional privileges via setuid binaries or sudo. Even if the code drops into a shell, it cannot escalate privileges.

output = client.containers.run(
    'python:3.12-slim',
    'python -c "import os; print(os.getuid())"',
    user='nobody',                              # run as non-root
    security_opt=['no-new-privileges:true'],    # no privilege escalation
    cap_drop=['ALL'],                           # no capabilities
    network_disabled=True,
    mem_limit='128m',
    auto_remove=True
)
print(output.decode())  # numeric UID of 'nobody'

Combining All Policies

A production-grade sandbox combines every layer: OS capabilities dropped, seccomp profile, read-only filesystem, no-new-privileges, network disabled, resource limits, and timeout. Defense-in-depth ensures a bypass of one layer is stopped by another.

import tempfile, json, docker
client = docker.from_env()

def maximum_security_run(code: str) -> dict:
    with tempfile.TemporaryDirectory() as tmpdir:
        try:
            out = client.containers.run(
                'python:3.12-slim',
                f'python -c "{code}"',
                user='nobody',
                security_opt=['no-new-privileges:true'],
                cap_drop=['ALL'],
                network_disabled=True,
                read_only=True,
                tmpfs={'/tmp': 'size=32m,mode=1777'},
                mem_limit='128m',
                nano_cpus=500_000_000,
                auto_remove=True,
                timeout=15
            )
            return {'success': True, 'output': out.decode()}
        except Exception as e:
            return {'success': False, 'error': str(e)}

Policy Violation Logging

When a policy violation is detected (blocked import, dangerous call, timeout), log the event with the agent session ID, the code snippet hash, and the violation type. This feeds a security dashboard and helps tune policies over time.

import hashlib, logging, time

logging.basicConfig(level=logging.INFO)
security_logger = logging.getLogger('sandbox.security')

def policy_check_and_run(code: str, session_id: str) -> dict:
    violations = check_imports(code) + check_dangerous_calls(code)
    code_hash = hashlib.sha256(code.encode()).hexdigest()[:16]

    if violations:
        security_logger.warning(
            'POLICY_VIOLATION session=%s hash=%s violations=%s',
            session_id, code_hash, violations
        )
        return {'success': False, 'error': f'Blocked: {violations}'}

    result = maximum_security_run(code)
    security_logger.info(
        'EXEC session=%s hash=%s success=%s',
        session_id, code_hash, result['success']
    )
    return result

What does the seccomp profile's 'defaultAction: SCMP_ACT_ERRNO' setting do?

seccomp profiles control which system calls a process is allowed to make. The defaultAction field determines what happens for syscalls not explicitly listed in the profile.

Security Policies Recap

Effective code execution security requires multiple layers: Linux capabilities dropped, seccomp syscall filtering, read-only filesystem with tmpfs scratch space, no-new-privileges, network disabled, memory/CPU/time limits, and AST-level import checking.

No single layer is sufficient — defense-in-depth means each layer compensates for bypasses in the others.

Frequently asked questions

Is the “Security Policies for Code Execution” lesson free?

Yes — the full text of “Security Policies for Code Execution” is free to read here on the web, and the AI Agents course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Agents course, upgrade to CoddyKit PRO.

What will I learn in “Security Policies for Code Execution”?

Capability restrictions, network isolation, file system limits, and timeouts. You practise AI Agents with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start AI Agents?

No prior experience is required. AI Agents on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Security Policies for Code Execution” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this AI Agents lesson?

Yes. Every AI Agents lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Docker-Based Agent Sandboxes
  2. VM Isolation for High-Security Code Agents
  3. E2B and Cloud Sandbox Services
  4. Security Policies for Code Execution
← Back to AI Agents