Marcus manages the day-to-day editorial workflow across all brands, coordinating coverage assignments and ensuring editorial standards are met. He previously served as city editor at the Pioneer Press and holds a degree in Political Science from Carleton College.
coldframe
A honeypot for supermarket refrigeration controllers.
In 2026, attackers broke into internet-facing refrigeration supervisors across Europe using default passwords and old, unpatched firmware, then forced defrost cycles. coldframe pretends to be one of those controllers. It records who connects, what passwords they try and which public exploits they use, and it never gives them anything real.
It is one Python file with no dependencies outside the standard library.
Personas
Persona
Pretends to be
What it serves
danfoss
Danfoss AK-SM 800A system manager
StoreView login, a fake store dashboard, the XML API at
/html/xml.cgi
and
/xml.cgi
, HTTP and HTTPS
dixell
Copeland/Dixell XWEB EVO supervisor
Login page and the XWEB 500 CGI paths targeted by public exploits
Looking like a real unit
The Danfoss persona copies what internet scanners record for real AK-SM 800A units: the
nginx
server header and header order, the
Danfoss StoreView
page title, TLS 1.2 only, and a 1-year certificate for
CN=DanfossSM800A
issued by a
SelfSigned-DanfossSM800A
authority. Each build generates its own keys.
Weak logins (for example
Supervisor
/
12345
) and a correct CVE-2025-41450 "code of the day"
succeed
. The attacker gets a session token and a dashboard of fake refrigerated cases with defrost buttons. Every action they take is logged as a
control
event; nothing is real.
The Dixell persona follows real XWEB EVO units the same way: no
Server
header, their security-header set and order, the same asset paths and page size. Its login uses the real
/cgi-bin/login.lua
flow: the documented factory login
Admin
/
Admin
succeeds, five failures in five minutes lock the user, and attempts at the XWEB Pro
auth_mode
bypass (CVE-2026-25085) are tagged.
Real units of both vendors serve their own favicon, and scanners match devices by its hash. That icon is the vendor's artwork, so it is not in this repository. For a private deployment you can point
FAVICON_FILE
at a copy you have the right to use.
The same goes for the Danfoss start page. Real units serve a short Angular loader page from the firmware, and thousands share its exact hash. Point
INDEX_FILE
at that
index.html
(from the public firmware package) and the decoy serves it byte for byte, with its original date in
Last-Modified
and
ETag
. The loader's
main.*.js
then shows the decoy's own login page, laid out like the firmware's remote login. If the firmware's
assets/images/Danfoss-logo.png
sits beside that
index.html
, the page uses it.
Both personas can also run an optional
Modbus TCP
service with fake case temperatures, setpoints and a defrost register. Real AK-SM 800A units only use Modbus to talk to their own case controllers, so Modbus is off by default. Turn it on to catch generic industrial scanners.
Exploits it recognises
Requests that match a public exploit get an
exploit
field in the log:
CVE-2023-25914
: AK-SM 800A path traversal via
get_file
CVE-2023-25915
: AK-SM 800A command injection via
load_cert
CVE-2025-41452
: AK-SM 800A nginx header overwrite via
load_nginx_headers
CVE-2025-41450
: AK-SM 800A "code of the day" login bypass (
getauth
with
eds_only
or
quantum_only
), noting whether the attacker computed the correct code
SM800 code-of-the-day bypass
:
eds_only
/
quantum_only
on other commands
CVE-2021-45420
: XWEB 500 upload and command paths under
/cgi-bin/
CVE-2026-25085
: XWEB Pro login bypass via an unexpected
auth_mode
Quick start
PERSONA=danfoss HTTP_PORT=8080 python3 -I honeypot.py
Open
http://localhost:8080
and every request shows up as a JSON line on stdout and in
events.jsonl
.
Run the tests (they start the honeypot on loopback and attack it):
python3 -I test_honeypot.py
Deploying
Quickest: paste into "User data"
Create an Ubuntu 24.04 server (a $4 DigitalOcean droplet is plenty) and paste
cloud-init.yaml
into the
User data
box. Change
PERSONA=danfoss
to
dixell
if you want the XWEB persona. About two minutes after boot the decoy is listening on port 80 (and 443 for Danfoss), hardened as below.
Then, in your provider's firewall, allow only the decoy's ports from anywhere and SSH from your own address. Read the log with:
ssh root@
<
server
>
'
podman logs coldframe
'
By hand
Build the container. Each build makes a fresh self-signed certificate, so two deployments never share one.
podman build -t coldframe
.
Run it with
rootful
podman or Docker. Rootless port forwarding rewrites the source address, which makes every attacker look like the same internal IP.
--userns=auto
maps the container to an unused host UID range, so even the container user is nobody on the host. Podman needs a range to hand out, once per host:
echo
"
containers:2147483647:2147483648
"
|
sudo tee -a /etc/subuid /etc/subgid
sudo podman run -d --name coldframe --restart=always \
--read-only --cap-drop=ALL --security-opt no-new-privileges --userns=auto \
--memory 128m --pids-limit 512 --log-opt max-size=10m \
-p 80:8080 -p 443:8443 \
-v coldframe-logs:/logs:U \
-e PERSONA=danfoss \
coldframe
For Modbus, add
-e MODBUS_PORT=5020 -p 502:5020
.
On the host, only expose the ports the persona would really have, and block outbound traffic except DNS and package updates: the decoy never needs to connect out. Keep SSH reachable only from your own address: a fridge controller with SSH open to the world looks wrong.
Configuration
Variable
Default
Meaning
PERSONA
danfoss
danfoss
or
dixell
HTTP_PORT
8080
HTTP listener
HTTPS_PORT
0
(off)
HTTPS listener, needs
CERT_FILE
and
KEY_FILE
MODBUS_PORT
0
(off)
Modbus TCP listener
LOG_PATH
events.jsonl
Where events are appended
FAVICON_FILE
none
Optional favicon to serve (private deployments only)
INDEX_FILE
none
Optional Danfoss firmware
index.html
to serve as the start page (private deployments only)
Reading the log
Each line is one event:
{
"ts"
:
"
2026-11-05T10:57:21.158+00:00
"
,
"persona"
:
"
danfoss
"
,
"proto"
:
"
http
"
,
"src_ip"
:
"
203.0.113.7
"
,
"src_port"
:
6782
,
"event"
:
"
login_attempt
"
,
"username"
:
"
admin
"
,
"password"
:
"
admin
"
}
event
When
request
Any HTTP(S) request, with method, path, Host, User-Agent, Authorization and body
login_attempt
Credentials submitted through the login page or the XML API, with
granted
control
An action from a logged-in session, such as starting a defrost
xml_cmd
A Danfoss XML API command, with its attributes and any
exploit
match
tls_error
A failed TLS handshake (often a scanner speaking plain HTTP to 443)
connect
,
read
,
write
,
device_id
,
unsupported
,
malformed
Modbus activity; writes to the defrost registers carry
"defrost": true
summary.py
turns a log into a short report:
python3 -I summary.py --hours 24 --ignore
<
your-own-ip
>
<
events.jsonl
Safety
It never runs commands, reads files, or forwards traffic. Exploit payloads are logged and dropped.
Request bodies, logged fields and connections are capped, including a per-source limit so one attacker cannot crowd out others. Idle sockets time out, and the event log rotates once it gets large.
Connections that trickle data to stay open are cut after 30 seconds, and refused connections are logged.
Over-long log lines are truncated, and
summary.py
neutralises terminal escape codes from attacker input.
The container runs as a non-root user in its own user namespace, on a read-only filesystem with no capabilities.
Log fields are attacker-controlled. Treat them as untrusted when you view or process them.
Limitations
Shallow interaction.
Logged-in attackers see a small dashboard and a handful of XML commands, not the full StoreView app.
Public code can be fingerprinted.
Anyone can read this repository and spot its exact responses. Change the persona strings, page layout and certificate subject for your deployment.
Unco