Stealth

A decoy is only as good as its accent.

Anyone can print a banner that says "Apache." Real servers give themselves away in a hundred smaller ways, and so do fake ones. This is how RipTide decoys learned to speak like the servers they claim to be, and the one-request mistake that taught us to listen harder.

RipTide Research9 min read

The short version

RipTide decoys answer in the exact dialect of the server they claim to be (Apache, nginx, IIS, Go or uvicorn), down to header order, error pages and when they hang up, because those details are how a fake gets spotted.

  1. 1

    A server's banner is the least of it. Header order, capitalization, error pages, which methods it allows and when it closes the connection all identify a server, and scanners compare them.

  2. 2

    Each personality is transcribed from the real server, not guessed: Apache's stock 404 page, nginx's "405 Not Allowed", Go's alphabetized headers, uvicorn's lowercase ones.

  3. 3

    Decoys match their story. The Ollama and Docker decoys speak Go, the vLLM decoy speaks uvicorn, and the decoy website speaks Apache on Ubuntu, right down to its error pages.

01Act I · The ordinary world

The banner said Apache. The 403 said otherwise.

On September 24, we probed our own production decoy by hand, the way an intruder would. On an admin page that asks for a password, we guessed one. The decoy answered with a 403.

The page had the right banner, Apache/2.4.58 (Ubuntu). It had the right heading, Forbidden. And underneath it, a paragraph that began: "The server encountered an internal error or misconfiguration…"

That is Apache's text for a 500 error, not a 403. No stock Apache server sends that combination. Our 403 page had been quietly borrowing the generic paragraph Apache uses for errors it has no specific words for. Anyone who knows Apache, or any tool that keeps a copy of its stock pages, could spot the decoy with a single wrong password.

We fixed it the same day: Apache's real 403 paragraph is one line, "You don't have permission to access this resource." But the lesson stuck. A decoy isn't judged by the banner it shows. It's judged by every small habit it has, and an intruder only needs to catch one.

02Act II · The villain

Scanners listen for the accent.

Every web server has an accent: habits that come from its code, not its configuration. Change the banner and the accent stays. Here are a few of the habits RipTide's personalities reproduce, side by side.

Habits that identify five common web servers, as RipTide's personalities reproduce them
HabitApache 2.4nginx 1.27Go net/httpuvicorn (FastAPI)
Server headerIts banner, after DateIts banner, first of allNone at allserver: uvicorn, lowercase
Header namesTitle-CaseTitle-CaseHandler headers sorted A to Z, then a fixed tailAll lowercase, except one
404 pageApache's stock HTML pageA short HTML page with the version in its footer404 page not found, plain text{"detail":"Not Found"}
A verb it doesn't know501405 Not Allowed405 or 404, depending on the path405 or 404, or a parser-level 400
OPTIONSAnswers 200 with an Allow list405No automatic answerNo automatic answer
Keep-aliveEchoes it only when asked, 5 s idle, 100 requestsAlways says keep-alive, 65 s idleSays nothing; keeps the connectionSays nothing; keeps the connection
Default behavior of stock installs, as transcribed into each personality. Frameworks change some of it; the Go and uvicorn personalities have settings for the common ones.

None of these are secrets. They are written down in each server's source code, which is exactly why a tool can check them. That is the problem. A decoy built on a general-purpose web framework answers every one of these questions in its own framework's accent, whatever banner it prints.

Ours did too, at first. The very first version of RipTide's web decoy announced itself as a Python server and hung up after every single response. Real Apache does neither. Two of the first changes we made, both on August 19, were to present as Apache on the wire, with Apache's header order and error pages, and to keep connections open the way Apache really does.

Change the banner and the accent stays.

03Act II · The struggle

Transcribe, don't guess.

The easy way to imitate a server is to write down what you remember about it. Memory is exactly what fails. So every personality follows one rule: find a real one, ask it the same questions, and copy the answers byte for byte.

The questions are always the same small set: a normal GET, a missing page, a wrong method, OPTIONS, HEAD, and a connection that asks to stay open next to one that asks to close. The real server's answers become the personality, and the tests.

  1. 01

    nginx

    Transcribed from the official nginx 1.27 container image answering that set of questions. Some of what it taught us: nginx calls a 405 "Not Allowed," not "Method Not Allowed," refuses OPTIONS on a static site, and says Connection: keep-alive even to a client that never asked.

  2. 02

    uvicorn

    Checked over raw sockets against a real FastAPI app. uvicorn writes every header name in lowercase, except one: when it closes a connection, the HTTP library underneath writes Connection: close with a capital C. Run uvicorn with its other parser, httptools, the one vLLM uses, and that last line is lowercase too, and a method the parser doesn't know gets a fixed 30-byte 400 before the app ever sees it. The vLLM decoy uses that flavor, because vLLM does.

  3. 03

    Go

    Go's server sorts the headers a handler sets alphabetically, then writes a fixed tail, and never sends a Server header of its own. Even where Go puts Content-Type depends on whether the program set it or Go guessed it from the bytes. The Docker decoy reproduces that detail on one endpoint, because a real Docker daemon does it there.

  4. 04

    IIS

    The hard one. We had no Windows machine to ask. So the IIS personality is built only from published captures of real IIS 10.0 servers, its 404 page matches a published copy byte for byte, and everything we couldn't verify is written down as unverified rather than invented.

