# Server Hardening Audit - sshd, nginx, Docker, Kubernetes (`kasimba/server-hardening-audit`) Actor

Audit server and container configs against hardening baselines. 9 formats, 74 checks. Catches insecure implicit defaults other linters miss. No network access - analyses only the text you supply.

- **URL**: https://apify.com/kasimba/server-hardening-audit.md
- **Developed by:** [John J. K. Omolo](https://apify.com/kasimba) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 90.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $28.00 / 1,000 config auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## Server Hardening Audit — sshd, nginx, Docker, ufw

Paste a config file. Get back every hardening failure, ranked by severity, with the exact line to change.

Built for the moment an AI coding agent has just written an `sshd_config`, a `Dockerfile` or a `docker-compose.yml` and nobody has checked whether it is safe to deploy. One call in, a graded finding list out.

**No network access. No scanning.** This Actor reads only the text you pass it. It never connects to a host, never probes a port and never touches a machine you do not control. That is a deliberate design choice, not a missing feature.

### Why this is not another linter

Most config checkers grade what is written in the file. That means an empty `sshd_config` scores 100% — while the daemon it describes is quietly accepting password logins from the entire internet.

This Actor evaluates **implicit defaults**. When a directive is absent, it is checked against the value the daemon actually uses, and the finding is tagged `source: implicit-default` so you can tell the two apart.

Three examples of what that catches:

| Finding | Why other tools miss it |
|---|---|
| `ClientAliveInterval` absent | Defaults to `0` — the session never times out |
| `PermitRootLogin` absent | Defaults to `prohibit-password` — key-based root login is still open |
| `add_header` inside a `location` block | nginx does **not** merge headers across scopes. One `add_header` in a nested block silently discards every security header set at `server` or `http` level. Config looks right, headers ship missing. |

### Supported formats

Auto-detected from the content — you do not need to say which is which.

- **`sshd_config`** — 20 checks. Correctly models sshd's first-value-wins precedence and flags `Match` blocks that re-enable password auth or root login.
- **`nginx.conf`** — 10 checks. TLS versions, cipher suites (understands OpenSSL `!` exclusion syntax), security headers, header-inheritance footgun.
- **`Dockerfile`** — 7 checks. Root execution, floating tags, secrets baked into layers, `curl | sh`, remote `ADD`.
- **`docker-compose.yml`** — 9 checks. `privileged`, Docker socket mounts, host namespaces, capabilities, datastore ports published to `0.0.0.0`.
- **`ufw status verbose`** — 4 checks. Default policy, exposed datastore ports, unlimited SSH.

### Input

Single file:

```json
{
  "configText": "PermitRootLogin yes\nPasswordAuthentication yes\n"
}
```

Batch — one run, one call, billed per config:

```json
{
  "configs": [
    { "name": "web-01 sshd",   "content": "..." },
    { "name": "web-01 nginx",  "content": "..." },
    { "name": "api compose",   "content": "..." }
  ],
  "minSeverity": "medium"
}
```

### Output

One dataset item per config:

```json
{
  "name": "web-01 sshd",
  "config_type": "sshd",
  "score": 43,
  "grade": "D",
  "counts": { "critical": 1, "high": 2, "medium": 3, "low": 1, "info": 0 },
  "findings": [
    {
      "rule_id": "SSH-001",
      "title": "Root login over SSH",
      "severity": "critical",
      "status": "fail",
      "source": "explicit",
      "line": 2,
      "observed": "yes",
      "rationale": "Direct root login removes per-user accountability...",
      "fix": "PermitRootLogin no",
      "ref": "CIS Distribution Independent Linux 5.2"
    }
  ]
}
```

A `SUMMARY` record in the key-value store aggregates the run: average score, total counts, and the top ten critical and high findings across every config as `priority_actions`.

**Scoring:** starts at 100, minus 25 per critical, 12 per high, 5 per medium, 2 per low. Info findings cost nothing. A → 90+, B → 75+, C → 60+, D → 40+, F below.

### Redaction

Detected credentials are never echoed back. A hardcoded password is reported as `POSTGRES_PASSWORD=<redacted, 18 chars>` — enough to locate it, not enough to leak it into your logs, your dataset or your agent's context window.

### Calling it from an agent

Available through the Apify MCP server, so Claude, Cursor and any MCP-speaking client can call it as a tool. Ask it to audit a config and it will route here.

### Pricing

Billed per config audited, not per run. Batch as many files as you like into a single call — the batch costs the same as running them separately, but finishes in one round trip.

### Limits and honest caveats

- 512 KB per config.
- `nginx` parsing handles `include` directives as opaque — files you do not paste are not analysed.
- This grades configuration. It says nothing about unpatched packages, running processes or kernel version.
- A clean score means the config passes these checks. It is not a certification, and it is not a substitute for a penetration test.
- `SSH-017` reports a non-default SSH port as informational only. Moving the port reduces log noise from untargeted scanners; it is not a security control and is scored as zero.

### Local use

```bash
pip install -r requirements.txt
python -m src.main path/to/sshd_config path/to/docker-compose.yml
```

Prints the same JSON structure to stdout, no Apify account needed.

# Actor input Schema

## `configText` (type: `string`):

Paste a single config file. The format is auto-detected. Nothing is fetched over the network — only the text you supply is analysed.

## `configType` (type: `string`):

Leave empty to auto-detect. Set this only if detection picks the wrong format.

## `configName` (type: `string`):

Appears in the output so you can tell results apart.

## `configs` (type: `array`):

Audit several files in one run: \[{"name":"web-01 sshd","content":"..."}]. Each config is billed as one audit. Cheaper than one run per file.

## `minSeverity` (type: `string`):

Only report findings at or above this severity.

## Actor input object example

```json
{
  "configText": "PermitRootLogin yes\nPasswordAuthentication yes\nX11Forwarding yes\n",
  "configType": "",
  "minSeverity": "info"
}
```

# 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 = {
    "configText": `PermitRootLogin yes
PasswordAuthentication yes
X11Forwarding yes`
};

// Run the Actor and wait for it to finish
const run = await client.actor("kasimba/server-hardening-audit").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 = { "configText": """PermitRootLogin yes
PasswordAuthentication yes
X11Forwarding yes
""" }

# Run the Actor and wait for it to finish
run = client.actor("kasimba/server-hardening-audit").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "configText": "PermitRootLogin yes\\nPasswordAuthentication yes\\nX11Forwarding yes\\n"
}' |
apify call kasimba/server-hardening-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kasimba/server-hardening-audit"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/0Sz6zS5dAC5oo75Ly/builds/R9rbNgeUygmYlMJfV/openapi.json
