Why we built Skald
We built Skald for one job: to know who really visits our websites. People, AI assistants, bots.
Skald reads the server's own log, because Google Analytics misses most of them. It only sees visitors who run its script.
The visitor
On 21 October 2026 I was testing Skald's new live view, “What visitors are doing now”, on spaceo.ai. A visitor from Sheung Wan, Hong Kong, was reading our home page.
Chrome browser. It arrived “from” wp.spaceo.ai, our own WordPress site. It even loaded the page's own scripts, like a real browser does.
I thought: a real person.
The twist
Then we opened the server log.
In 14 seconds, from 20:00:46 to 20:01:00 UTC, that “visitor” made 150 requests. It was hunting for our AI keys: Amazon Bedrock configs (27 paths), LiteLLM (7), OpenAI proxies (5), Anthropic and Claude settings (3), ai/credentials and llm/credentials.
Then .env password files (17), Terraform files (9) and AWS deployment files such as appspec.yml and cdk.out.
Not a visitor. A vulnerability scanner in a browser costume, built for the AI era.
Every line said Chrome on Windows. Every line claimed to come from our own site: wp.spaceo.ai on 124 of them, www.spaceo.ai on the 26 that followed a redirect. Here are twelve, as the server wrote them.
- address who asked (replaced)
- [time] when, UTC
- "GET /path" what it asked for
- 404 what our server answered
- "https://wp.spaceo.ai/…" the page it claimed to come from
- "… Chrome …" the browser it claimed to be
- AI keys
- Cloud and Terraform files
- .env files
- The page and its scripts
- Other credentials
203.0.113.7 - - [21/Oct/2026:20:00:46 +0000] "GET /api/bedrock HTTP/2.0" 308 13 "https://wp.spaceo.ai/api/bedrock" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"1Claims it came from our own WordPress site, wp.spaceo.ai, from a page of the same name. Part of the costume.203.0.113.7 - - [21/Oct/2026:20:00:47 +0000] "GET / HTTP/2.0" 200 79878 "https://wp.spaceo.ai/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"2Loads our home page (200), like a browser would.203.0.113.7 - - [21/Oct/2026:20:00:49 +0000] "GET /bedrock/config.json HTTP/2.0" 404 22201 "https://wp.spaceo.ai/bedrock/config.json" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"203.0.113.7 - - [21/Oct/2026:20:00:49 +0000] "GET /litellm/config.yaml HTTP/2.0" 404 22201 "https://wp.spaceo.ai/litellm/config.yaml" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"203.0.113.7 - - [21/Oct/2026:20:00:49 +0000] "GET /anthropic/config.json HTTP/2.0" 404 22202 "https://wp.spaceo.ai/anthropic/config.json" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"203.0.113.7 - - [21/Oct/2026:20:00:49 +0000] "GET /claude/config.json HTTP/2.0" 404 22200 "https://wp.spaceo.ai/claude/config.json" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"203.0.113.7 - - [21/Oct/2026:20:00:50 +0000] "GET /.env HTTP/2.0" 404 12351 "https://wp.spaceo.ai/.env" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"203.0.113.7 - - [21/Oct/2026:20:00:52 +0000] "GET /_next/static/chunks/fd9d1056-210eea99f6122c96.js HTTP/2.0" 200 53773 "https://wp.spaceo.ai/_next/static/chunks/fd9d1056-210eea99f6122c96.js" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"3Even fetches the page's own script, as a real browser does.203.0.113.7 - - [21/Oct/2026:20:00:57 +0000] "GET /.aws/credentials.json HTTP/2.0" 404 22201 "https://wp.spaceo.ai/.aws/credentials.json" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"203.0.113.7 - - [21/Oct/2026:20:00:59 +0000] "GET /.env.production.local HTTP/2.0" 404 12374 "https://wp.spaceo.ai/.env.production.local" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"203.0.113.7 - - [21/Oct/2026:20:00:59 +0000] "GET /terraform.tfvars.json HTTP/2.0" 499 0 "https://wp.spaceo.ai/terraform.tfvars.json" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"203.0.113.7 - - [21/Oct/2026:20:01:00 +0000] "GET /appspec.yml HTTP/2.0" 499 0 "https://wp.spaceo.ai/appspec.yml" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/141.0.0.0 …"
Magnified: line 1 of 12
20:00:46 GET /api/bedrock 308
Asked for an Amazon Bedrock path, where AWS's AI keys live; got a redirect (308).
Claimed to come from https://wp.spaceo.ai/api/bedrock
Source the access log of spaceo.ai, 21 Oct 2026, 20:00:46 to 20:01:00 UTC. The scanner's real address is replaced with 203.0.113.7, an address kept for documentation; the browser name is cut where you see “…”. Nothing else is changed.
- AI keys 83
- Cloud and Terraform files 32
- .env files 17
- The page and its scripts 16
- Other credentials 2
- :46
- :47
- :48
- :49
- :50
- :51
- :52
- :53
- :54
- :55
- :56
- :57
- :58
- :59
- :00
- 1 Starts on /api/bedrock, then loads our home page (200), like a browser
- 2 Peak: 52 requests in one second, for Bedrock, LiteLLM, OpenAI, Anthropic and AI credentials
- 3 No more AI paths. 57 requests: .env files; cloud and Terraform files; the page's scripts
- 4 Done: 150 requests in 14 seconds
20:00:49 UTC: 52 requests, as the log has them
- 404
/bedrock/config.json - 404
/litellm/config.yaml - 404
/openai-proxy/config.yaml - 404
/openai-proxy/config.yml - 404
/llm/config.yml - 404
/proxy/config.yml - 404
/llm/config.yaml - 404
/bedrock/config.yml - 404
/proxy/config.yaml - 404
/ai/credentials.json - 404
/bedrock/config.yaml - 404
/bedrock/config.toml - 404
/openai-proxy/config.json - 404
/ai/config.yaml - 404
/anthropic/config.json - 404
/ai/config.json - 404
/api/ai/config/ - 404
/ai/config.yml - 404
/api/bedrock/ - 404
/config/bedrock.yaml - 404
/config/ai.yaml - 404
/config/litellm.yaml - 404
/claude/config.json - 404
/ai/credentials/ - 404
/api/v1/bedrock/ - 404
/model/invoke/ - 404
/gpt/config.json - 404
/config/llm.yaml - 404
/litellm/config.yml - 404
/bedrock/config.js - 404
/api/bedrock-runtime/ - 404
/config/ai.json - 404
/config/bedrock.toml - 404
/aws/bedrock/ - 404
/api/v1/aws/bedrock/ - 404
/config/bedrock.json - 404
/api/aws/bedrock/ - 404
/llm/credentials/ - 404
/api/v1/bedrock-runtime/ - 308
/api/config/ai - 308
/invoke - 404
/litellm_config.yaml - 404
/litellm.yml - 404
/litellm.yaml - 499
/openai-proxy.yaml - 499
/litellm_config.yml - 308
/api/invoke - 404
/.env.local - 308
/v1/model/invoke - 308
/api/model/invoke - 308
/inference-profiles - 308
/api/v1/invoke
- 308
/api/bedrock - 308
/api/ai/config
- 308
/api/v1/bedrock - 308
/ai/credentials - 308
/foundation-models - 308
/llm/credentials - 308
/model/invoke - 200
/ - 308
/bedrock-runtime - 308
/api/bedrock-runtime - 308
/aws/bedrock - 308
/api/aws/bedrock - 308
/api/v1/aws/bedrock
- 308
/bedrockruntime - 308
/api/v1/bedrock-runtime
- 404
/bedrock/config.json - 404
/litellm/config.yaml - 404
/openai-proxy/config.yaml - 404
/openai-proxy/config.yml - 404
/llm/config.yml - 404
/proxy/config.yml - 404
/llm/config.yaml - 404
/bedrock/config.yml - 404
/proxy/config.yaml - 404
/ai/credentials.json - 404
/bedrock/config.yaml - 404
/bedrock/config.toml - 404
/openai-proxy/config.json - 404
/ai/config.yaml - 404
/anthropic/config.json - 404
/ai/config.json - 404
/api/ai/config/ - 404
/ai/config.yml - 404
/api/bedrock/ - 404
/config/bedrock.yaml - 404
/config/ai.yaml - 404
/config/litellm.yaml - 404
/claude/config.json - 404
/ai/credentials/ - 404
/api/v1/bedrock/ - 404
/model/invoke/ - 404
/gpt/config.json - 404
/config/llm.yaml - 404
/litellm/config.yml - 404
/bedrock/config.js - 404
/api/bedrock-runtime/ - 404
/config/ai.json - 404
/config/bedrock.toml - 404
/aws/bedrock/ - 404
/api/v1/aws/bedrock/ - 404
/config/bedrock.json - 404
/api/aws/bedrock/ - 404
/llm/credentials/ - 404
/api/v1/bedrock-runtime/ - 308
/api/config/ai - 308
/invoke - 404
/litellm_config.yaml - 404
/litellm.yml - 404
/litellm.yaml - 499
/openai-proxy.yaml - 499
/litellm_config.yml - 308
/api/invoke - 404
/.env.local - 308
/v1/model/invoke - 308
/api/model/invoke - 308
/inference-profiles - 308
/api/v1/invoke
- 404
/api/config/ai/ - 404
/api/invoke/ - 308
/api/inference-profiles - 308
/api/v1/foundation-models - 308
/api/foundation-models - 404
/api/v1/invoke/ - 404
/v1/model/invoke/ - 404
/api/model/invoke/ - 404
/api/inference-profiles/ - 499
/foundation-models/ - 404
/api/v1/foundation-models/ - 404
/api/foundation-models/ - 499
/bedrockruntime/ - 499
/openai-proxy.yml - 499
/bedrock-runtime/ - 404
/.env
- 308
/credentials
- 200
/_next/static/chunks/2117-61721170b6f773fa.js - 404
/_next/static/chunks/2117-61721170b6f773fa.js.map - 404
/_next/static/chunks/fd9d1056-210eea99f6122c96.js.map - 200
/_next/static/chunks/fd9d1056-210eea99f6122c96.js - 404
/.env~ - 404
/credentials/ - 499
/invoke/ - 404
/foundation-models.json - 404
/inference-profiles/
- 499
/.env.bedrock - 404
/serverless.yaml - 404
/serverless.yml - 404
/elasticbeanstalk/config.yml - 404
/cloudformation.yaml - 200
/_next/static/chunks/main-app-b8bf49213dcc25c9.js - 404
/.env.example
- 404
/terraform.tfstate.backup - 404
/_next/static/chunks/main-app-b8bf49213dcc25c9.js.map - 404
/.env.claude - 200
/_next/static/chunks/7498-cc41dbdeca7696ef.js
- 404
/cloudformation.yml - 404
/.env.aws - 404
/template.yml - 200
/_next/static/chunks/7648-9a6167c257978fe6.js - 200
/_next/static/chunks/2223-445cb6fb33643d80.js - 404
/_next/static/chunks/7498-cc41dbdeca7696ef.js.map - 404
/_next/static/chunks/7648-9a6167c257978fe6.js.map - 404
/template.yaml
- 308
/cdk.out/
- 200
/_next/static/chunks/3942-c6864f9b9a5aae2c.js - 200
/_next/static/chunks/app/layout-1a647b802baae2fa.js - 404
/.aws/credentials.json - 404
/deploy/terraform.tfstate - 404
/infra/terraform.tfstate - 404
/.aws/credentials.bak - 404
/.aws/config.json - 404
/terraform/terraform.tfstate - 404
/infrastructure/terraform.tfstate - 404
/.aws/config.bak - 404
/_next/static/chunks/2223-445cb6fb33643d80.js.map - 404
/_next/static/chunks/3942-c6864f9b9a5aae2c.js.map - 404
/.env.aws.local
- 404
/cloudformation/ - 404
/samconfig.yaml - 404
/buildspec.yml - 404
/cdk.json
- 404
/samconfig.toml - 499
/cloudformation.json - 404
/.env.production - 404
/.env.prod - 499
/.ebextensions/ - 499
/codedeploy/ - 499
/amplify.yml - 404
/.env.staging - 404
/.env.production.local - 499
/cdk.out - 404
/.env.development - 499
/outputs.tf - 499
/variables.tf - 499
/terraform.tfvars.json
- 499
/appspec.yml - 499
/main.tf - 404
/.env.dev - 404
/.env.bak - 404
/.env.development.local - 499
/.env.prod.bak
The numbers, second by second
| Second (UTC) | AI keys | Cloud and Terraform files | .env files | The page and its scripts | Other credentials | All |
|---|---|---|---|---|---|---|
| 20:00:46 | 2 | 0 | 0 | 0 | 0 | 2 |
| 20:00:47 | 10 | 0 | 0 | 1 | 0 | 11 |
| 20:00:48 | 2 | 0 | 0 | 0 | 0 | 2 |
| 20:00:49 | 51 | 0 | 1 | 0 | 0 | 52 |
| 20:00:50 | 15 | 0 | 1 | 0 | 0 | 16 |
| 20:00:51 | 0 | 0 | 0 | 0 | 1 | 1 |
| 20:00:52 | 3 | 0 | 1 | 4 | 1 | 9 |
| 20:00:53 | 0 | 4 | 2 | 1 | 0 | 7 |
| 20:00:54 | 0 | 1 | 1 | 2 | 0 | 4 |
| 20:00:55 | 0 | 3 | 1 | 4 | 0 | 8 |
| 20:00:56 | 0 | 1 | 0 | 0 | 0 | 1 |
| 20:00:57 | 0 | 8 | 1 | 4 | 0 | 13 |
| 20:00:58 | 0 | 4 | 0 | 0 | 0 | 4 |
| 20:00:59 | 0 | 9 | 5 | 0 | 0 | 14 |
| 20:01:00 | 0 | 2 | 4 | 0 | 0 | 6 |
Source all 150 lines of the log, counted when this page is built. Each request has one colour: the page and its scripts first, then .env, cloud and Terraform files, then AI keys.
What it wanted: the keys
Most of its requests, 83 of 150, went for the keys to AI services: Amazon Bedrock, LiteLLM, OpenAI, Anthropic. The rest went for .env files, cloud files and the page itself.
- Amazon BedrockAWS's AI models27 paths
/bedrock/config.jsonAnswered: 404 ×15 · 308 ×9 · 499 ×3404 · nothing leaked - AWS deployment files and keysappspec.yml, cdk.out, CloudFormation, ~/.aws23 paths
/serverless.yamlAnswered: 404 ×16 · 499 ×6 · 308 ×1404 · nothing leaked - .env fileswhere apps keep passwords and keys17 paths
/.env.localAnswered: 404 ×15 · 499 ×2404 · nothing leaked - Terraformcloud state and variables9 paths
/terraform.tfstate.backupAnswered: 404 ×5 · 499 ×4404 · nothing leaked - LiteLLMa gateway that holds AI keys7 paths
/litellm/config.yamlAnswered: 404 ×6 · 499 ×1404 · nothing leaked - OpenAI proxiesconfigs that hold an OpenAI key5 paths
/openai-proxy/config.yamlAnswered: 404 ×3 · 499 ×2404 · nothing leaked - AI and LLM credentials/ai/credentials, /llm/credentials5 paths
/ai/credentials.jsonAnswered: 404 ×3 · 308 ×2404 · nothing leaked - Anthropic and Claudesettings with a Claude key3 paths
/anthropic/config.jsonAnswered: 404 ×3404 · nothing leaked
Source every path in the log, matched to the family it names. A path can name two: /.env.bedrock is Bedrock and .env, /.env.claude Claude and .env. The rest of the 150: 16 for the page itself, its scripts and their source maps, and 2 for a bare /credentials. Product names and logos belong to their owners; shown to name what the scanner looked for. Brand marks from Simple Icons (CC0).
The relief, with proof
Every secret-file request got “not found” (96), a redirect (26) or a dropped connection (19).
The only things it received (9 answers) were our public home page and its public scripts. Nothing leaked, and the log proves it.
- 96× not found (404)The file is not there.
- 26× redirected (308)The scanner followed each one and got “not found” or nothing.
- 19× dropped (499)The scanner hung up before an answer.
- 9× answered (200)Our public home page (1) and its own public scripts (8). Nothing secret.
Source the answer code on each line of the log: 404 not found, 308 a redirect, 499 the client hung up, 200 sent.
The scale nobody sees
This was one scanner. On one of our sites, Skald's audited figures show 1,085 to 4,148 scanner probes a day (17 to 20 Oct 2026), from 100 to 150 addresses. The php2shell guide has them day by day.
Google Analytics shows none of it: scanners never run its script.
- 1,085–4,148scanner probes a day
- 100–150addresses a day
- Noneof it in Google Analytics
Source Skald's audited daily counts, 17 to 20 Oct 2026, one of our sites; every day counted twice and matched. Day by day in the php2shell guide.
Two birds, one stone
The log Skald reads to count real visitors is the same log that records every scanner. The tool we built for marketing turned out to be a security tool too.
One install, one log, two answers: who is really visiting, and who is trying to break in.
An illustration: a developer at a laptop throws one stone, the Skald mark. In one arc it pops two different birds: a "Fake visitor", a bot in glasses and a false nose, and a "Scanner", a red bird with a crowbar. They turn into two badges, "Real visitors counted" and "Attackers caught", under the words "One log. Two answers."
Fake visitors inflate your visitor counts; scanners hunt your secret files. Both are lines in the same server log.
One line of the log203.0.113.7 … "GET /anthropic/config.json HTTP/2.0" 404 … "https://wp.spaceo.ai/anthropic/config.json" "… Chrome/141.0.0.0 …"
Who is really visiting
- People, AI assistants, search engines and bots, each counted apart
- Where each visit came from, AI assistants included
- Which pages they read, and the leads they became
Who is trying to break in
- Scanners asking for secret files, like this one
- What your server answered, and whether anything was exposed
- Attack attempts, fake browsers and a block list
What you get
Skald's Security section (2.21 and later) turns the same log into seven reports. Each opens in the live demo, with sample data.
What Skald's Security section shows you
Is anything exposed?
Every probe that got an answer, set against the server's own harmless page. A different size is a possible exposure to open and check now.
- 1The verdict first: 1 possible exposure, check it now.
- 22,145 probes; 1,072 of the answers were the harmless page.
- 3The one that differs: /.env.production answered 200, 1,873 bytes, with what to do.
Who is scanning us?
Each scanner: where it came from, what it looked for, and what your server answered.
- 1Probes per day, for the last 30 days.
- 2What they looked for: secret files (.env, keys) lead.
- 3One row per scanner: where from, what it asked for, what it got.
Attack attempts
Login hammering, request bursts and injection in the address, with the log signs worth a look.
- 1Injection attempts, login and form posts, burst requests.
- 2Log signs worth a look, each with its usual level.
- 3Path tricks by kind: Log4Shell, path traversal, SQL injection.
Fake browsers
Bots that claim to be a browser, like this one, and what gave them away.
- 1172 requests from bots dressed as browsers.
- 2What gave them away.
- 3The pages they took.
Your stack and advisories
What runs your site, read from outside once a day, and whether this week's flaws apply, with what to do.
- 1PHP 8.3.10: update needed, with the fixed release.
- 2Node.js hides its version: check it on the server.
- 3What serves each site, read from outside.
Block list
The addresses and networks to block, ready for nginx, Cloudflare or fail2ban, with a note to review before blocking.
- 1Review before blocking: why, in plain words.
- 2Ready to paste: nginx, Cloudflare or a plain list.
- 3Networks with two or more scanners, then each address.
Alerts to your team
A possible exposure, a new scan wave or an advisory for your stack, sent to your team by email or Slack.
- 1Three rules, each with when it opens.
- 2Off until you turn them on.
- 3Every alert of the last 30 days.
Skald tells you what applies and what was tried; it does not block attacks. Free to run on your own server, source-available (Elastic License 2.0).
How we keep it honest
- Every number is counted twice, by two separately written counts, and re-checked every morning. A day that doesn't match is withheld.
- Our own audit caught two false “possible exposure” rules in the first version, before any alert went out.
- 8 releases in 30 hours (2.20.0 to 2.22.3, 21–22 Oct 2026), each with public release notes.
- 2,600+ automated tests on four Python versions, and protected releases.
- The advisory list is updated with each release: php2shell was in Skald the night it was announced.
The same night
The same night, PHP announced fixes for two critical flaws, CVE-2026-109618 and CVE-2026-109619, in 8.5.12, 8.4.27, 8.3.36 and 8.2.35 (php.net). Cloudflare, which calls them php2shell, reports attacks already (Cloudflare's blog).
We added it to Skald, built a free check and an open-source command-line tool, php2shell-check (MIT).
Built for this week's threats
Skald's “Your stack” check reads what serves each site, once a day from outside, and matches it against the advisories shipped with each release. Today that is two.
Critical
PHP: php2shell
CVE-2026-109618 and CVE-2026-109619, with attacks already seen.
Fixed in
- 8.5.12
- 8.4.27
- 8.3.36
- 8.2.35
High
Node.js: October 2026 releases
The highest of the fixes is CVE-2026-111497. A Next.js site runs on Node.js.
Fixed in
- 22.23.4
- 24.22.1
- 26.12.1
Each finding says one of three things, with what to do:
- Update neededThe server shows a version without the fix. Update to the fixed release of its branch, then restart it.
- Check your versionThe server hides its version. Check it on the server, for example with
php -v. - No official fixPHP 8.1 and older get no official release with the fix. Move to a supported branch.
- 1PHP 8.3.10: update needed.
- 2Node.js: check your version.
- 3What serves each host.
Skald tells you what applies to your stack and how to fix it. It does not detect or block an exploit of these flaws.