# SSH certificates for AI agents and local credential custody

SSH certificates and local credential custody work together. A certificate gives an SSH server a time-bounded statement about a public key. Local custody keeps the matching private credential out of an AI agent's context, files, subprocesses, and logs. Treating either one as a substitute for the other leaves a gap that shows up at the worst possible time.

The popular version of this design says that a 15 minute certificate makes everything safe. It does not. If the agent can read the private key, it can keep using that key until the certificate expires, ask for another certificate if it can reach issuance, or leave a copy in a workspace that survives the run. Short validity reduces the duration of server acceptance. It does not remove the capability from the process that holds the secret.

For agents that can change infrastructure, use certificates to reduce the accepted lifetime and use local custody to prevent private credential disclosure. Then design renewal, interruption, and revocation as one operational system. The details matter more than the certificate lifetime printed on a diagram.

## A certificate limits acceptance, not possession

An OpenSSH user certificate is a signed public key plus constraints. The SSH server verifies the CA signature and checks fields such as the certificate type, validity interval, principal list, critical options, and extensions. The client still must prove it controls the private key corresponding to the certified public key.

That distinction gets blurred constantly. A certificate is public material. You can place `id_ed25519-cert.pub` beside a private key, copy it to a client, and inspect it freely. The secret is still `id_ed25519`. If an agent receives that private file, short certificate validity only limits how long one issued certificate works. The agent still holds a reusable signing credential.

OpenSSH documents the certificate format in PROTOCOL.certkeys and exposes the usual creation controls in the `ssh-keygen` manual. The signing command takes a CA private key with `-s`, an identity string with `-I`, allowed principals with `-n`, and a validity specification with `-V`. Those are authorization inputs, not decorative metadata. A server must be configured to trust the CA and interpret the principals before any of them affect access.

Consider two designs:

- In the first, an agent receives `id_ed25519`, a certificate, and the CA endpoint. The agent can sign SSH challenges itself and may be able to make future certificate requests.
- In the second, a local executor holds `id_ed25519` in protected storage. The agent requests a named SSH action, and the executor handles the SSH authentication without returning private material.

Both designs can use the same certificate. Only the second stops an agent prompt injection, a compromised plugin, or an overly curious subprocess from copying the private credential.

Do not use a certificate identity field as proof of custody either. The `-I` value helps humans correlate issuance and access records, but any caller that can request certificates may choose an impressive-looking identity. Bind issuance to a known public key and a known agent session. If your issuer accepts arbitrary public keys and arbitrary identity strings, your audit trail will become fiction under pressure.

## Local custody changes the failure you must plan for

Keeping a credential local changes a secret-exfiltration incident into an action-authorization incident. That is a better problem, but it is still a problem. The agent may ask for a destructive command, target the wrong host, or use an approved session longer than the operator intended.

The local component should hold the private SSH credential and perform the protocol operation itself. The agent should receive command output, exit status, and limited diagnostic information. It should not receive the private key as text, a temporary file path, an environment variable, an SSH agent socket it can forward, or a pretend redacted value that another tool can resolve.

A common bad compromise mounts a secret directory read-only into the agent environment. Read-only protects against edits, not reads. Another bad compromise puts the private key in an SSH agent and gives every child process access to `SSH_AUTH_SOCK`. That may be acceptable for a tightly controlled interactive shell, but an autonomous coding agent spawns tools, test runners, package hooks, and helper processes. Each one expands who can ask the agent to sign.

Local custody also gives you a useful point for human control. A person can approve the first action from a new process, demand approval for sensitive credentials on every use, or lock the vault entirely. Those controls do not replace server authorization. They decide whether a process gets to attempt an action at all.

Keep this boundary narrow. The component holding the private key should not accept arbitrary shell text and blindly run it because the agent supplied it. It should know the host, target account, and action request it will execute, and it should record those facts before or alongside execution. If you cannot reconstruct which process asked for `ssh deploy@host`, local custody has become a secret wrapper with poor accountability.

