Install
openclaw skills install sparkeyProvides time-limited, self-revoking SSH access for AI agents using certificate TTL, user expiry, forced command restrictions, and scheduled automated cleanup.
openclaw skills install sparkeyDefense-in-depth temporary SSH access for AI agents — combines certificate TTL, OS account expiration, command-restricted dispatch, and automated cleanup. Zero session artifacts survive — all agent accounts, keys, certificates, dispatch shells, and cleanup timers are destroyed on session end or TTL expiry. The CA private key is a persistent operator-side credential requiring separate protection (see Security Considerations).
Platform: Linux (requires standard user-management tools).
Scripts check for required tools at startup and report what is missing. Install them on common distributions:
Debian / Ubuntu:
sudo apt-get update && sudo apt-get install -y openssh-client coreutils passwd at e2fsprogs procps
Alpine Linux:
apk add openssh-keygen bash shadow coreutils util-linux procps at e2fsprogs
RHEL / CentOS / Fedora:
sudo dnf install -y openssh-clients coreutils shadow-utils at e2fsprogs procps-ng
| Tool | Package (Debian) | Package (Alpine) | Required |
|---|---|---|---|
ssh-keygen | openssh-client | openssh-keygen | Yes |
useradd / userdel | passwd | shadow | Yes |
passwd | passwd | shadow | Yes |
pkill | procps | procps | Yes |
getent | libc-bin | musl-utils | Yes |
at | at | at | One of at or systemd-run |
shred | coreutils | coreutils | Optional (falls back to rm) |
chattr | e2fsprogs | e2fsprogs | Optional (for immutable authorized_keys) |
If a required tool is missing, the script exits with an error listing missing dependencies. Optional tools produce warnings but do not block execution.
The target server (the machine the agent will SSH into) must have:
~/.ssh/ directory for the user whose authorized_keys will receive the agent's public key (the agent's one-liner creates this if missing)TrustedUserCAKeys configured in /etc/ssh/sshd_config pointing to the CA public keyIf the target is a fresh container or VM, install and start sshd first:
# Alpine
apk add openssh && ssh-keygen -A && rc-service sshd start
# Debian/Ubuntu
apt-get install -y openssh-server && systemctl start sshd
When an AI agent determines it needs shell access, it briefly states its intent: "I need shell access to [target] to [reason]. I'll generate a temporary key — you decide where it goes. What username should I connect as?" Then:
Verify target is reachable — nc -z -w 2 TARGET 22 (or equivalent). If the port is closed, stop and inform the user: "Port 22 is not open on [target]. Please start sshd, then I'll continue."
Check for existing session — look for stale keys in /tmp/agent_access_key*. If found, test connectivity (ssh -i /tmp/agent_access_key ... 'echo ok'). If the key still works, skip to step 7. If expired or revoked, shred stale keys and continue.
Generate keypair locally — Ed25519, stored in /tmp/ with a session-tagged comment.
Ask for the connect username — default to the current user on the target, not root. Ask: "What username should I connect as? (default: your current user)". If the task requires root, inform the user: "This task requires root. Please add the key to root's authorized_keys (or use sudo)."
Present public key with expiry — provide a one-liner with expiry-time for crash safety. The one-liner creates ~/.ssh/ if absent and is tailored to the target username:
mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo 'expiry-time="YYYYMMDDHHMMSS" ssh-ed25519 AAAA... agent-session-SID' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
expiry-time (OpenSSH 8.2+) ensures the key is server-rejected after the timestamp, even if the agent crashes and never cleans up.
Important: The one-liner must run as the target user so ~/.ssh/ resolves to the correct home directory. If adding the key for a different account (e.g., root), either su - root first or use the absolute path (e.g., /root/.ssh/authorized_keys).
User adds the key — the user decides where and how to grant access (maintains control).
Verify connection — connect as the agreed username: ssh -i /tmp/agent_access_key USER@TARGET 'echo ok'
Schedule dead-man cleanup — immediately after connecting, schedule automatic key removal on the target:
echo "sed -i '/agent-session-SID/d' ~/.ssh/authorized_keys" | at now + 4 hours
This fires independently of the agent. If the agent finishes early, it cancels the job and cleans up itself.
Run fast recon — OS, key packages, screen/tmux availability. Under 10 seconds.
Create shared session — start a screen or tmux session on the target so the user can attach and observe in real time.
Work — run diagnostics, remediation, or other tasks within the granted scope.
Clean up — remove the public key from authorized_keys, cancel the at job, destroy the local keypair.
Two independent mechanisms protect against agent crashes:
| Safeguard | Fires without agent? | Mechanism |
|---|---|---|
expiry-time in authorized_keys | Yes | Server-side — OpenSSH rejects the key after the timestamp |
Scheduled at job | Yes | Runs on target's scheduler — removes the key entry on timer |
If the agent crashes and restarts, it reuses the stale keys from /tmp/ (step 2) rather than re-provisioning, avoiding repeated user interaction.
This flow requires no script transfer to the target and leaves no persistent artifacts. For stronger guarantees (certificate TTL, command restriction, scheduled cleanup), layer the CA-based workflow below on top.
After establishing access, the agent should create a shared terminal session:
# On the target — agent creates a named session
screen -dmS agent-work
screen -S agent-work -X multiuser on
screen -S agent-work -X acladd root
# User attaches to watch
screen -x agent-work
If screen is unavailable, tmux works similarly:
tmux new-session -d -s agent-work
# User attaches:
tmux attach -t agent-work
The agent should check for screen/tmux availability during initial recon and install if needed (with user permission).
Four independent expiration and restriction mechanisms ensure no single failure leaves access open:
| Layer | Mechanism | What It Does |
|---|---|---|
| 1 | SSH Certificate TTL | Cryptographically enforced expiry (-V +Nh) — server rejects expired certs |
| 2 | OS Account Expiration | useradd --expiredate — login denied after date |
| 3 | Forced Command / Safe Dispatch | Exact command-name matching, path-restricted arguments, no eval |
| 4 | Scheduled Cleanup | at or systemd-run timer — removes account, keys, and config after TTL |
All scripts run on the operator's machine (the admin/signing host), not on the target server. Only public artifacts (CA public key, dispatch shell, agent account) are deployed to the target.
# 1. One-time: Initialize the SSH Certificate Authority (run on admin/signing host)
sudo bash scripts/setup-ca.sh
# 2a. Preview what would happen (no changes made):
sudo bash scripts/grant-access.sh --host TARGET_HOST --duration 4h --agent-pubkey /path/to/agent_key.pub --dry-run
# 2b. Grant access using the agent's existing public key (recommended):
sudo bash scripts/grant-access.sh --host TARGET_HOST --duration 4h --agent-pubkey /path/to/agent_key.pub
# 2c. Or generate a keypair (you must securely deliver the private key to the agent):
sudo bash scripts/grant-access.sh --host TARGET_HOST --duration 4h --allow diagnostic
# 3. Revoke access: Immediately revoke before TTL expires (if needed)
sudo bash scripts/revoke-access.sh --session SESSION_ID
# 4. Audit: Scan for orphaned artifacts from previous sessions
sudo bash scripts/audit.sh
Run scripts/setup-ca.sh once on the signing host (your admin machine, not the target server).
This script:
/etc/ssh/agent_ca (private) and /etc/ssh/agent_ca.pub (public)chmod 400) on the private keyOn each target server, add to /etc/ssh/sshd_config:
TrustedUserCAKeys /etc/ssh/agent_ca.pub
Then restart sshd to trust certificates signed by your CA.
Run scripts/grant-access.sh to provision a temporary session:
# Create account with OS-level expiration (Layer 2)
# Account suffix uses 4 bytes of entropy (8 hex chars) with collision check
sudo useradd \
--expiredate "$(date -d '+5 hours' +%Y-%m-%d)" \
--shell /bin/bash \
--create-home \
--comment "AI Agent Support [SESSION_ID]" \
agent_support_XXXXXXXX
--expiredate sets an OS-level hard cutoff (rounded to midnight — a safety net, not the primary TTL)getent passwd (no eval)Recommended: Agent provides its own public key (--agent-pubkey)
The agent retains its private key — no secret transfer required. The script signs the agent's public key with the CA (or installs it in authorized_keys).
sudo bash scripts/grant-access.sh --host TARGET --duration 4h --agent-pubkey /path/to/agent.pub
Alternative: Script generates a keypair
If --agent-pubkey is omitted, the script generates an ephemeral Ed25519 keypair in /tmp/. The operator must then securely deliver the private key to the agent.
ssh-keygen -t ed25519 -N "" -f /tmp/agent_session_XXXX -C "agent-support-SESSION_ID"
-N "") — the AI agent cannot enter passphrases interactively/tmp/ until cleanup (securely deleted with shred -u)# Sign the public key with the CA, enforcing time limit
ssh-keygen -s /etc/ssh/agent_ca \
-I "agent-support-SESSION_ID" \
-n agent_support_XXXXXXXX \
-V +4h \
-O no-port-forwarding \
-O no-x11-forwarding \
-O no-agent-forwarding \
-O no-pty \
-O force-command=/usr/local/bin/agent-support-shell-SESSION_ID \
/path/to/agent.pub
Certificate options explained:
-s /etc/ssh/agent_ca — sign with the CA private key-I "agent-support-SESSION_ID" — identity string for audit logs-n agent_support_XXXXXXXX — principal must match the account name-V +4h — certificate valid for exactly 4 hours (cryptographic enforcement)-O no-port-forwarding — prevent SSH tunneling-O no-x11-forwarding — no GUI forwarding-O no-agent-forwarding — no agent key forwarding-O no-pty — omit if the agent needs an interactive shell (--with-pty)-O force-command=... — all commands routed through the safe dispatch shellEach session receives its own restriction shell at /usr/local/bin/agent-support-shell-SESSION_ID. The shell uses safe dispatch — no eval, no prefix matching:
;, |, &, $, backticks, (), {}, <>, \ all rejected before parsingread -racase statement (not prefix matching)systemctl status allowed, systemctl enable denied)"$COMMAND" "${ARGS[@]}" with a 5-minute timeout/sbin/shutdown)For broader access profiles, use the --allow flag:
| Profile | Commands Permitted |
|---|---|
diagnostic | Read-only: system info, logs (path-restricted to /var/log/, /proc/, /sys/, /run/, /tmp/), service status, network diagnostics, Docker inspect |
remediation | Diagnostic + service restarts, process kills, file operations (path-restricted to /etc/, /var/, /tmp/), Docker management, curl/wget |
full | Unrestricted (use with extreme caution — no restriction shell installed) |
# Scheduled via at(1) or systemd-run with ceiling-rounded delay
echo "bash /usr/local/sbin/agent-cleanup-SESSION_ID.sh" | at now + 80 minutes
Each session receives its own cleanup script (agent-cleanup-SESSION_ID.sh) to prevent concurrent-session collisions.
The AI agent uses the session credentials:
# With agent-provided key:
ssh -i ~/.ssh/agent_key agent_support_XXXXXXXX@TARGET_HOST 'uptime'
# With generated key:
ssh -i /tmp/agent_session_XXXX agent_support_XXXXXXXX@TARGET_HOST 'df -h'
Access terminates automatically through multiple independent mechanisms and can also be revoked on demand.
-V window. Server logs: error: Certificate invalid: expired--expiredate. Logs: User account has expiredsudo bash scripts/revoke-access.sh --session SESSION_ID
# or
sudo bash scripts/revoke-access.sh --user agent_support_XXXXXXXX
# or revoke all:
sudo bash scripts/revoke-access.sh --all
The revocation script:
usermod -L)chattr -i)/var/log/agent-support-archive/userdel -r)shred -u; falls back to rm -f)All agent activity is logged to /var/log/agent-support.log with sanitized entries (via printf '%q' to prevent log injection):
[2026-03-15T16:00:00Z] SESSION_START: 20260315160000-a1b2c3d4 user=agent_support_e5f6g7h8 host=TARGET ttl=4h profile=diagnostic
[2026-03-15T16:00:05Z] AGENT_EXEC: uptime
[2026-03-15T16:00:10Z] AGENT_EXEC: df\ -h
[2026-03-15T16:00:15Z] AGENT_BLOCKED: rm\ -rf\ /
[2026-03-15T20:00:00Z] SESSION_END: 20260315160000-a1b2c3d4 reason=ttl_expired
# 1. Create temporary account (expires tomorrow as safety net)
sudo useradd --expiredate "$(date -d '+1 day' +%Y-%m-%d)" \
--shell /bin/bash --create-home agent_support
# 2. Accept the agent's public key (recommended) or generate a keypair
# Option A: Agent provides their pubkey
PUBKEY="/path/to/agent_pubkey.pub"
# Option B: Generate
ssh-keygen -t ed25519 -N "" -f /tmp/agent_key -C "agent-support"
PUBKEY="/tmp/agent_key.pub"
# 3. Sign with CA (4-hour window, restricted)
ssh-keygen -s /etc/ssh/agent_ca \
-I "agent-manual-session" \
-n agent_support \
-V +4h \
-O no-port-forwarding \
-O no-x11-forwarding \
-O no-agent-forwarding \
"$PUBKEY"
# 4. Verify the certificate
ssh-keygen -L -f "${PUBKEY%.pub}-cert.pub"
# 5. Schedule cleanup
echo "userdel -r agent_support && shred -u /tmp/agent_key*" | at now + 5 hours
# 6. Deliver the certificate (and private key if generated) to the AI agent
# Lock the account immediately
sudo usermod -L agent_support
# Kill active sessions
sudo pkill -u agent_support
# Remove account and home directory
sudo userdel -r agent_support
# Destroy session keys
shred -u /tmp/agent_key /tmp/agent_key-cert.pub /tmp/agent_key.pub 2>/dev/null
# Remove scheduled cleanup
atq | grep agent && atrm JOB_NUMBER
For environments where a CA is not feasible:
sudo bash scripts/grant-access.sh --host TARGET --duration 4h --no-ca --agent-pubkey /path/to/agent.pub
This installs the agent's public key in authorized_keys with the following restrictions:
expiry-time="YYYYMMDDHHMMSS",restrict,command="/usr/local/bin/agent-support-shell-SID" ssh-ed25519 AAAA...
expiry-time — key rejected after this UTC timestamprestrict — disables port/agent/X11 forwarding and PTY allocationcommand="..." — forces all commands through the safe dispatch shellchattr +i makes the file immutable to prevent modificationLimitation: expiry-time is server-side only and lacks cryptographic enforcement. Always pair with chattr +i.
No passphrase on session keys — mitigated by short-lived certificates (hours, not days), immediate key destruction, and immutable authorized_keys
Command injection — the dispatch shell blocks all shell metacharacters, matches command names exactly via case (not prefix), validates argument paths against directory allowlists, resolves symlinks before comparison, and rejects absolute paths as command names; commands execute directly without eval
Key delivery — use --agent-pubkey (recommended) so the private key never leaves the agent; if generating keys, the operator must deliver the private key securely
Scope creep — start with diagnostic; escalate to remediation or full only when explicitly needed
Symlink traversal — path validation resolves symlinks via readlink -f before checking directory prefixes; a symlink from /var/log/evil → /etc/shadow is resolved and rejected
If the agent loses access mid-session (certificate expired, context lost, network failure):
lxc-attach, AWS SSM, GCP serial console) to regain accesssudo bash scripts/revoke-access.sh --all to clean up orphaned accountsgrant-access.sh again for a fresh sessionCritical rule: never disable existing access paths (e.g., root SSH) before verifying the agent's own access works. Always keep at least one independent recovery path available.
MaxSessions 1 for the agent account in sshd_configMaxAuthTries 3 to limit brute-force attemptsAuthenticationMethods publickey to restrict to key-based authLogLevel VERBOSE in sshd_config/var/log/auth.log and /var/log/agent-support.log during active sessions| Symptom | Cause | Fix |
|---|---|---|
Certificate invalid: expired | Certificate TTL elapsed | Re-run grant-access.sh for a new session |
Certificate invalid: name is not a listed principal | Username mismatch between cert -n and account | Ensure -n matches the created account name |
User account has expired | OS-level --expiredate passed | Create a new account with later expiry |
Permission denied (publickey) | CA public key not in TrustedUserCAKeys | Verify server config and restart sshd |
WARNING: UNPROTECTED PRIVATE KEY FILE | Key file permissions too open | chmod 600 /tmp/agent_session_XXXX |
Shell metacharacters are not allowed | Agent sent ;, |, $, etc. | Send simple commands without shell operators |
Command not in allowlist | Agent tried an unlisted command | Use a broader --allow profile or add the command |
Path outside allowed directories | Agent accessed a path outside allowed prefixes | Restrict to /var/log/, /proc/, /sys/ (diagnostic) or add /etc/ (remediation) |
Missing required dependencies | Linux tools not installed | Install openssh-client, coreutils, at or systemd |
Connection reset by peer (before auth) | IP auto-blocked by brute-force protection | Unblock the IP (fail2ban, Synology autoblock, etc.), then retry |
| Key added but auth fails | AuthorizedKeysFile points elsewhere | Check sshd_config; Proxmox uses /etc/pve/priv/authorized_keys |
Synology DSM — not fully compatible. DSM wraps sshd behind synorelayd, which intercepts SSH on port 22 and may silently block connections after repeated failures:
/var/services/homes/USER/ (symlinked from /volume1/homes/USER/)sshd_config on service restart777 — SSH silently rejects pubkey auth; fix with chmod 755PubkeyAuthentication yes must be explicitly uncommented in sshd_configadmin (not root) via the DSM-configured SSH portsudo /usr/syno/bin/synosystemctl restart synorelaydProxmox VE — may store authorized keys in /etc/pve/priv/authorized_keys instead of ~/.ssh/. Check AuthorizedKeysFile in sshd_config.
| File | Location | Purpose |
|---|---|---|
setup-ca.sh | scripts/ | One-time CA key pair generation (warns on key age >90 days) |
grant-access.sh | scripts/ | Provision temporary agent access (supports --dry-run, sshd pre-flight check) |
revoke-access.sh | scripts/ | Immediate access revocation with post-removal verification |
audit.sh | scripts/ | Scan for orphaned artifacts (accounts, keys, shells, at jobs) |
| CA private key | /etc/ssh/agent_ca | Signs session certificates (keep secure) |
| CA public key | /etc/ssh/agent_ca.pub | Distributed to target servers |
| Support shell | /usr/local/bin/agent-support-shell-SID | Per-session command dispatch |
| Cleanup script | /usr/local/sbin/agent-cleanup-SID.sh | Per-session auto-cleanup |
| Session log | /var/log/agent-support.log | Audit trail of all agent commands |
| Archive | /var/log/agent-support-archive/ | Archived logs from expired sessions |
| Category | Details |
|---|---|
| Environment variables | None accessed |
| External endpoints | None — this skill makes zero network calls |
| Local files read | /etc/passwd (user lookup), /etc/ssh/agent_ca (CA signing), agent public key (if provided) |
| Local files written | /etc/ssh/agent_ca{,.pub} (setup-ca.sh), /usr/local/bin/agent-support-shell-SID (per-session), /usr/local/sbin/agent-cleanup-SID.sh (per-session), /tmp/agent_session_SID{,.pub,-cert.pub} (ephemeral keys), /var/log/agent-support.log (audit), ~agent/.ssh/authorized_keys (no-ca mode) |
| OS state mutated | Creates/deletes Linux user accounts (useradd/userdel), schedules cleanup timers (at/systemd-run) |
| Data transmitted | No data leaves the local machine. All operations are local to the host. |
This skill operates entirely offline — no telemetry, no analytics, no data transmitted to external services. All sensitive material (private keys, certificates) is destroyed after session expiration via shred -u (falling back to rm -f). Audit logs remain on the local filesystem under /var/log/ for operator review.