
Illustrated by Jeff Prymowicz
A working agent is not a secure agent
Shipping answers one question: does the workflow run? It does not answer the harder one. What is the workflow allowed to do?
LangGraph exposes control flow and can persist execution. Add a checkpointer and it can also pause for human review. CrewAI assembles roles, tools, memory, hooks, and multi-agent coordination. Both orchestrate work. Neither makes a model trustworthy. A Python process does not become a security boundary because an agent runs inside it.
An attacker may not need to escape a sandbox. Agents already have private repositories, production databases, email, or arbitrary URLs within reach. Those are useful privileges. Prompt injection only has to redirect a normal tool call.
Our framework security comparison examines LangGraph and CrewAI side by side. This article takes the next step. It puts controls around either framework so the model never holds direct authority.
One rule shapes the design: the model proposes; software decides. Deterministic controls choose whether the action runs. They also choose its identity and execution boundary.
Move authority out of the agent process
Local development usually puts everything together: model loop, tool code, credentials, memory, and network. Fewer moving parts make debugging easier. They also give one compromised process the whole stack.
LangGraph's visible execution graph helps engineers review and debug a workflow. It offers no isolation on its own. The LangChain security policy calls for limited permissions, misuse planning, and defense in depth.
CrewAI takes a direct route: attach tools to an agent. Good for a demo, but in production that connection is application wiring—not an authorization system.
A production design separates four jobs:
1. Reasoning. The model proposes a tool and arguments.
2. Authorization. A policy layer evaluates the principal, tool, resource, destination, and risk.
3. Execution. A constrained worker performs the approved operation with narrow credentials.
4. Evidence. An audit pipeline records the decision and outcome without copying sensitive data into logs.
The change is visible in the diagram. The default process owns every capability. The hardened process sends each proposed action through policy, then into a constrained executor.

