# Security Headers & SSL Certificate Checker API (`martin10291/security-headers-ssl-check`) Actor

No API key, no login. One call per host list returns a security headers grade A+ to F (HSTS, CSP, frame protection, nosniff, Referrer-Policy, Permissions-Policy, COOP/CORP/COEP, cookie flags, information leaks) with an exact fix line per issue, and the SSL certificate: issuer, SANs, days left, hostn

- **URL**: https://apify.com/martin10291/security-headers-ssl-check.md
- **Developed by:** [Martin Petrov](https://apify.com/martin10291) (community)
- **Categories:** Developer tools, Automation, Business
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 results

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Security Headers & SSL Certificate Checker API

**Problem:** you run an agency, a monitoring tool, a compliance check or a pentest pre-scan and need to know in seconds whether a company website sends the right security headers and whether its SSL certificate is healthy. The manual way is `curl -I`, `openssl s_client` and a checklist.

**This API:** up to 50 hosts per call. One row per host with a grade from A+ to F, every security header with a verdict and the exact line to add, and the TLS certificate with days left.

- **Headers:** Strict-Transport-Security (max-age, includeSubDomains, preload), Content-Security-Policy (present, `unsafe-inline`, `unsafe-eval`, wildcard sources, report-only only), X-Frame-Options or CSP `frame-ancestors`, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener / Resource / Embedder-Policy.
- **Cookies:** how many `Set-Cookie` headers have Secure, HttpOnly and SameSite. Names and values are never returned.
- **Information leaks:** Server and X-Powered-By headers that disclose a version. Deprecated headers (X-XSS-Protection, Expect-CT) are listed as notes.
- **Redirect:** does plain `http://` redirect to `https://`, and the final URL after redirects.
- **Certificate:** subject, SAN count and first 20 SANs, issuer, notBefore / notAfter, `daysLeft`, `expired`, `expiringSoon`, hostname match, self-signed / untrusted verdict with the verify error text, signature algorithm, key type and size, SHA-256 fingerprint.
- **TLS:** negotiated version and cipher, and which of TLS 1.0 / 1.1 / 1.2 / 1.3 the server accepts (`accepted`, `rejected`, or `not_testable` when the local OpenSSL cannot try that version).
- **Issues:** sorted by severity, each with an exact `fix` line.

### Why it is different

- **"Could not check" is never "header missing".** A host that times out, has no DNS entry or refuses the connection returns a row with an `error` code (`dns_error`, `timeout`, `blocked_private_address`, `tls_error: ...`) and no grade.
- **Bad certificates are still read.** Expired, self-signed and wrong-host certificates are reported with the verify error text, and the headers are graded as well.
- **Fix lines, not just findings.** Every missing or weak header comes with the header line to add.
- **Deterministic grade.** The scoring table below is the full rule set. The same input gives the same grade.
- **Monitor mode:** `onlyNew: true` + `monitorId` returns only hosts whose grade dropped, whose header disappeared, whose certificate was replaced or that entered the certificate warning window.

### Example (API)

```bash
curl -X POST "https://api.apify.com/v2/acts/martin10291~security-headers-ssl-check/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hosts": ["github.com", "example.com"], "certWarnDays": 30}'
```

Output row (trimmed, measured on 2026-11-12):

```json
{"host": "github.com", "url": "https://github.com/", "status": "ok", "finalUrl": "https://github.com/", "httpStatus": 200,
 "httpToHttps": {"checked": true, "redirectsToHttps": true, "status": 301},
 "grade": "B", "score": 87, "issueCount": 5,
 "headers": {
   "strictTransportSecurity": {"present": true, "value": "max-age=31536000; includeSubdomains; preload", "maxAge": 31536000, "includeSubDomains": true, "preload": true, "verdict": "good"},
   "contentSecurityPolicy": {"present": true, "reportOnly": false, "unsafeInline": false, "unsafeEval": false, "wildcardSource": false, "verdict": "good"},
   "xFrameOptions": {"present": true, "value": "deny", "verdict": "good"},
   "permissionsPolicy": {"present": false, "verdict": "missing", "fix": "Permissions-Policy: camera=(), microphone=(), geolocation=()"}
 },
 "cookies": {"count": 3, "secure": 3, "httpOnly": 2, "sameSite": 3, "sameSiteNoneWithoutSecure": 0},
 "certificate": {"subjectCN": "github.com", "issuerCN": "Sectigo Public Server Authentication CA DV E36", "sanCount": 2,
                 "notBefore": "2026-10-30T00:00:00Z", "notAfter": "2027-01-27T23:59:59Z", "daysLeft": 76, "expired": false,
                 "expiringSoon": false, "hostnameMatch": true, "trusted": true, "selfSigned": false,
                 "signatureAlgorithm": "ecdsa-with-SHA256", "keyType": "EC P-256", "keyBits": 256},
 "tls": {"negotiated": "TLSv1.3", "supported": {"TLSv1.0": "rejected", "TLSv1.1": "rejected", "TLSv1.2": "accepted", "TLSv1.3": "accepted"}},
 "issues": [{"severity": "low", "area": "permissions-policy", "issue": "Permissions-Policy header is missing.",
             "fix": "Permissions-Policy: camera=(), microphone=(), geolocation=()", "points": 4}],
 "checkedAt": "2026-11-12T14:45:07Z"}
```

### Scoring (grade A+ to F)

The score starts at 100 and loses the points below, once per finding. Info items cost 0 points.

| Finding | Points |
|---|---|
| Site served over plain HTTP at the final URL | 30 |
| Strict-Transport-Security missing (HTTPS sites) | 15 |
| HSTS max-age under 15552000 s (180 days) | 5 |
| Content-Security-Policy missing, or only the Report-Only header | 20 / 15 |
| CSP without script-src and default-src | 6 |
| CSP `'unsafe-inline'` in script-src (ignored when a nonce, hash or `'strict-dynamic'` is present) | 8 |
| CSP `'unsafe-eval'` in script-src | 5 |
| CSP wildcard or scheme-only source (`*`, `http:`, `https:`) in script-src | 6 |
| No clickjacking protection (no X-Frame-Options DENY/SAMEORIGIN and no CSP frame-ancestors) | 10 |
| X-Frame-Options with another value and no frame-ancestors | 5 |
| X-Content-Type-Options not `nosniff` | 8 |
| Referrer-Policy missing | 6 |
| Referrer-Policy `unsafe-url` or `no-referrer-when-downgrade` | 3 |
| Permissions-Policy missing | 4 |
| Cross-Origin-Opener-Policy / Resource-Policy / Embedder-Policy missing | 2 each |
| Cookies without Secure (HTTPS sites) | 6 |
| Cookies without HttpOnly | 3 |
| Cookies without SameSite | 2 |
| Cookies with SameSite=None but no Secure | 3 |
| Server header with a version, X-Powered-By, X-AspNet(Mvc)-Version | 3 each |
| Plain `http://` does not redirect to HTTPS (only when port 80 answers) | 8 |
| Certificate expired | 50 |
| Certificate not valid yet | 40 |
| Certificate expires inside the warning window (`certWarnDays`) | 10 |
| Certificate does not cover the hostname | 40 |
| Certificate chain not trusted or self-signed | 40 |
| RSA key under 2048 bits | 15 |
| Signature algorithm MD5 or SHA-1 | 15 |
| Server accepts TLS 1.0 or 1.1 | 10 |
| Server does not accept TLS 1.3 | 3 |
| X-XSS-Protection (other than 0), Expect-CT, HSTS without includeSubDomains | 0 (note) |

Grade: **A+** = 100, **A** = 90 to 99, **B** = 80 to 89, **C** = 65 to 79, **D** = 50 to 64, **F** = below 50. The grade is **F** whatever the score when the certificate is expired, does not cover the hostname, is not trusted, or the site is only served over plain HTTP. Severity of an issue: `high` from 15 points, `medium` from 6, `low` from 1, `info` for 0. Issues are sorted by severity, then points.

A check that could not run (timeout, TLS handshake failure) adds no points: if the headers cannot be fetched there is no grade, if the certificate cannot be read there are no certificate points.

### Schedule it / monitor your sites

Run it daily or weekly with an Apify schedule and set `onlyNew: true` with a `monitorId` such as `my-customers` and `certWarnDays` such as 21. The first run returns every host (`change: first_check`). Later runs return only hosts where the grade dropped (`grade_dropped:A->B`), a security header disappeared (`header_removed:strictTransportSecurity`), the certificate was replaced (`certificate_changed`) or the certificate entered the warning window (`certificate_expiry_warning`); the reasons are in `changes`. Connect the dataset to a webhook, Slack, Zapier or Make through an Apify integration to get an alert. A run with no change returns no rows and no charge. Hosts that could not be reached in that run (timeout, refused connection) are skipped and tried again next run, they never overwrite the stored state.

### Use from agents (MCP)

Add the Apify MCP server (`https://mcp.apify.com?tools=martin10291/security-headers-ssl-check`) to Claude, Cursor or any MCP client and ask: "Check the security headers and certificate of these sites and list the fixes: ...".

### Input

| Field | What it does |
|---|---|
| `hosts` | Domains, `host:port` or URLs, up to 50 (ports 80, 443, 8080, 8443) |
| `host` / `url` | Shortcuts for a single site |
| `certWarnDays` | Days before expiry that count as "expiring soon" (default 30) |
| `onlyNew` / `monitorId` | Return only hosts that changed since the last run with this Monitor ID |

### What it does on the network

Per host: one DNS lookup, one GET of the front page you give (plus redirects, at most 5), one plain-HTTP request to see the redirect, one TLS handshake for the certificate (two when the first fails verification, to still read the certificate) and one handshake per TLS version. No port scanning, no path guessing, no exploitation, no logins. The User-Agent is `RadarSecurityCheck/1.0`. Private, loopback, link-local and reserved addresses are refused and re-checked on every redirect.

### Honest limits

- **Front page only.** Headers can differ per path (an API or an admin area may send other headers).
- **Headers are read from the final response after redirects** (up to 5). Sites that block automated clients can answer with a bot-protection page whose headers differ from the real site.
- **The certificate is the one served for the hostname on port 443** (or on the port you give), with SNI. Only the leaf certificate is reported; the standard library does not return the chain, so chain order and intermediates are not checked beyond the trust verdict.
- **Trust verdict** uses the certificate store of the machine running the check. The verify error text is returned.
- **TLS versions:** each is one handshake attempt. `not_testable` means the local OpenSSL refuses that version; `unknown` means the attempt timed out.
- **A grade is a header and certificate hygiene score**, not a penetration test or a guarantee that a site is safe.
- **Checked from one location.** CDNs can answer differently by region.

### FAQ

**Pricing?** $1.00 / 1000 results (one row per host, or per changed host in monitor mode).
**Do I need a key?** No. Only the website you name is contacted.
**Does it return cookie values?** Never. Only counts of the Secure, HttpOnly and SameSite flags.
**Why is there no grade for a host?** The page could not be fetched (see `headersError`): that is reported as an error, never as missing headers.
**How fresh is the data?** Live: every call performs the requests again.

# Actor input Schema

## `hosts` (type: `array`):

Websites to check, up to 50 per run. Bare domains (example.com), hosts with a port (example.com:8443) or full URLs (https://example.com/) are accepted. Only the front page you give is requested; private, local and our own addresses are refused and come back as a row with error blocked_private_address.

## `host` (type: `string`):

Optional shortcut for one website, same as a one-entry Hosts list. Handy for API calls with a single value.

## `url` (type: `string`):

Optional shortcut for one URL, for example https://example.com/. Same as a one-entry Hosts list.

## `certWarnDays` (type: `integer`):

A certificate that expires in fewer days than this is flagged as expiring soon (expiringSoon true, an issue is added, and monitor mode reports it). Between 1 and 365.

## `onlyNew` (type: `boolean`):

Return only hosts whose grade dropped, whose security header disappeared, whose certificate was replaced or that entered the certificate warning window since the last run with this Monitor ID (the first run returns every host). Use it on a schedule to watch your own or your customers' sites.

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

Name of the watch list for the changes-only mode, for example my-sites. Each Monitor ID keeps its own memory.

## Actor input object example

```json
{
  "hosts": [
    "github.com",
    "example.com",
    "google.com"
  ],
  "certWarnDays": 30,
  "onlyNew": false,
  "monitorId": "default"
}
```

# Actor output Schema

## `results` (type: `string`):

One row per host: grade A+ to F, score, issues with a fix line each, every security header with a verdict, cookie flag counts, the SSL certificate (issuer, SANs, validity, days left, hostname match, trust) and the TLS versions accepted. In monitor mode only hosts that changed.

# 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 = {
    "hosts": [
        "github.com",
        "example.com",
        "google.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("martin10291/security-headers-ssl-check").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 = { "hosts": [
        "github.com",
        "example.com",
        "google.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("martin10291/security-headers-ssl-check").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 '{
  "hosts": [
    "github.com",
    "example.com",
    "google.com"
  ]
}' |
apify call martin10291/security-headers-ssl-check --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,martin10291/security-headers-ssl-check"
        }
    }
}
```

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/vNZGZ6kECTuACpcFM/builds/84xMiNkYKInCneMOt/openapi.json