The practical consequence is straightforward: certificate renewal should refresh public authorization, while local custody continues to control private signing. Do not rebuild the private credential every time a certificate expires unless you have a separate reason to rotate it.

## Certificate lifetime should follow interruption tolerance

Choose validity based on how long you can tolerate an already issued credential after you halt new work. The right answer differs for a read-only diagnostic run, a deployment workflow, and an agent that can alter access controls.

Start with a duration that leaves room for real work. Certificate renewal fails more often than teams expect because laptops sleep, VPN routes change, the CA is briefly unavailable, a long command holds a connection open, or the machine clock drifts. A very short lifetime turns ordinary interruptions into a constant source of retry logic and tempted bypasses.

For many autonomous tasks, a certificate valid for 15 to 60 minutes is a reasonable first setting. Use the lower end where the account can affect production systems. Use a longer window only when the job truly needs it and you can state why. A multi-hour certificate for a job that runs for ten minutes is usually convenience disguised as operational necessity.

Do not confuse certificate validity with SSH connection lifetime. SSH authenticates when it establishes a connection. A server normally does not eject an already authenticated session when the certificate reaches its expiry time. Connection multiplexing can make this more surprising: a later command may reuse an existing authenticated master connection rather than perform a fresh certificate check.

That behavior changes the design:

1. Set certificate validity to bound new authentications.
2. Bound command or session duration separately in the local executor.
3. Avoid handing an agent a reusable multiplexed control socket.
4. Terminate active sessions during an emergency when the action itself must stop.

The OpenSSH `ssh_config` option `ControlMaster` is useful for interactive speed, but it undermines simple assumptions about per-command authentication. For autonomous work, disable multiplexing for sensitive targets or make the executor own and tear down the connection. Do not let an agent inherit a control path that happens to stay authenticated after its approval or certificate window should have ended.

Clock discipline also belongs in this section. A certificate has an absolute validity interval. If the issuing host and target server disagree materially on time, a freshly minted certificate can look expired or not yet valid. Monitor time synchronization on both sides, and fail closed when the server rejects the validity window. Do not work around a clock failure by issuing a much longer certificate.

## Renewal must prove the same custody boundary still exists

A renewal service should issue a new certificate only after it can tie the request to the locally held credential, the current agent session, and the requested authorization scope. Merely accepting a public key in an HTTP request proves nothing about who controls the matching private half.

The cleanest flow asks the local executor to prove possession of its registered public key during renewal. The issuer verifies that proof, checks the active session and requested principal, signs the public key, and returns only the public certificate. The executor attaches that certificate to its internally held private credential for the next SSH connection.

A certificate creation command might look like this on the issuer:

```
ssh-keygen -s agent_user_ca -I run-4821 -n deploy-prod -V +30m agent-run-4821.pub
```

This command signs `agent-run-4821.pub` with the CA private key. It creates a companion public certificate, usually named `agent-run-4821-cert.pub`. The `run-4821` string helps correlate records, `deploy-prod` is the allowed principal, and `+30m` requests a thirty minute interval. The issuer should generate that identity string itself rather than trust an agent-provided label.

Inspect every new certificate before you let automation depend on it:

```
ssh-keygen -L -f agent-run-4821-cert.pub
```

The output lists the certificate type, signing CA fingerprint, identity, serial, validity range, principals, critical options, and extensions. Make this inspection part of tests for your issuer. A missing principal, a validity range that starts in the future, or an unexpected extension will otherwise surface as a failed deployment with somebody reaching for a long-lived fallback key.

Renew before the work needs a new SSH connection, not at the exact expiry second. The executor can request a replacement near the end of the interval while the existing certificate still works. It should also refuse to start a command if too little certificate lifetime remains for the command's allowed duration. That check prevents a job from beginning an important write just before the next authentication fails.