The personalities also keep a written list of the places they still differ from the real server, and why. We don't publish that list, for obvious reasons. But it exists, and each entry on it is a decision rather than an accident. The 403 page was the accident that taught us to keep it.

04Act III · The turn

One engine, five accents.

RipTide splits a decoy's HTTP conversation in two. The engine handles the protocol: reading requests, routing, framing responses. A personality decides everything a listener could use to tell one server from another.

  • Identity: the Server banner, or none, and set per decoy, so the website can say Apache/2.4.58 (Ubuntu) and the Docker decoy Docker/27.3.1 (linux).
  • Header order and casing: the last thing that runs before a response goes out, so every header, from any route, lands where that server would put it.
  • Words: reason phrases and error pages, copied from the real server.
  • Manners: whether OPTIONS gets an automatic answer, what an unknown method gets, whether HEAD comes free with GET, and what the Allow header lists, in which order.
  • Patience: how long an idle connection stays open, how many requests one connection may make, and which errors make the server hang up.

Each decoy picks the personality its story needs. The decoy website speaks Apache on Ubuntu. The Ollama decoy speaks Go, because Ollama is written in Go, with Gin's plain-text error pages. The vLLM decoy speaks uvicorn with the httptools parser and FastAPI's cross-origin middleware, because that is how vLLM ships. The Docker decoy speaks Go the way the Docker daemon's router does, where a wrong method falls through to the same bare 404 as a wrong path.

missing page · nginx personality
HTTP/1.1 404 Not Found
Server: nginx/1.27.5
Date: Fri, 02 Oct 2026 14:07:12 GMT
Content-Type: text/html
Content-Length: …
Connection: keep-alive

<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.27.5</center>
</body>
</html>
A missing page as stock nginx 1.27 answers it, and as the nginx personality does. Abridged: headers that don't change the point are left out.

The personality also guards everything else a decoy says. Replies a language model writes can't set Server, Date or framing headers; those are stripped and the personality writes its own. Pre-written replies can't set them either, and a test checks that every version number in them matches the server the personality claims.

05Under the hood

The details that took the longest.

Keep-alive is a fingerprint

How a server handles a connection that wants to stay open is one of the most reliable tells there is, because it's rarely configured and easy to probe. Apache with keep-alive on only advertises it to a client that asks, and then counts down the requests the connection has left: Keep-Alive: timeout=5, max=100, then 99, and so on. A plain HTTP/1.1 client like curl, which doesn't ask, gets no keep-alive headers at all, and the connection still stays open. nginx always says Connection: keep-alive and never sends a Keep-Alive line. Go and uvicorn say nothing. Each personality reproduces its server's habit, idle timeout and per-connection limit.

Some errors end the conversation

Real servers hang up after certain errors no matter what the client asked for. Apache drops the connection after a 400, 408, 411, 413, 414, 500, 501 or 503. nginx after a 400, 413, 414, 500 or 501. Go only after malformed requests. A decoy that keeps talking after an error the real server would hang up on has an accent.

HEAD is not always free

Apache, nginx and Go 1.22's standard router answer HEAD for any page that answers GET. FastAPI doesn't: a HEAD on a GET-only route is a 405. Docker's router doesn't either, except on one health-check endpoint. Each personality knows which kind of server it is.

What the personalities don't cover

Personalities shape the HTTP conversation: everything on the wire once a connection is open. They don't change how the host's TLS or TCP stack behaves, and we don't claim they do.

06The moral

Sound boring.

The best decoy doesn't sound impressive. It sounds like a default install somebody forgot about: stock error pages, stock headers, stock manners. Every detail a scanner checks is a detail we copy from the real thing, because the moment a decoy sounds different, it stops being a decoy and becomes a sign that says "skip this one."

We learned that from a single 403 page. Now every personality is built the slow way, by asking the real server and writing down exactly what it says, so that an intruder never gets the one-request tell, and keeps talking to a decoy that sounds like every other server it has ever met.

Scenes marked as illustrative are composites written to show how the technique works, not a record of a specific customer incident. Canary credentials are non-privileged and exist only for detection. RipTide detects and alerts; it never takes destructive action against anyone's infrastructure.

Questions

Fair questions.

Which servers can a decoy imitate?

Apache 2.4, nginx 1.27, Microsoft IIS 10.0, Go's net/http (in the styles of Gin, the standard router and the Docker daemon) and uvicorn with Starlette or FastAPI. The banner is set per decoy, so an Apache decoy can claim the exact version and distribution you want it to.

Is the imitation perfect?

No, and we say so. Each personality keeps a written list of where it still differs from the real server. Those differences are deliberate and few, and the list keeps shrinking. We don't publish it.

Do model-written and pre-written replies keep the accent?

Yes. The personality owns the Server, Date and framing headers, so a reply can't set them. Pre-written replies are also tested to make sure every software version they mention matches the server the decoy claims to be.

Make contact.

Tell us what your real servers run. We'll show you a decoy that sounds the same.

Book a briefing

Thirty minutes with the people who built it. Bring your hardest question.

  • Watch a live agent set off a detection
  • Map decoys to your crown jewels
  • Plan a first deployment in one sitting

We use your email only to reply. No newsletter, no list, no sharing.

Keyboard shortcuts

T
Change the theme. Shift+T goes back.
?
Show this list
Esc
Close whatever's open