Control what the agent can do
Start with the tool inventory
Build the inventory before rewriting prompts. For every role or graph path, record its tools, downstream operations, reachable data, and credentials. Cut the rest. OWASP names the combined risk of excessive functionality, permissions, and autonomy Excessive Agency.
Free-form strings hide power. run_shell(command) and fetch_url(url) let one argument set the blast radius. Purpose-built functions reveal it. Compare those calls with get_invoice(invoice_id), draft_refund(order_id, amount), and publish_comment(repository_id, pull_request_id, body).
Typed schemas are the first gate. Validate every argument. Cap sizes, normalize paths and URLs, and reject fields the schema does not know.
Split reading from mutation. Research may justify list_issues and read_issue. It does not justify close_issue. Publishing gets a separate credential and a separate approval rule.
Put policy in the execution path
Prompts can ask the model to behave. They cannot enforce a decision. Policy belongs between the proposed call and the tool.
For LangGraph, place a fail-closed policy node in front of ToolNode. Inspect calls one by one; ToolNode may run several in parallel. Forward only calls that policy approved without modification. LangGraph does not supply this authorization layer. The application must.
When policy asks for review, LangGraph can invoke interrupt(). The graph needs a database-backed production checkpointer and a stable thread_id. Resume that same thread with Command(resume=...). One detail is easy to miss: the interrupted node starts over. Work done before interrupt() must be idempotent. The interrupt documentation shows the complete resume path.
CrewAI exposes InterceptionPoint.PRE_TOOL_CALL for the same boundary. The current API registers the hook with @on(...). The older global @before_tool_call decorator still works. A denied call must raise HookAborted. Ordinary hook exceptions are logged and may fail open.
The example below treats policy and approval_service as application-owned services:
from crewai.hooks import (
HookAborted,
InterceptionPoint,
ToolCallHookContext,
on,
)
@on(InterceptionPoint.PRE_TOOL_CALL)
def authorize(context: ToolCallHookContext) -> None:
principal = request_identity() # Authenticated runtime context
try:
decision = policy.evaluate(
principal=principal,
tool=context.tool_name,
arguments=context.tool_input,
)
except Exception as exc:
raise HookAborted(
reason="authorization policy unavailable",
source="authorization-policy",
) from exc
if decision.effect == "deny":
raise HookAborted(
reason="denied by authorization policy",
source="authorization-policy",
)
if decision.effect == "review" and not approval_service.verify(
principal=principal,
action_digest=decision.action_digest,
token=current_approval_token(),
):
raise HookAborted(
reason="valid external approval required",
source="approval-gate",
)
Identity comes from authenticated runtime context. That context owns the principal and tenant. Prompts, memory, and model-generated arguments never set them. A broken identity lookup, failed policy check, or invalid argument ends in denial.
Use approval for consequences, not uncertainty
Reserve human review for consequences: irreversible, external, high-value, or cross-boundary actions. A reviewer should not have to interpret a vague policy.
Reviewers need the target and destination. Include the data class and irreversible effects. The resulting approval covers a hash of the tool name and normalized arguments, with a short expiry. Any argument change voids the decision.
CrewAI's request_human_input() blocks while it reads from a terminal. That fits local development. It does not provide a durable production approval workflow. Keep production approval outside the agent process. Bind its token to the principal and normalized-action digest.
Contain what gets through
Policy decides what may run. Containment decides how bad a wrong decision can become. Both controls matter because approved tools still have bugs.
Send risky work to disposable workers
Keep model-generated code out of the orchestrator. CrewAI has deprecated its built-in code-execution controls and removed its own CodeInterpreterTool from crewai-tools. Hosted-provider features with similar names are separate. The safer design sends code to a dedicated external sandbox.
Published CrewAI advisories have documented RCE or sandbox-fallback flaws, arbitrary local file reads, and SSRF in code-execution tooling. These are useful examples of why an agent framework cannot double as a security boundary. Pin and test the framework and tool packages, keep unsafe path overrides disabled, and isolate execution outside the orchestrator.
Push risky work into disposable workers: code, browsers, document converters, and untrusted parsers. Each job starts with a clean filesystem. No host mounts. No container socket. Block cloud metadata and leave orchestrator credentials behind. Once the result passes validation, destroy the worker.
Apply a hard runtime baseline
The following Kubernetes pod is a starting point. Its image must support a known non-zero user. If the application writes to /tmp, mount an explicit writable volume rather than opening the root filesystem.
apiVersion: v1
kind: Pod
metadata:
name: agent-worker
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: agent-worker
image: registry.example.com/agent-worker:VERSION
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: "250m"
memory: "256Mi"
ephemeral-storage: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
ephemeral-storage: "1Gi"
Kubernetes documents these fields under security contexts and resource controls. Treat the pod spec as a floor, not a finished sandbox.
Do not inherit CrewAI's loop budget by accident. Set max_iter, max_execution_time, and max_retry_limit explicitly. Then cap tool calls, PIDs, graph steps, tokens, output size, and spend. A sandbox contains code; it does not stop a loop from filling the cluster or draining the budget.
Close the network by default
Executor Pods should start without egress. A NetworkPolicy-enforcing CNI can create that boundary, then open only named paths. Audit every policy that selects the Pods because policies combine additively. DNS is not exempt from default-deny egress. Permit UDP and TCP port 53 only to the cluster's resolver.
Standard NetworkPolicy filters addresses and ports, not DNS names. Domain allow-lists need controlled DNS, an egress proxy, a service mesh, or CNI-specific FQDN policy. Exclude private, link-local, and metadata destinations. Then test the actual CNI. Node traffic, hostNetwork, service DNAT, and additive policies can punch unexpected holes.
Treat memory as untrusted input
Memory changes the time horizon of an attack. A prompt injection may die with one request. Save it, and the instruction can steer future runs.
Persist less
Every stored field needs a reason to exist. Recovery state or an approved preference may qualify. Exclude secrets, credential-bearing headers, system prompts, and full tool responses. Prefer a reference or a derived fact. Set retention for each field.
A LangGraph checkpointer writes state at super-step boundaries. The persistence documentation recommends database-backed production checkpointers and supports EncryptedSerializer. That encryption covers bytes at rest. The runtime still sees plaintext after decryption. Use database least privilege, and enforce tenant authorization outside the model.
Patch the serializer path
Serializer advisories offer another useful example. If an attacker can modify persisted checkpoint bytes, unsafe object reconstruction may turn a compromised store into code execution. Treat checkpoint data as untrusted input and keep the serializer path tightly constrained.
Use a current supported release. Set LANGGRAPH_STRICT_MSGPACK=true where compatible. A compiled graph can derive an allow-list only when its checkpointer supports with_allowlist(); manual serializers may need explicit configuration. Avoid permissive deserializers and pickle fallbacks whenever an attacker could influence state.
CrewAI stores memory locally unless configured otherwise. Unified Memory defaults to LanceDB at ./.crewai/memory, relative to the process working directory. Set CREWAI_STORAGE_DIR=/base and the location becomes /base/memory. Memory(storage="/path") chooses another path. A StorageBackend instance supplies a custom backend.
Unified Memory may ask an LLM to infer scope, categories, and importance during save analysis. That happens when the caller omits those values. Treat promotion into memory as a security decision. Encrypt the store. Separate it by tenant and project.
Memory never grants authority
Attach provenance, trust level, and source run to every durable memory. Retrieved memory is data—not policy. It cannot grant tools, widen identity, rewrite system instructions, or suppress approval.
Check high-impact facts against a system of record before acting on them. Expire low-confidence entries. OWASP's research on memory and context poisoning explains the failure mode: one injection becomes persistent influence.
Keep credentials behind the tool boundary
The agent asks for a capability. The tool service holds the credential.
Put secrets in a secret manager. Give every tool service its own workload identity and short-lived, audience-bound credentials. AWS also recommends temporary credentials through IAM roles. A search tool has no reason to inherit payment or production-database access.
Resolve a credential only inside the tool service, after authorization passes. Never send it back. LangGraph state, CrewAI memory, model context, error messages, and tool results must remain credential-free.
Passing secrets through environment variables feels convenient, but code in that process can usually read them. Keep high-value credentials out of the orchestrator.
Scan prompts, checkpoints, traces, crash dumps, MCP configuration, and container layers. Rotate any secret after suspected exposure. Tool code should accept that rotation without prompt edits or a full agent redeploy.
Make every privileged action explainable
Application traces answer, “Why did this run fail?” Security telemetry answers a different question: “Who exercised authority?” It needs the requester, evaluated identity, policy reason, and resulting change.
Wire security events into observability before the first production run. Otherwise the evidence disappears when the process exits.
Record the decision before execution
Emit the first event before a tool executes. Include the argument hash and policy result. Add approval, credential scope, executor, destination, and data class when they exist. Record the outcome and state mutation afterward.
Correlate the record with its run, principal, tenant, framework version, and policy version. Keep denials. They are security data.
Alerts should cover repeated policy failures and newly observed destinations. Watch for tool enumeration, odd memory writes, replayed approvals, and retry spikes. Attempts to reach metadata or private network ranges also need an alert.
Redact before telemetry leaves the workload
Telemetry creates its own exfiltration path. Raw prompts and tool output stay inside by default. Redact sensitive fields at the workload boundary, before exporters see them.
LangSmith's sensitive-data tracing controls can mask inputs, outputs, and metadata. CrewAI hooks should produce the same sanitized event shape.
Well continue this series with with agent observability and detection. Teams building multi-agent security workflows can read Designing a Security Review Agent Team with Superpowers. For agent-driven CI/CD, GitHub Actions Security applies the same access and supply-chain rules.
The pre-production hardening checklist
Run this checklist against the deployed configuration—not the architecture diagram. Save the evidence with the release.
Dependencies and supply chain
- Pin and scan the framework, checkpoint, SDK, tool, and MCP packages.
- Patch them before release.
- Inventory MCP server definitions and tool metadata.
- Approve exact versions and watch for drift.
Tools and identity
- Give each role or graph path only the tools its task requires.
- Separate read, draft, write, delete, payment, and publish capabilities.
- Set the principal and tenant from runtime identity, never model output.
- Validate tool arguments and route every call through a fail-closed policy gate.
- Require expiring approval for irreversible, external, high-value, or cross-boundary actions.
Execution and network
- Keep model-generated code and untrusted parsers out of the orchestrator.
- Use ephemeral, non-root, read-only executors with no host access.
- Deny egress by default and allow explicit destinations and DNS paths.
- Cap CPU, memory, storage, PIDs, duration, iterations, retries, and spend.
State, memory, and secrets
- Give each persisted field a purpose, classification, retention period, and deletion path.
- Encrypt checkpoints and memory, isolate tenants, and block unauthorized writes.
- Enable strict serializer modes and allow-lists where supported.
- Record provenance for durable memory. Never let memory change policy, identity, or tool permissions.
- Keep secrets out of prompts, state, memory, logs, images, and tool results.
Verification and response
- Record policy decisions, approvals, tool outcomes, and state changes.
- Redact sensitive data before telemetry leaves the workload.
- Test indirect prompt injection, confused-deputy flows, SSRF, path traversal, memory poisoning, approval replay, policy-engine outage, sandbox failure, and runaway loops.
- Keep an emergency control that can disable a tool, credential, MCP server, or workflow without a full redeploy.
Run the checklist again whenever tools, models, prompts, policies, credentials, memory schemas, framework versions, or deployment boundaries change.
From working demo to defensible deployment
Hardening LangGraph or CrewAI is an exercise in removing authority. The model may propose an action. It may not authorize itself. It does not choose its identity, fetch credentials, or decide where code runs.
Cloud Security Partners' Agent Security Assessment reviews the real execution path, not the slideware. We trace framework and tool wiring. Then we test identity boundaries, policy controls, sandboxing, state, secrets, and observability. The result maps the current deployment to a hardened one.
Request an Agent Security Assessment here.
About the Author
Peter Karman is a Senior Principal AI Engineer at DryRun Security. He builds agentic, LLM-powered code-review systems that review PRs, ground findings in evidence, and alert of potential security issues. With 17+ years across infrastructure, networking, and software engineering, he designs dependable review pipelines, grounds findings in evidence, and instruments the process so performance meets real-world cost and latency constraints. He previously led engineering and AppSec initiatives as a Principal Engineer at companies like Leafly and AnyRoad.
Cloud Security Partners partnered with DryRun Security for this blog. DryRun Security is the industry’s first AI-native, agentic code security intelligence solution. Powered by their proprietary Contextual Security Analysis engine, they secure software built for the future by helping security and developer teams quiet noise, gain insights, and surface risks that pattern-based scanning tools inherently miss.
Stay in the loop.
Subscribe for the latest in AI, Security, Cloud, and more—straight to your inbox.