Keep renewal idempotent at the request level. A lost response can make a client retry even though the issuer already created a valid certificate. Record a request identifier, session identifier, public-key fingerprint, principal, and expiry. On a retry, return the previously issued certificate when the inputs match. If they differ, reject the request rather than guessing which request the agent meant.

## Principals and server policy decide what the certificate can do

A certificate says who may authenticate. The server still decides which local account accepts that identity and what that account can do. If you allow an agent certificate to authenticate as a broad shared administrator account, short validity will not rescue a loose authorization decision.

OpenSSH can trust a user CA through `TrustedUserCAKeys`. You can then map accepted principals with `AuthorizedPrincipalsFile` or `AuthorizedPrincipalsCommand`. The latter is useful when a server needs a centrally managed account mapping, but it adds an availability dependency to login. A static principals file is less flexible and often easier to reason about for a small fleet.

A constrained server configuration can look like this:

```
TrustedUserCAKeys /etc/ssh/agent_user_ca.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
RevokedHostKeys /etc/ssh/revoked_agent_credentials.krl
```

For a local account named `deploy`, `/etc/ssh/auth_principals/deploy` might contain only:

```
deploy-prod
```

That layout means the server accepts a certificate from the named CA only when it contains `deploy-prod` as a principal and has not been revoked by the configured KRL. Test the exact version of OpenSSH on your servers. Configuration directives and certificate behavior are stable in principle, but distribution packaging and older releases can affect what you can rely on.

Certificate critical options and extensions can narrow use further. OpenSSH supports critical options such as `force-command` and `source-address`; it also recognizes extensions such as `permit-pty`, `permit-port-forwarding`, and `permit-agent-forwarding`. Use these when the target workflow has a narrow shape. An agent that only runs a deployment helper should not receive a general interactive shell by accident.

Do not put a generic `permit-pty` capability on an agent certificate because someone may need debugging later. Issue a separate, consciously approved diagnostic credential when you need one. Convenience extensions tend to remain long after the exceptional task that justified them.

## Emergency revocation needs more than waiting for expiry

Short validity handles routine containment. If you stop issuance and the certificate expires in thirty minutes, new SSH authentications stop after that window. That may be adequate when you caught an errant task before it touched a sensitive target. It is inadequate when a private credential may have escaped or an agent has begun harmful work.

Your first containment move is to stop new issuance for the relevant session, credential, or CA. Then prevent already issued credentials from authenticating where the urgency warrants it. In OpenSSH, a Key Revocation List, or KRL, gives servers a way to reject specific certificates, public keys, or a signing CA.

To add an issued certificate to a KRL, an operator can use a command shaped like this:

```
ssh-keygen -k -f revoked_agent_credentials.krl -s agent_user_ca.pub agent-run-4821-cert.pub
```

The `-s` argument identifies the CA that signed the certificate. Distribute the resulting KRL to the target servers, keep the path consistent with `RevokedHostKeys`, and reload sshd according to your operating procedure. Test the full path in advance: create a certificate, authenticate successfully, add it to the KRL, distribute the file, reload sshd, and verify that a new authentication fails.

A KRL is not a central kill switch. Every SSH server must receive the file and read it. If a host is unreachable, offline, or managed under a separate configuration regime, it may keep accepting the certificate until expiry. That is why a short validity interval remains useful even when you operate revocation lists.

When you suspect the CA private key itself, revoking individual certificates is the wrong scale of response. Remove or replace trust in that CA on affected servers, issue a new CA, and recertify only the credentials you still trust. Keep a distinct CA for agent access so this operation does not lock out human emergency access. Maintain a tested break-glass path, but keep it outside the agent's reach and record its use.

Finally, kill active sessions. KRL distribution blocks future authentication; it does not necessarily terminate an SSH connection that the server already accepted. Use your host session controls, job supervisor, or network controls to stop live work. Then review commands already executed, because credential revocation cannot undo a deployment or restore a deleted file.

## A certificate issuer must fail closed during uncertainty

