Designing Secure Autonomous Agents: Credential Isolation and Independent Approval Controls

    Autonomous agents can access sensitive systems and act without continuous supervision, creating risks such as credential exposure, unintended operations, and prompt injection. This article presents a control model based on risk classification, credential handles with egress injection, independent approval services, and fail-closed execution policies.

    1. Threat Modeling and Risk Classification

    Long-running agents may continuously access email, messaging, financial, and document systems, aggregate information proactively, and perform operations when needed. Their primary risks fall into three categories: credential exposure, unattended mistakes, and instruction injection through external content. Before designing controls, each risk should be mapped to a specific control.

    Threat Typical consequence Corresponding control
    Credential exposure Account takeover or impersonation Credential handles and egress injection; secrets never enter the model context
    Unattended mistakes An incorrect email is sent or a file is deleted Approval workflows, human confirmation, and rollback plans
    Prompt injection The agent is manipulated into performing unauthorized actions Independent checkers, execution isolation, and default denial

    Each operation should then be evaluated across four dimensions: reversibility, external impact, financial or legal effect, and sensitivity. The result can be mapped to one of three policy tiers.

    Evaluation criteria Example action Mapped tier Notes
    Read-only, reversible, and without external impact Reading email, organizing summaries, generating drafts Execute directly Audit logging is still required
    Has external impact or is difficult to reverse Sending email, deleting files, creating accounts Execute after approval Requires an independent checker and human confirmation
    High financial or legal effect Transferring funds, signing contracts Deny or require strict approval Reject directly unless additional authentication is completed

    The policy table should be implemented in code rather than documented only in prose.

    1. from enum import Enum
    2. class Tier(Enum):
    3. AUTO = "auto" # Execute directly
    4. APPROVE = "approve" # Execute after independent review and human confirmation
    5. DENY = "deny" # Never execute
    6. POLICY = {
    7. "read_mail": Tier.AUTO,
    8. "draft_reply": Tier.AUTO,
    9. "send_mail": Tier.APPROVE,
    10. "delete_file": Tier.APPROVE,
    11. "create_account": Tier.APPROVE,
    12. "transfer_funds_without_extra_auth": Tier.DENY,
    13. "transfer_funds_with_extra_auth": Tier.APPROVE,
    14. }
    15. def classify(action: str, extra_auth_verified: bool = False) -> Tier:
    16. if action == "transfer_funds":
    17. action += "_with_extra_auth" if extra_auth_verified else "_without_extra_auth"
    18. # Unregistered actions are denied by default.
    19. # New capabilities must explicitly declare a policy tier.
    20. return POLICY.get(action, Tier.DENY)

    extra_auth_verified must be written by the authentication service, not populated by the model. The result of additional authentication must therefore remain independent of the model’s own judgment.

    2. Keep Credentials Out of the Model Context

    The credentials used to access accounts are among the most important assets to protect. If a secret is placed in an environment variable or prompt that the model can read, the model may expose it through generated output, logs, tool arguments, or requests to third parties. Whether this happens through an accidental action or an injected instruction, the result is the same.

    A safer design gives the model only a handle. A credential handle is a reference identifier that contains no secret information. The actual credential is injected by an egress component at the network boundary:

    • The agent holds only a credential handle.
    • Requests first pass through the egress component. Based on the target service and operation type, the component retrieves the credential from the secret management service and adds it to the request headers.
    • The egress component can access only services on an allowlist.
    • Before returning the response to the agent, the component redacts sensitive content and filters strings that appear to be credentials.
    • Audit logs record the handle, target, action, and result, but never the secret itself.
    1. ALLOWED_TARGETS = {"mail-service", "payment-service"}
    2. def egress(request, audit):
    3. if request.target not in ALLOWED_TARGETS:
    4. audit.record(event="egress_denied", handle=request.credential_handle,
    5. target=request.target)
    6. raise PermissionError("Target is not on the allowlist")
    7. # The real secret exists only in the egress component's memory.
    8. # It is never included in the model context.
    9. secret = vault.fetch(request.credential_handle, scope=request.target)
    10. request.headers["Authorization"] = f"Bearer {secret}"
    11. try:
    12. response = send(request)
    13. except Exception:
    14. audit.record(event="egress_error", handle=request.credential_handle,
    15. target=request.target)
    16. # Do not propagate the original exception, which could expose headers.
    17. raise RuntimeError("Upstream request failed") from None
    18. audit.record(event="egress_ok", handle=request.credential_handle,
    19. target=request.target, status=response.status)
    20. return redact(response, secret)

    With this structure, exposing a secret is no longer an action the model can choose to perform. The system does not need to depend on the model exercising restraint.

    3. Independent Approval: The Checker Must Not Review Itself

    A common approval failure occurs when the same model that performs a task also decides whether the operation should proceed. The executor is responsible for completing the task, so its judgment is naturally biased toward execution. The more urgent the task appears, the easier it becomes to justify an unauthorized action.

    A more reliable design separates approval into two layers. The executor proposes a structured plan containing the action, parameters, rationale, and scope of impact. An independent checker, operating as a separate service with its own identity and policy inputs, determines whether the plan may proceed to human confirmation. Only structured fields should pass between the two layers; the executor’s conversational context should not be passed to the checker.

    Dimension Self-review by the same model Independent checker
    Identity Shared identity with overlapping permissions Separate service identity with least privilege
    Input Full conversation and task context, which may contain injected instructions Structured plan only
    Output Natural-language judgment that is difficult to validate mechanically Fixed status that can be processed directly
    Timeout and failure behavior Often implemented as “approve by default” Timeouts and errors always result in denial
    Auditability Reasoning is difficult to reconstruct Each decision records the rule ID and an input summary

    Approval should use five states: pending, approved, rejected, expired, and revoked. Execution may also produce executed and failed states. State transitions must be explicitly defined rather than inferred from informal logic.