Rich Gibbs

Dionaea via Docker, and Two 'No Hits' Calls the Logs Overruled

homelab · honeypot · dionaea · docker · log-analysis

Dionaea was added to the lab's cloud VPS as the first attack-surface addition beyond the existing Cowrie, mailoney and wp-honeypot stack. The planned from-source build was abandoned because its shellcode-emulation dependency, libemu, has been dropped from the Debian and Ubuntu repositories, which is why the official project documentation warns against Ubuntu 20.04 or higher; both from-source routes either fail outright on 24.04 or need a years-stale fork compiled by hand. The resolution was a deliberately scoped exception: the upstream dinotools/dionaea container, installed through Ubuntu's packaged docker.io, for this one component only, while everything else stays hand-built. Port 80 was left with nginx and the hand-written WordPress decoy, because a generic HTTP responder cannot imitate that login page and because Dionaea's value is the protocol coverage nothing else on the box provides. Verification was run twice over, the container's own report cross-checked against the kernel socket table. The first capture arrived within the first hour: 32 SMB connections from a single external address, spaced four to nine seconds apart, and recon-only, since the DCERPC request and downloads tables both came back empty. The two beliefs that did not survive the session were both about absence. The WordPress decoy was assumed silent but had been logging a Vite dev-server scanner population nobody had looked for, and the mail component was assumed idle while its session database held 456 SMTP sessions. Verify by function, not by status output.

One lab session on 2026-08-31, on the cloud VPS. The parts worth keeping are not the deployment. They are a build that died for a reason I want written down, and two beliefs about silence that did not survive contact with the logs.

The build died on dependency decay, so this one component runs in a container

The plan was to build Dionaea from source, the way everything else here has been built: understand every dependency, compile it yourself. Official Dionaea documentation says plainly to avoid Ubuntu 20.04 or higher, and the reason is libemu, the shellcode-emulation dependency Dionaea needs. It was dropped from the Debian and Ubuntu package repositories and has not been repackaged since. Both from-source paths available to me — the cmake build and the honeynet/nightly PPA — either fail outright on 24.04 or want a years-stale libemu fork compiled by hand first.

That is not a read-the-error-and-fix-it situation. The upstream piece is genuinely gone, and building it by hand would mean fighting infrastructure decay that has nothing to do with honeypot work. So this one component runs the official dinotools/dionaea container, installed from Ubuntu's packaged docker.io (Docker 29.1.3) rather than adding Docker's own APT repository for a single-purpose install.

I am writing that down as a decision because a scoped exception to a principle is still a decision. The scope is the part that keeps it honest: one component, one container, and everything else stays hand-built.

Port 80 stayed where it was

Dionaea's documented default port list includes 80. I dropped it. nginx already owns port 80, reverse-proxying to the hand-written WordPress decoy, and there were two separate reasons not to take it over.

Realism is the first one. Dionaea's HTTP module is a generic responder; it has no idea it is supposed to look like a WordPress login page. Swapping the purpose-built wp-login.php decoy for that would be a downgrade aimed exactly at the population the decoy exists to catch, the credential-stuffing bots.

The second is that duplication is not coverage. What Dionaea adds is protocol surface nothing else on this box captures: SMB, MSRPC, MSSQL, FTP, TFTP, MQTT, SIP, memcached. Handing it port 80 would trade a distinct new capability for a worse copy of something that already works.

So it got the rest of the documented defaults, with port 80 deliberately absent: FTP 21, WINS 42, TFTP 69/udp, MSRPC 135, HTTPS 443, SMB 445, MSSQL 1433, PPTP 1723, MQTT 1883, UPnP/SSDP 1900/udp, MySQL 3306, SIP 5060 on tcp and udp plus 5061, and memcached 11211.

Three bind mounts went in before any of it mattered — config, state and library, logs — plus --restart unless-stopped, so a restart or a crash does not quietly discard every captured binary and log line. The first boot printed a wall of template/... -> ... lines. That is the container populating the empty mounts with its default config and directory layout: expected first-boot scaffolding, not an error.

The verification that actually counts

