# SSH (remote host, read-only)

> Connect a Linux host over SSH with a dedicated read-only user and key; what Connect and verify runs, what the agents can and cannot do with it today, and where approved commands run.

_How-to · Updated 2026-10-23 · Describes release 0.1.164_

## Overview {#overview}

An SSH connection reaches a server the platform has no API for: an on-premises VM, a bare-metal host, a jump box. The catalog lists it under On-prem / network. Two facts decide how to use it:

- The connection check is real: **Connect and verify** runs `uptime` on the host with the saved key, so reachability, the key and the login are proven before anything else depends on them.
- In this release an investigation, a chat turn and an estate x-ray refuse every command through an SSH connection, read commands included. Only `kubectl` has the platform's verified read adapter; for any other binary the `<name>_exec` tool answers `Investigation connections are read-only. This operation was not executed; prepare an incident change using a write connection and obtain approval.` whatever read verbs the catalog lists. The connection's agent-facing value today is the inventory note and the check; the host's measurements reach the agent through a metrics or log backend the host reports to.

Where SSH does run commands is the write side: a second, write-role SSH connection bound to a `cli` remediation route runs the approved command after a human approval, through the Change executor and never through the investigation loop.

## Setup {#setup}

### Before you start {#before-you-start}

- The `datasources.manage` permission; the built-in owner and operator roles carry it.
- An unencrypted private key for a read-only account on the host ([Installation](#installation)).
- The host's SSH port reachable from the platform, or a [private-network agent](/help/private-access.md) on its network.

### Installation {#installation}

On a secure workstation create a dedicated, unencrypted key — the connector cannot answer a passphrase prompt — and give only its public key to the server administrator. [Connect on-premises servers](/help/setup-on-prem.md) has the commands, and a forced-command wrapper that limits the key to `uptime`, `df -h`, `free -m` and `uname -a`. The administrator creates an account such as `aiops-reader` with no sudo, no application write permission and access only to the logs and host facts you intend to share, installs the public key in its `authorized_keys`, and confirms the host's fingerprint. The host must accept non-interactive key authentication on the port you enter.

### Configuration {#configuration}

1. Open **Settings · Data in → Data sources** and select **Connect datasource**.
2. Under **Type**, choose **SSH (remote host, read-only)**. Enter a **Connection name**; the connected list shows it, and per-connection agent tools are named after it.
3. Fill the fields:

| Field (portal label) | API key | Value |
|---|---|---|
| Host (IP or DNS) | `host` | `10.2.0.15` or `edge-01.example.com` |
| SSH port (usually 22) | `port` | `22` |
| Read-only user | `user` | `aiops-reader` |
| Private key (PEM) | `private_key` | The whole key file, `-----BEGIN` line included; a secret, checked for its armor on save |

4. Optionally, open **Optional · describe this environment** and fill **What should AIOps know about it?** with what runs on the host.
5. Under **Use this connection for**, choose **Investigate only** and select **Connect and verify**.

The same connection through the API (`POST /api/aiops/{tenant_id}/datasources`, requires `datasources.manage`; the key goes in `secret_values`, and one posted in `config` is moved there):

```json
{
  "type_key": "ssh",
  "name": "edge-01",
  "description": "On-prem edge host running the checkout reverse proxy; nginx logs under /var/log/nginx.",
  "config": {
    "host": "edge-01.example.com",
    "port": "22",
    "user": "aiops-reader"
  },
  "secret_values": {
    "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n<key>\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  "role": "read",
  "enabled": true
}
```

A key without its armor is refused at save time with `400 'private_key' does not look like a PEM key (missing -----BEGIN block) — paste the full key file`.

### Validation {#validation}

**Connect and verify**, and **Test** on the row later, looks for `ssh` on the host's PATH, tries `ssh --version`, then runs the saved connection vector — `ssh -i <key file> -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 -p <port> <user>@<host> uptime`. The key is written to a private file for the call; `BatchMode` means no prompt is ever answered, `accept-new` records the host key on first contact and refuses a changed one afterwards, and the five-second connect timeout keeps a host that drops packets from hanging the check. **You should see** a dialog titled `<connection name> is connected` with **Connected — verified from the control plane**, and later in the row's **Last test** `ok · binary found: /usr/bin/ssh — … — 'ssh uptime' ok (<the host's uptime line>)`. When a private-network agent serves the workspace, the platform does not dial the host: the row shows the check as queued, the agent runs it, and the result reads **Connected — verified by your private-network agent**.

### If something fails {#if-something-fails}

The dialog shows **Needs a fix**, what to fix and the check's own detail; the row is saved either way. Select **Edit**, correct the field the detail names and save again, or select **Retry** once a cause outside the form (network, vendor-side permission) is fixed. Every verdict this connector produces, and what to do about it, is in [Troubleshooting](#troubleshooting) below.

## Data collected {#data-collected}

### Tools the agent gets {#tools}

- `<name>_exec` — registered for a read-role connection, with the connection vector preconfigured so the agent would pass only the remote command (`args=["df", "-h"]`). The catalog lists `cat`, `ls`, `ps`, `df`, `free`, `uptime`, `uname`, `tail`, `head`, `grep`, `netstat`, `ss`, `ip`, `hostname`, `id`, `du`, `vmstat`, `iostat`, `top`, `stat`, `last`, `w` and `who` as its read verbs for the approval gate.

### Read surface {#read-surface}

The approval gate's verb list never comes into play: the investigation's read floor runs before it, and that floor verifies reads for `kubectl` only. Every call to an SSH `<name>_exec` tool — a listed read verb, an unlisted command, a chained `df; rm -rf …` — is refused with `Investigation connections are read-only. This operation was not executed; prepare an incident change using a write connection and obtain approval.`, and the estate x-ray denies it the same way and counts the denial. Nothing is executed on the host by any agent. The full rule table is under **The read/write gate in detail** in [Connections and remediation access](/help/read-and-write-roles.md#read-role-vs-write-role).

### Approved commands {#approved-commands}

Approved writes use a separate connection: connect the write-capable account and key as a second SSH connection, choose **Approved changes only (separate credential)** under **Use this connection for**, tick **Allow use for approved plans**, and save with **Save remediation credential**. Agents never see a write-role connection. A `cli` remediation route on it names the exact command; after approval the Change executor runs it over the same connection vector, records what ran, and validates afterwards. Keep the OS account's own permissions narrow as well — a sudoers rule for the one command, no unrestricted sudo — because the platform's gate is a floor and the host is the boundary. How approvals and tiers apply is in [Set up write connections](/help/setup-write-connections.md).

## Troubleshooting {#troubleshooting}

| What you see | Cause | What to do |
|---|---|---|
| `400 'private_key' does not look like a PEM key (missing -----BEGIN block) — paste the full key file` | The paste lost the armor lines or the line breaks. | Paste the complete key file into the multi-line field. |
| `failed · binary 'ssh' not found on PATH` | The host that runs the check has no ssh client. | Ask your platform operator; a private-network agent needs it in its image. |
| `failed · 'ssh uptime' failed (exit 255): …Permission denied (publickey)…` | The public key is not in the user's `authorized_keys`, the user is wrong, or the key is passphrase-protected. | Install the public key for that user; use an unencrypted key. |
| `failed · 'ssh uptime' failed (exit 255): …Connection timed out…` or `…Connection refused…` | No route from the platform to the host, or the wrong port. | Check the port; for a host on a private network, install a [private-network agent](/help/private-access.md) inside it. |
| `failed · 'ssh uptime' failed (exit 255): …Host key verification failed…` | The host key changed since the first contact; `accept-new` refuses a changed key. | Confirm the change with the administrator; your platform operator clears the recorded key. |
| `failed · 'ssh uptime' timed out after 5s` | The host accepted the connection but did not answer. | A forced-command wrapper that does not allow `uptime` blocks the check; allow it. |
| The row shows the check queued for the private agent and never returns | No live agent, or the agent cannot reach the host. | Check the agent's heartbeat under **Connect** and its route to the host. |
| Every command through `<name>_exec` answers `Investigation connections are read-only.` | The investigation read floor verifies kubectl reads only. | Expected in this release; see [Read surface](#read-surface). |
| An approved command runs but the host refuses it | The OS account lacks the permission. | Grant it with a narrow sudoers rule on the write account. |

The generic verdict strings are explained in [Test a connection and read the failure](/help/manage-connections.md#troubleshooting). Every connection status word is in the [status and error reference](/help/status-and-error-reference.md#connection-statuses).

## Next steps {#next-steps}

- Read what an investigation may run through this connection today: [Read surface](#read-surface).
- Run approved commands through a separate write connection: [Approved commands](#approved-commands).
- The on-premises walkthrough, key commands included: [Connect an on-premises server](/help/setup-on-prem.md).
- See what else each agent needs in [Choose your connection](/help/choose-your-connection.md#agent-needs).
