# SSH & SFTP Host Key Rotation Monitor (`monema/ssh-sftp-host-key-monitor`) Actor

Monitor authorized public SSH and SFTP endpoints for negotiated host-key fingerprint changes with persistent cross-run history.

- **URL**: https://apify.com/monema/ssh-sftp-host-key-monitor.md
- **Developed by:** [Maarten Vreeburg](https://apify.com/monema) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 successful ssh host key checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## SSH & SFTP Host Key Rotation Monitor

Monitor the SSH server host key presented by authorized public SSH and SFTP endpoints. The Actor performs a bounded SSH handshake without authenticating, logging in, opening an SFTP session, or transferring data. It stores a named cross-run history and emits a structured record when the negotiated fingerprint changes.

This is useful for SFTP integrations, CI/CD systems, managed file-transfer jobs, and vendor endpoints where an unplanned host-key change can stop strict clients—or could indicate that the endpoint needs independent security review.

### What it detects

- first successful fingerprint for a target and selected host-key algorithm
- unchanged fingerprint
- a different fingerprint, with separate `fingerprint_first_seen` context
- return to a fingerprint seen in an earlier run
- mismatch against an optional allowlist of expected fingerprints
- connection, DNS, timeout, handshake, and algorithm-negotiation failures without overwriting the last successful state

### Input

Use a stable `monitorId` and 1–50 authorized public targets.

```json
{
  "monitorId": "finance-sftp-vendors",
  "targets": [
    {
      "host": "sftp.example.com",
      "port": 22,
      "hostKeyAlgorithm": "ssh-ed25519",
      "expectedFingerprints": [
        "SHA256:..........................................."
      ],
      "label": "Settlement export"
    }
  ],
  "timeoutSeconds": 15,
  "maxConcurrency": 3
}
````

| Field | Required | Description |
|---|---:|---|
| `monitorId` | Yes | Stable ID separating one watchlist's history from another. |
| `targets` | Yes | 1–50 public, authorized SSH/SFTP endpoints. Duplicate host/port/algorithm tuples are checked once. |
| `host` | Yes | Public FQDN or public IP address. Supply a host, not a URL. |
| `port` | No | TCP port, default `22`; allowed range 1–65535. |
| `hostKeyAlgorithm` | No | Exact algorithm to negotiate, default `ssh-ed25519`. Select the algorithm used by the client workflow you want to protect. |
| `expectedFingerprints` | No | Up to 20 trusted OpenSSH `SHA256:` fingerprints. Multiple values support planned overlap rotations. |
| `label` | No | Human-readable integration label. |
| `timeoutSeconds` | No | Total bounded timeout per target, 5–30 seconds. |
| `maxConcurrency` | No | 1–5 concurrent endpoint checks. |

Supported algorithm selectors are `ssh-ed25519`, NIST P-256/P-384/P-521 ECDSA, `rsa-sha2-512`, `rsa-sha2-256`, and legacy `ssh-rsa`. Selecting an RSA SHA-2 signature algorithm can still return a key whose underlying key type is `ssh-rsa`; the record includes both the requested negotiation algorithm and returned key type.

The default checks GitHub's documented SSH-over-HTTPS endpoint on port 443, so a default QA run is non-empty. This product is independent and is not affiliated with GitHub.

### Output

One record is emitted per target.

| `status` | Meaning |
|---|---|
| `initial_snapshot` | First successful observation; baseline and history were stored. |
| `unchanged` | Same fingerprint as the previous successful run. |
| `changed` | Different from the previous successful run. Check `fingerprint_first_seen` to distinguish a never-before-seen key from a previously observed key reappearing. |
| `connection_error` | DNS, policy, TCP, timeout, SSH handshake, or algorithm negotiation failed. Successful prior state is preserved and no custom event is charged. |

Important fields include `fingerprint_sha256`, `previous_fingerprint_sha256`, `fingerprint_first_seen`, `observed_fingerprint_count`, `expected_fingerprint_match`, `requested_host_key_algorithm`, `key_type`, `key_bits`, `server_identification`, `resolved_ip`, `checked_at`, and `state_store_mode`.

### Persistence and rotation overlap

Successful observations are stored in the named key-value store `ssh-sftp-host-key-monitor-state-v1`, keyed by SHA-256 of `monitorId` + host + port + selected algorithm. Up to 20 distinct observed fingerprints are retained per target.

Some managed SFTP services present old and new keys interchangeably during a staged rotation. Such transitions still have `status: changed`, but a key already in history has `fingerprint_first_seen: false`. Configure every independently verified planned fingerprint in `expectedFingerprints` to distinguish an allowed overlap from an unknown key.

### Pricing

The Actor charges one `ssh_host_key_check` event only after a public endpoint has resolved safely, completed the selected SSH host-key negotiation, produced a fingerprint, and stored the successful observation. Input rejects, blocked destinations, connection failures, and handshake failures are not charged by this event. The current Store price is shown on the listing.

### Security and responsible use

- Check only endpoints you own or are authorized to monitor.
- Production rejects private, loopback, link-local, multicast, and reserved destinations, including hostnames resolving to any such address.
- The connection is pinned to a validated resolved public address to reduce DNS-rebinding risk.
- The Actor performs one bounded SSH handshake per target and does not authenticate, send credentials, enumerate accounts, execute commands, open SFTP, or transfer files.
- Targets are capped at 50, concurrency at five, and time at 30 seconds per target.
- Failed checks do not overwrite a successful baseline and are not charged by the custom event.

### Critical trust limitation

A remotely observed host key is **not self-authenticating**. This Actor detects what the network endpoint presented from Apify's vantage point; it cannot prove that a first-seen or changed key belongs to the intended operator. Verify new fingerprints through an independent trusted channel such as the vendor's signed security notice, official documentation, account console, or support contact before updating production `known_hosts` data. Never auto-trust a changed fingerprint solely because this Actor observed it.

The Actor monitors one explicitly selected negotiation algorithm per target. It does not enumerate every key a server might advertise, test user authentication, validate SSH certificates against a CA, assess cipher/KEX policy, prove SFTP availability, or replace strict host-key verification in the real client.

### Scheduling

Schedule daily or hourly according to the vendor's change process. Route `changed`, `connection_error`, or `expected_fingerprint_match: false` records to a human review workflow using Apify integrations or webhooks. Do not automate acceptance of new keys.

### Local development

```bash
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
APIFY_LOCAL_STORAGE_DIR=./storage .venv/bin/python -m src
```

Run deterministic state and adversarial tests with:

```bash
.venv/bin/python smoke_test.py
```

# Actor input Schema

## `monitorId` (type: `string`):

A stable ID that keeps this watchlist's history separate from other runs.

## `targets` (type: `array`):

Authorized public endpoints. Select the host-key algorithm used by the client workflow you want to protect.

## `timeoutSeconds` (type: `integer`):

Total bounded TCP and SSH handshake timeout per endpoint.

## `maxConcurrency` (type: `integer`):

Number of authorized endpoints checked concurrently.

## Actor input object example

```json
{
  "monitorId": "qa-default",
  "targets": [
    {
      "host": "ssh.github.com",
      "port": 443,
      "hostKeyAlgorithm": "ssh-ed25519",
      "label": "GitHub SSH over 443"
    }
  ],
  "timeoutSeconds": 15,
  "maxConcurrency": 3
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "targets": [
        {
            "host": "ssh.github.com",
            "port": 443,
            "hostKeyAlgorithm": "ssh-ed25519",
            "label": "GitHub SSH over 443"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("monema/ssh-sftp-host-key-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "targets": [{
            "host": "ssh.github.com",
            "port": 443,
            "hostKeyAlgorithm": "ssh-ed25519",
            "label": "GitHub SSH over 443",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("monema/ssh-sftp-host-key-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "targets": [
    {
      "host": "ssh.github.com",
      "port": 443,
      "hostKeyAlgorithm": "ssh-ed25519",
      "label": "GitHub SSH over 443"
    }
  ]
}' |
apify call monema/ssh-sftp-host-key-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=monema/ssh-sftp-host-key-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "SSH & SFTP Host Key Rotation Monitor",
        "description": "Monitor authorized public SSH and SFTP endpoints for negotiated host-key fingerprint changes with persistent cross-run history.",
        "version": "0.1",
        "x-build-id": "wM0PKlW8WCKHthifZ"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/monema~ssh-sftp-host-key-monitor/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-monema-ssh-sftp-host-key-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/monema~ssh-sftp-host-key-monitor/runs": {
            "post": {
                "operationId": "runs-sync-monema-ssh-sftp-host-key-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/monema~ssh-sftp-host-key-monitor/run-sync": {
            "post": {
                "operationId": "run-sync-monema-ssh-sftp-host-key-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "monitorId",
                    "targets"
                ],
                "properties": {
                    "monitorId": {
                        "title": "Monitor ID",
                        "minLength": 1,
                        "maxLength": 200,
                        "type": "string",
                        "description": "A stable ID that keeps this watchlist's history separate from other runs.",
                        "default": "qa-default"
                    },
                    "targets": {
                        "title": "SSH/SFTP targets",
                        "minItems": 1,
                        "maxItems": 50,
                        "type": "array",
                        "description": "Authorized public endpoints. Select the host-key algorithm used by the client workflow you want to protect.",
                        "items": {
                            "type": "object",
                            "properties": {
                                "host": {
                                    "title": "Public SSH/SFTP host",
                                    "description": "Public FQDN or public IP address; do not enter a URL.",
                                    "type": "string",
                                    "minLength": 1,
                                    "maxLength": 253,
                                    "editor": "textfield"
                                },
                                "port": {
                                    "title": "TCP port",
                                    "description": "SSH/SFTP service port.",
                                    "type": "integer",
                                    "minimum": 1,
                                    "maximum": 65535,
                                    "default": 22,
                                    "editor": "number"
                                },
                                "hostKeyAlgorithm": {
                                    "title": "Host-key algorithm",
                                    "description": "Exact negotiation algorithm used by the client workflow to monitor.",
                                    "type": "string",
                                    "enum": [
                                        "ssh-ed25519",
                                        "ecdsa-sha2-nistp256",
                                        "ecdsa-sha2-nistp384",
                                        "ecdsa-sha2-nistp521",
                                        "rsa-sha2-512",
                                        "rsa-sha2-256",
                                        "ssh-rsa"
                                    ],
                                    "editor": "select"
                                },
                                "expectedFingerprints": {
                                    "title": "Expected SHA256 fingerprints",
                                    "type": "array",
                                    "description": "Optional trusted SHA256 fingerprints, including the SHA256: prefix. Multiple values support planned overlap rotations.",
                                    "maxItems": 20,
                                    "uniqueItems": true,
                                    "editor": "stringList",
                                    "items": {
                                        "type": "string",
                                        "pattern": "^SHA256:[A-Za-z0-9+/]{43}$"
                                    }
                                },
                                "label": {
                                    "title": "Label",
                                    "description": "Optional human-readable integration name.",
                                    "type": "string",
                                    "maxLength": 200,
                                    "editor": "textfield"
                                }
                            },
                            "required": [
                                "host"
                            ]
                        },
                        "default": [
                            {
                                "host": "ssh.github.com",
                                "port": 443,
                                "hostKeyAlgorithm": "ssh-ed25519",
                                "label": "GitHub SSH over 443"
                            }
                        ]
                    },
                    "timeoutSeconds": {
                        "title": "Per-target timeout (seconds)",
                        "minimum": 5,
                        "maximum": 30,
                        "type": "integer",
                        "description": "Total bounded TCP and SSH handshake timeout per endpoint.",
                        "default": 15
                    },
                    "maxConcurrency": {
                        "title": "Maximum concurrent checks",
                        "minimum": 1,
                        "maximum": 5,
                        "type": "integer",
                        "description": "Number of authorized endpoints checked concurrently.",
                        "default": 3
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