Two layers, and I trust the second one. The container's own listing said it was up and that the published mappings matched the list I asked for. But a container can report Up while a port mapping silently failed to bind, so that column is a claim, not proof. The independent check is the kernel's socket table, where docker-proxy appears as the owner on every one of those ports. That is the expected answer: Docker routes host-port traffic into the container's network namespace through its own userspace proxy, so the real Dionaea process never shows up directly on the host.

Cost, with it running: roughly 200 MB for the Docker daemon plus the container, 2.8 GB still available, swap untouched.

Two calls about absence, both wrong

Both were mine, and both were about something I had decided was not happening.

The WordPress decoy I had filed as zero hits. It wasn't — the log had simply never been read. What is in it: nineteen requests from one external address inside a one-second window, every path a Vite dev-server hot-module-reload endpoint, and not a single WordPress path among them. All of them correctly 404'd, because this box was never built to emulate that stack. The useful part is the attempt rather than the outcome: a second, distinct scanner population probing for Vite dev servers left running in production, which is a current vulnerability class of its own and nothing to do with WordPress credential stuffing.

Every request in that burst also carried a different randomized User-Agent while coming from the same address in the same second against identical paths. That is automated fingerprint rotation, not nineteen humans with nineteen browsers, and it is the kind of tell worth keeping for later adversary-taxonomy work.

The only two non-hostile entries in that log were my own verification checks from the box itself, which is why they get excluded from adversary analysis rather than counted as a hit.

Mailoney was the other one. I had an impression of no mail, based on nothing better than a point-in-time socket check that showed nothing connected at that instant. The session database says 456 SMTP sessions. The component has been capturing real traffic the whole time; the thing that was broken was my test, not the sensor.

One item left open there: a tail against the log glob returned nothing, probably because the glob does not match mailoney's real log filename. Fast follow, not urgent, since the session count already proves the component works.

The first capture, and one dead end in the data

Within the first hour of Dionaea being live: 32 connections from a single external address to the SMB port, spaced roughly four to nine seconds apart over several minutes. Persistent automated scanning, not a one-off probe, and genuinely new surface that neither Cowrie's shell emulation nor the web decoy could have captured.

The follow-up queries against the DCERPC request table and the downloads table both came back with zero rows — a clean empty result, not an error. The traffic never got past connect and disconnect. That reads as recon-only: a discovery scanner, or a worm's early probing phase, confirming the port is open and reachable, with no exploit call ever made and nothing offered as a download. A useful negative result. Whether that address or others escalate on a later connection is unobserved, and the connections table is the place to check for repeat visitors.

Captured data lives in three places, by type: log files, a binaries directory, and a SQLite database. sqlite3 is not installed on the host by default, so that is a step to remember. The main log runs at debug verbosity by default, all the way down to connection ref and unref lines, so it wants grepping rather than tailing raw, and the errors log should stay near empty.

The dead end worth naming: logsql.sqlite is not the database. The container touches an empty placeholder at startup that is never written to. The real, actively growing file is dionaea.sqlite, even though the handler is named logsqlhandler in the startup log. The class name and the output filename do not match in this image version, it is not documented anywhere obvious, and I only found it by comparing file sizes from inside the running container.

The schema is far broader than what Cowrie produces — roughly one table per protocol or module, from connections and downloads through the DCERPC tables, MQTT, MSSQL, MySQL, SIP, logins, resolves and the VirusTotal tables.

And the binaries directory is the payoff: real malware an attacker tried to drop, ready for the same triage the other captures get, under the same standing rule as everything else captured here — nothing found in there gets executed.

Reminders for next time

  • A documented upstream incompatibility beats a guessed build. The "avoid Ubuntu 20.04+" line was the deciding fact, not a hunch, and it saved the budget of a doomed build.
  • A scoped exception to a principle is still a decision. Write the scope down, or it reads later as drift.
  • "No hits" is a claim that needs the log, not the absence of a memory of checking it.
  • A point-in-time socket check is not a historical function check. Count the sessions; do not sample the port.
  • Four distinct interaction models now run concurrently on one 4 GB / 2 vCPU box at 2.8 GB headroom. That is the number I want to look at again before adding a fifth.