HTTP or SSH for AI agents: limit blast radius
Choose HTTP or SSH for AI agents by comparing authority, audit records, retry safety, and the failure modes that turn small tasks into broad access.

An AI agent should use HTTP when a service can express the intended action as a narrow, authenticated operation. It should use SSH only when the job needs a machine level capability that an API cannot provide, and then only through an account and command surface built for that one job.
The usual mistake is to compare the two transports as if one is modern and one is old. That misses the decision. HTTP and SSH are delivery mechanisms. The authority you attach to them, the inputs you accept, and the evidence you retain decide whether an agent can make a contained change or wander through a production host with an administrator's privileges.
I have seen teams issue a supposedly temporary SSH key because an agent needed one operational fact. A month later the key could read deployment secrets, reach internal services, and run an interactive shell. Nobody had made a dramatic security decision. They had just accepted a convenient default. That is precisely how a small task gets a large blast radius.
The interface determines the authority an agent receives
HTTP or SSH for AI agents is a question about capability shape, not protocol preference. An HTTP call can be broad and dangerous, while an SSH connection can be tightly limited. In practice, APIs more often give you a useful place to constrain authority because an endpoint, method, request schema, and token permission can all describe one operation.
Consider the instruction: restart a failed worker. An HTTP endpoint such as POST /workers/worker-17/restart states the target and the permitted verb. The service can reject an unknown worker, require a role that permits restarts, and write a record tied to the token. A shell instruction such as ssh host sudo systemctl restart worker has a wider implied authority. It relies on the account, sudo configuration, unit naming rules, shell parsing, and host state all being correct.
That does not make the API automatically safe. A token that can call every endpoint, create other tokens, or export every record has a wide blast radius behind a tidy URL. Likewise, a forced SSH command that accepts a fixed worker identifier from an allowlist can be smaller than a poorly designed administrative API.
Use this test before connecting either tool: write the smallest successful action in one sentence, then list what else the same credential can do if the agent produces unexpected input. If you cannot explain the second part, you have not measured authority yet.
A narrow interface has four properties:
- It names a small set of targets rather than an entire environment.
- It accepts structured input with a grammar you can validate.
- It rejects adjacent actions that the current task does not need.
- It creates a record that lets another person explain the result later.
The task should also decide how long the credential lives. A credential used for one run should not silently turn into standing access because nobody remembered to remove it. Process lifetime is a better boundary than a calendar reminder.
HTTP gives you useful boundaries only when the API enforces them
HTTP gives an agent a smaller blast radius when the service checks authorization at the resource and operation level. A bearer token is only a carrier. Its safety comes from what the server verifies after it receives the token.
RFC 9110 describes HTTP methods in terms of semantics, including the distinction between safe methods and idempotent methods. That language helps with retries and intent, but it does not grant permission. A GET can disclose sensitive material. A PUT may be idempotent yet still overwrite a production setting. Treat method names as clues for client behavior, not as a permission model.
Ask the service owner these questions before handing an agent a token:
- Which exact paths and methods can this token call?
- Does the service check access on each resource, or only on the broad collection?
- Can the token create credentials, alter permissions, or trigger exports?
- Can a request cross into another tenant, project, or environment through an identifier?
- Does the service record the credential identity and request outcome?
The awkward question is whether a read permission leaks more than the task requires. A repository API may let a read token fetch source, pull request comments, build logs, and configuration. One poorly stored configuration value can make read access equivalent to secret access. If the agent only needs the state of one deployment, give it an endpoint that returns that state. Do not hand it a general repository token and call the result least privilege.
Request schema matters as much as scope. Compare these two requests:
POST /v1/releases/release-42/promote HTTP/1.1
Authorization: Bearer token-value
Content-Type: application/json
{"environment":"staging"}
POST /v1/admin/execute HTTP/1.1
Authorization: Bearer token-value
Content-Type: application/json
{"operation":"promote","arguments":{"environment":"staging"}}
Both may promote a release. The first leaves the server little room to interpret the operation. The second creates an administrative dispatcher. Dispatchers attract exceptions, then arbitrary operation names, then a token whose true authority is hard to state. I avoid them for agent use unless the server applies a strict operation allowlist and validates each operation's argument schema independently.
Use distinct credentials for distinct verbs when the service permits it. Separate observation from mutation, and separate routine mutation from identity or billing changes. This creates more setup work, but it makes authorization failures meaningful. A denial tells you that the task definition and the credential do not match. A broad token turns every mistake into a successful request that you must investigate afterward.
Do not put a long lived API secret in an agent prompt, environment file, repository setting, or tool configuration. The problem is not only accidental disclosure in output. Agents inspect their environment, tools collect diagnostics, and a process with plaintext access can pass that secret to a different destination. Keep the secret outside the agent process and authorize the resulting action instead.
SSH exposes the host unless you deliberately remove the shell
SSH has a large default blast radius because an interactive account can inspect files, run programs, alter configuration, open tunnels, and use every network path available to that account. The fact that you intend to run one command does not restrict an account that receives a normal shell.
RFC 4251 describes SSH as a protocol for secure remote login and other secure network services. It deliberately supports sessions, channels, port forwarding, and more than one authentication method. Those capabilities are useful for administrators. They are a poor starting point for an autonomous actor that needs one constrained maintenance operation.
The command itself also matters. systemctl restart service-name looks bounded until you trace the surrounding authority: which units the account can restart, whether unit files can run privileged hooks, whether the account can edit those files, and whether service names come from validated input. A command that appears operational may reach deployment credentials, mounted volumes, or an internal control plane through the service it restarts.
If SSH is necessary, build a small remote program with a closed input grammar. The program should map known request fields to known operations. It should not concatenate a request into a shell command. Avoid accepting file paths, hostnames, regular expressions, shell snippets, or environment assignments unless the program validates each against a tight allowlist.
A restricted authorized_keys entry can make that boundary visible. The following pattern forces a single receiver program and removes several SSH features that agents rarely need:
command="/usr/local/libexec/agent-maintenance",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample agent-runner
This line does not solve authorization by itself. The agent-maintenance program must reject unknown subcommands and validate its arguments. The operating account must have only the file, service, and network permissions that program needs. If the program invokes sudo, its sudo rule must name a fixed executable and must not permit editor, interpreter, wildcard, or shell escape paths.
OpenSSH's authorized_keys manual documents command=, no-pty, and forwarding restrictions. Treat those options as a seatbelt, not the vehicle. They remove several easy escapes, but a forced command running as an overprivileged account still has overprivileged access.
A useful remote input protocol can be boring JSON on standard input:
{"action":"restart_worker","worker":"worker-17","request_id":"8b4f3c2a"}
The receiver should accept only restart_worker and worker names from its own inventory. It should write an event before it executes, invoke a fixed program without a shell, capture the exit status, and write a completion event. If the agent sends worker-17; cat /etc/shadow, validation must reject the complete value before any operating system command runs.
Do not give an agent SSH access merely because a human already uses SSH for the same work. Humans can recognize an unusual prompt, notice a hostname mismatch, and stop after a surprising result. Agents need the restriction embedded in the interface.
Logging must explain the attempted action and the resulting state
A log line that says "request failed" is not an audit record. It may be enough for debugging a client library, but it cannot settle whether an agent changed something, whether a person approved it, or what to inspect after an incident.
For HTTP, capture the agent run identity, credential identity or credential label, destination host, method, normalized path, a safe representation of the request body, response status, request correlation value, start time, and completion result. Redact secrets and sensitive fields before the event leaves the action boundary. Logging the Authorization header to preserve evidence is a self inflicted breach.
For SSH, record the host identity, remote account, forced command name, validated arguments, source process identity, exit status, standard error classification, and the remote operation identifier. A raw command string alone is weak evidence because it can hide quoting behavior and does not tell you which arguments the receiver accepted.
The event sequence should distinguish intent from effect. This shape works for either transport:
{"event":"authorization_granted","run":"r-204","action":"restart_worker","target":"worker-17"}
{"event":"action_started","run":"r-204","transport":"ssh","operation":"restart_worker","request_id":"8b4f3c2a"}
{"event":"action_finished","run":"r-204","outcome":"success","remote_status":0,"request_id":"8b4f3c2a"}
If a connection breaks after action_started, write outcome:"unknown" rather than inventing failure. That word forces the correct next action: query the remote state before retrying. It also makes a later investigation honest.
Normal logs also have a custody problem. A host administrator or a process that gains sufficient access can truncate, rewrite, or remove them. Central collection helps, but it still may leave gaps when the collector or network path fails. If the log must settle disputes about an agent's actions, retain append oriented records with integrity checks and verify them outside the action path.
A hash chain makes edits detectable when each event incorporates the prior event's digest. It does not prove that the recorder saw every event, and it does not make an untrusted clock trustworthy. Those limits matter. The chain answers a narrower and useful question: did someone alter this retained sequence after the fact?
Sallyport keeps a Sessions journal for agent runs and an Activity journal for individual calls, both projected from an encrypted hash chained audit log. Its sp audit verify command checks that chain offline over ciphertext, which is the right property when you need to inspect evidence without first exposing secrets.
Timeouts create unknown outcomes, not failed actions
Network failures are where otherwise careful teams cause duplicate changes. A client sends a request, the remote side performs the work, and the response disappears. The agent sees a timeout and runs the action again. With SSH, the same sequence can occur after the remote command starts but before the client receives its exit status.
Do not let an agent interpret a transport error as permission to retry a mutation. First classify the operation.
An operation is idempotent only when repeating the same request produces the same intended state without extra effect. Setting a named worker's desired state to running may fit that definition. Creating a payment, appending a record, rotating a secret, or restarting a process often does not. A restart can interrupt a recovery sequence that the first attempt already began.
Use an idempotency identifier when the API supports one. The service must retain the identifier with the completed effect and return the prior result for a duplicate. A client supplied request identifier that the server merely logs does not prevent duplication.
For remote commands, add a status operation that can answer a precise question. After a failed restart_worker response, query the worker's current generation, last restart request identifier, and health state. If the receiver stores the request identifier before execution and returns it with status, it can tell a retrying agent whether the request already ran.
This failure sequence exposes why it matters:
- The agent submits a restart for
worker-17with request identifier8b4f3c2a. - The receiver records the identifier and restarts the worker.
- The SSH connection drops while the worker stops.
- The agent asks for status instead of restarting again.
- The status response says the same identifier is in progress, so the agent waits and checks health.
A retry budget does not fix ambiguous outcomes. It limits damage after you make the wrong retry decision. State observation fixes the decision itself.
HTTP has another hazard: services sometimes return a success status before asynchronous work completes. A 202 Accepted means the server accepted work for later processing, not that the requested state exists. Require an operation resource or status endpoint, and make the agent wait for the terminal result that matters to the task.
SSH has an equivalent trap when a command backgrounds work and exits zero. Do not treat a zero exit code from a launcher as proof of a completed maintenance action. Make the receiver wait for completion, or return a durable operation identifier that the agent can query.
A shell command hides more authority than its text reveals
The shortest remote command often has the widest hidden authority. Shell expansion, environment inheritance, current directories, configuration files, and executable search paths all influence what runs. An agent can produce harmless looking text that triggers a surprising result because the host interprets it in context.
Avoid this pattern:
ssh ops@host "deploy $branch $environment"
Even if the caller quotes variables correctly today, the remote shell parses a command language. The deployment script may perform its own expansions. A branch name may choose a source location. An environment name may select credentials or a target cluster. You must inspect every layer before you can claim that the input is bounded.
Use a receiver that reads structured input and invokes a fixed executable directly. In most languages that means an argument array, not a string passed to sh -c. The receiver should own the mapping between a user facing target name and a host specific identifier. Do not make the agent discover filesystem paths or service unit names.
The same concern applies to HTTP parameters. A path such as /files?path=... may look structured while the server passes the value to a filesystem operation. An API only reduces risk when the server validates meaning, not when it relocates command parsing behind a URL.
Credential placement changes the consequence of a compromised agent process. If an agent stores an SSH private key or API token locally, any process that can read that material can act later without the agent. A separate action boundary can hold the secret and pass only a request plus an authorization decision to the remote service. That distinction is sharp: hiding a secret from model output is not the same as keeping the secret out of the agent process.
Sallyport takes this latter approach for HTTP credentials and SSH keys: the app holds them in an encrypted vault and executes the requested action rather than handing credential material to the agent. That arrangement does not make a dangerous request safe, so you still need to restrict targets and inspect approvals.
Choose the transport with a written capability comparison
You can make a defensible choice without a lengthy risk workshop. Write one row for the proposed HTTP call and one for the proposed SSH command, then fill each row with the same facts. Vague labels such as "read access" or "maintenance access" do not count.
Use this five part comparison:
- State the exact result: for example, "retrieve deployment state for service A" or "restart worker-17 after a failed health check."
- Name every reachable target: API collections, projects, hosts, services, files, and network destinations.
- List mutations that the same credential or account can still perform beyond the intended result.
- Describe the evidence available after a timeout, rejection, or apparent success.
- Define the approval boundary: one run, one call, or a preapproved routine with a fixed identity.
Choose HTTP if the API row names a smaller target set, exposes a narrower operation, and leaves a clearer record. Choose SSH if the remote receiver row can do those things better than the API, or if no API exists for the required host operation. If neither row is acceptably narrow, do not connect the agent yet. Build the missing endpoint or receiver first.
This comparison also catches a common bad recommendation: "use SSH for reads and APIs for writes." It sounds prudent because shell access feels operational and API calls feel transactional. It fails because reading a host can reveal credentials, source, customer data, and topology, while a carefully scoped API mutation can change exactly one desired state. Read and write are not adequate risk categories. Reachable data and reachable side effects are.
A team running an agent that diagnoses build failures may need an HTTP endpoint for job status, an API call to fetch a bounded log window, and a remote receiver for one host only when a specific repair requires it. Splitting the job does add interfaces. It also prevents a routine diagnostic task from carrying a standing shell credential merely because one rare repair needs it.
Approval should follow the cost of a wrong action
Approval is most useful when it appears at a boundary that a person can understand. Prompting for every harmless status read trains people to click without reading. Granting one broad approval that covers every future executable removes meaningful review entirely.
Use per run approval when a new agent process first asks to act and its identity can be shown clearly. The person can compare the requesting executable with the work they expected to start. Revoke the run when it behaves unexpectedly, then investigate its prior calls through the action record.
Use per call approval for actions with irreversible or expensive side effects: credential rotation, deletion, production promotion, account changes, and anything whose target an agent may select dynamically. The approval prompt should name the destination and operation in ordinary terms. "Execute tool request" tells the reviewer almost nothing.
Do not try to replace this judgment with a sprawling rule language for every exception. Teams then end up maintaining a second programming environment whose edge cases authorize the thing they meant to block. A small set of fixed controls is easier to inspect: the vault lock state, a run authorization, and an optional requirement to approve each use of a sensitive credential.
Approval does not compensate for a credential with unlimited authority. It gives a human a chance to stop an action before it leaves the machine. The underlying service must still enforce its own authorization, and the audit record must retain what happened after approval.
Build a remote action boundary before the first incident
The best first change is usually not a more complicated agent prompt. Replace one broad credential with one action boundary that has a stated input grammar, a limited target set, a timeout plan, and evidence that another operator can verify.
For an HTTP task, ask the service owner for an endpoint and credential whose permissions match the one action. Test rejected paths and methods as deliberately as successful calls. For an SSH task, create a dedicated account, disable interactive features, force a receiver program, and test malformed input against it. Run those tests from the same route the agent will use, because network access and identity checks often differ from an administrator's laptop.
Then test the failure nobody wants to simulate: complete the remote work and sever the connection before the response reaches the caller. If your agent cannot determine whether to wait, query, or retry from the retained record and remote state, the design will duplicate work under pressure.
The protocol choice becomes straightforward once you insist on those properties. Use the interface that grants the smallest capability you can name, gives you a reliable answer after failure, and leaves an account of the action that does not depend on somebody's memory of a terminal session.
FAQ
When should an AI agent use HTTP instead of SSH?
Use HTTP when the task maps to a narrow resource operation that the service can authenticate, authorize, validate, and record on its own. SSH is appropriate when the task truly needs host level inspection or a remote program that has no adequate API.
Is SSH always too dangerous for an AI agent?
SSH does not have to mean full shell access, but that outcome is common because teams copy an administrator's key setup. Restrict the account, force one command, disable forwarding, and make the command validate its arguments before you call it safe.
Do scoped API tokens eliminate blast radius?
API scopes limit actions only if the API actually enforces them on the endpoint and method the agent can reach. Read operations, mutation operations, token creation, and export functions often sit behind different permissions, so test each one rather than trusting a scope name.
What should logs record for agent actions?
A useful record identifies the agent process, the credential identity, the destination, the request or command, the authorization result, and the outcome. For mutations, retain the resource identifier and a request correlation value so an operator can reconstruct the change.
Can an agent safely retry a timed out remote action?
A timeout only proves that the client lacks an answer. The remote side may have completed the operation, may still be working, or may have rejected it after the connection dropped. Retry only after checking the operation's idempotency and querying for the expected state.
How do I measure the blast radius of an SSH command?
Treat a command as a capability with its own input grammar, operating user, working directory, and reachable network. A command that accepts arbitrary paths, shell fragments, or environment variables grants far more authority than its short name suggests.
Should I approve every API call an agent makes?
Approval should cover an identifiable executable process and expire when that process exits. A blanket approval for every future process removes the one boundary that lets a person notice a new or substituted agent before it acts.
Are read only API permissions safe for agents?
Usually, no. Read access can expose source code, customer data, configuration, or credentials stored in a poorly chosen location. Grant the smallest collection or endpoint set that answers the task, and separate discovery access from export access.
When is SSH the better choice for automation?
Use SSH when the API lacks the necessary operation, when you need local host facts, or when a controlled maintenance program already provides the safest interface. Do not choose it merely because a shell command feels quicker to write than an API client.
Why is a tamper evident audit trail different from normal logs?
An audit trail must resist silent edits and support a clear account of who authorized and performed an action. Ordinary application logs help with debugging, but administrators or compromised processes can often change or delete them without leaving evidence.