The issuer should reject renewal when it cannot verify the local executor's identity, the session state, the requested principal, or the authorization required for that scope. Availability pressure makes teams turn these checks into warnings. That decision converts a temporary outage into unbounded credential issuance.

Plan for the ordinary failure cases before deployment day. If the issuer is unreachable, let an existing certificate work until its normal expiry, but do not silently substitute a static private key. If the local vault is locked, deny the action. If a human approval is required and nobody responds, expire the work rather than allowing the agent to retain an approval indefinitely.

A useful renewal record includes the CA fingerprint, certificate serial, issuance time, expiry time, public-key fingerprint, requested principal, agent process identity, session identifier, target class, and approval result. The certificate itself contains some of this information, but it does not tell you whether the request followed your intended local control path.

The issuer should also prevent scope drift across renewal. A session approved for `deploy-prod` should not renew into `root-prod`, a broader principal, merely because the agent revised its plan. Require a new authorization boundary for a different account, target group, or command class. This is where long-running autonomous jobs often become unsafe: the session starts narrow, then accumulates exceptions through renewal logic nobody reviews.

Use separate records for issuance and execution. Issuance proves that the CA authorized a certificate. SSH server logs and executor activity records prove where a credential was used and which command ran. You need both when an operator asks whether a certificate was misissued or correctly issued but misused.

## Test revocation as a timed operational drill

A runbook that says revoke the certificate is incomplete until it names the systems, files, access rights, expected failure, and person who can act when the issuer is down. Run the drill against a nonproduction target, with real certificates and the same distribution mechanism you use in production.

Use this sequence:

1. Issue a certificate with a known serial and a short, documented interval.
2. Authenticate once and record the server-side acceptance event.
3. Disable renewal for the associated agent session.
4. Add the certificate to the KRL, distribute it, and reload the target sshd instances.
5. Attempt a new SSH connection and confirm that the server rejects it, then terminate the original live session if one exists.

Measure the actual time from the containment request to rejection on each server group. Do not publish a target number you have never observed. The number depends on how quickly your configuration system delivers the KRL and how reliably it reloads sshd.

Also test the uncomfortable cases. Revoke a certificate when one target host is unreachable. Revoke during an existing multiplexed connection. Revoke while the local machine is asleep and renewal cannot complete. Revoke after an operator has approved a session but before the first command. Each result tells you whether your controls block issuance, new authentication, or active execution. They are different controls, and calling all of them revocation creates dangerous assumptions.

Store the evidence from the drill. Keep the issued certificate, the KRL update, the executor record, and the target server's rejection log. When a real incident happens, that packet gives responders a verified command path instead of a document that has aged into wishful thinking.

## Keep approval, audit, and SSH access connected

Human approval is useful only when the approval maps to the process that will use it. Approving an unnamed background process is an invitation to approve the wrong thing. The reviewer should see who signed the process, what session it is starting, and whether the requested SSH credential carries an ordinary or sensitive scope.

Sallyport keeps SSH credentials in its encrypted local vault and executes SSH through its bundled helper, so an MCP-capable agent does not need the private SSH key in its own environment. Its session and activity journals can connect a human approval with the agent process and subsequent action records.

The audit trail needs tamper evidence as well as convenient viewing. Sallyport projects its journals from an encrypted, hash-chained audit log, and `sp audit verify` lets an operator verify that chain offline without needing vault access. Run that verification during incident review and when you export evidence; a readable activity list alone does not prove that nobody removed an inconvenient event.

Do not make the audit system responsible for access policy it does not enforce. The SSH server must still trust the right CA, accept only intended principals, read the current KRL, and restrict the target account. The local executor must still protect private credentials and ask for the approvals you chose. Logs give you evidence and operational feedback. They do not repair an account with excessive permissions.

The strongest starting move is not a smaller certificate duration. Inventory which agent actions need SSH, give each one a narrow principal and account, place the private credential behind local custody, then rehearse the path that halts issuance, distributes revocation, and ends live sessions. Once that works, choose the shortest certificate lifetime your actual renewal system can sustain.
