Secure Redis on a VPS without breaking your application
Keep Redis private, add authentication and ACLs, set memory policy, persist intentionally, and verify that public access is impossible.

Redis is fast partly because it assumes it lives on a trusted network. Exposing port 6379 to the internet is not a shortcut; it is an incident waiting to happen.
Bind to a private interface
For an application on the same VPS, use loopback only in /etc/redis/redis.conf:
bind 127.0.0.1 ::1
protected-mode yes
port 6379
For multiple servers, bind to a WireGuard or private-network address and restrict the port to your application hosts with an explicit firewall rule. With ufw on Ubuntu 24.04, allow each application host and nothing else:
sudo ufw allow from 10.0.0.5 to any port 6379 proto tcp
sudo ufw status verbose
Replace 10.0.0.5 with each application host's private address, one rule per host, and check that the ufw status verbose output reports Default: deny (incoming); an allow rule only restricts anything when the default policy denies everything else. If ufw status reports inactive, allow your own SSH access first with sudo ufw allow OpenSSH before running sudo ufw enable, or enabling the firewall will cut off your session. Never use bind 0.0.0.0 unless a firewall rule like this has already been tested.
One more thing before moving on: editing redis.conf changes nothing on the running server. The bind and protected-mode lines take effect at the next restart, which happens during the ACL cutover below, so do not skip that restart. Until it runs, Redis is still listening on whatever interface it used before, and the exposure is not yet closed.
Use an ACL user
Create a dedicated application identity instead of relying on the default user. Do this in stages: the application is connected as the default user right now, and the order of operations decides whether it keeps working.
Start by finding out which key names your application actually uses. ACL key patterns are strict, and a pattern that misses a prefix means NOPERM errors on every command that touches those keys. Sample the live keyspace:
redis-cli --scan --count 1000 | cut -d: -f1 | sort | uniq -c | sort -rn | head
If everything lives under a prefix like app:, the pattern below works as written. If your keys use other prefixes, or none at all, widen the pattern to match reality: list each real prefix (~app:* ~sess:*), or use ~* to allow all keys, which is still a big improvement over the unrestricted default user. Migrating keys to a single prefix is also an option, but do it before the ACL cutover, not after.
Add the new users in /etc/redis/redis.conf, and leave the default user enabled for now. The application still authenticates as default, and disabling it at this point would cut the app off.
These lines put both passwords into the file in plaintext, and the admin line grants +@all, so anyone who can read redis.conf gets full administrative access to Redis. Check the permissions before you write the secrets in: ls -l /etc/redis/redis.conf should show the file owned by root:redis or redis:redis with mode 640. If it is any more open than that, tighten it first with sudo chown root:redis /etc/redis/redis.conf and sudo chmod 640 /etc/redis/redis.conf:
user webapp on >replace-with-a-long-random-secret ~app:* &* +@read +@write +@connection -@dangerous
user admin on >replace-with-a-different-long-secret ~* &* +@all
The webapp user can read and write keys matching its pattern, plus connection housekeeping like PING, but cannot run administrative commands. The admin user is your management identity; keep one, because once the default user is off there is otherwise no way to administer Redis without editing the config file and restarting again. Store each secret in the appropriate protected environment file. For a stricter setup, Redis also accepts a SHA-256 hex digest in place of the plaintext password (write # followed by the digest instead of the > rule, generating it with echo -n 'the-secret' | sha256sum), and the user definitions can move to a dedicated aclfile kept out of the main config.
The restart below also puts the bind and protected-mode change from the first section into effect. Restarting Redis drops every current client connection for a moment while the service comes back, so if the application cannot tolerate a brief blip, pick a quiet window:
sudo systemctl restart redis-server
Test the new user. Avoid putting real passwords directly on a shared shell command line; pass them via REDISCLI_AUTH so they stay out of shell history and process listings:
REDISCLI_AUTH='your-secret' redis-cli --user webapp SET app:health ok
REDISCLI_AUTH='your-secret' redis-cli --user webapp GET app:health
Now switch the application itself over: configure its Redis client with the webapp username and secret, restart the application, and exercise its real Redis paths, not just one test key. Log in, write a session, run a background job, whatever the workload does. Watch the application logs for NOPERM or authentication errors; a wrong key pattern shows up here, while there is still a working default user to fall back to.
Only when the application is verifiably running as webapp should you disable the default user. This change locks out every client that still connects without credentials as soon as Redis restarts. A forgotten cron job, metrics exporter, or second service still using the default user will start failing:
user default off
The restart below is the actual cutover. It interrupts the service again and enforces the lockout, so make sure the previous verification passed first:
sudo systemctl restart redis-server
REDISCLI_AUTH='your-secret' redis-cli --user webapp PING
Confirm the application stays healthy after this restart before you walk away.
Bound memory use
Redis will otherwise grow until the kernel intervenes. Choose the eviction policy before you write anything down, because it decides what happens to your data at the limit: allkeys-lru evicts, meaning permanently deletes, whichever keys were least recently used once memory is full, so only set it on a disposable cache. For queues, sessions, or anything else that must not vanish, use noeviction instead and alert before the limit is reached:
maxmemory 768mb
maxmemory-policy allkeys-lru
Leave memory for the operating system and application. Redis persistence can temporarily increase memory use during background saves.
Choose persistence deliberately
For a cache, persistence may be unnecessary. Understand what these two lines do before writing them: with persistence disabled, every key in this instance is permanently lost on any restart or crash, with no way to recover it. Only choose this if every key can be regenerated from another source. If the instance currently holds anything durable, take a final snapshot first: run BGSAVE as the admin user, confirm it finished with LASTSAVE, and copy /var/lib/redis/dump.rdb off the box before applying:
save ""
appendonly no
For durable state, enable append-only persistence:
appendonly yes
appendfsync everysec
Even then, Redis replication is not a backup. Copy tested RDB or AOF data off the VPS and understand how much recent data can be lost.
Apply the memory and persistence changes
Nothing in the last two sections is live yet. Redis reads its config file only at startup, and the last restart happened back at the ACL cutover, before these lines existed. Do not leave it in this state: the running server and the file now disagree, and the next unplanned restart would activate the new settings, persistence changes included, at an arbitrary moment. As before, a restart drops every client connection briefly, so pick a quiet window:
sudo systemctl restart redis-server
If another blip is unacceptable right now, set the same values on the running server instead. They take effect immediately, and the file you already edited carries them across the next restart:
REDISCLI_AUTH='your-admin-secret' redis-cli --user admin CONFIG SET maxmemory 768mb
REDISCLI_AUTH='your-admin-secret' redis-cli --user admin CONFIG SET maxmemory-policy allkeys-lru
REDISCLI_AUTH='your-admin-secret' redis-cli --user admin CONFIG SET appendonly no
REDISCLI_AUTH='your-admin-secret' redis-cli --user admin CONFIG SET save ""
Match the values to whatever you chose above; if you enabled AOF, use CONFIG SET appendonly yes and CONFIG SET appendfsync everysec instead.
Verify exposure and behaviour
sudo ss -lntp | grep 6379
sudo ufw status numbered
REDISCLI_AUTH='your-admin-secret' redis-cli --user admin CONFIG GET maxmemory
REDISCLI_AUTH='your-admin-secret' redis-cli --user admin CONFIG GET appendonly
REDISCLI_AUTH='your-admin-secret' redis-cli --user admin INFO memory
REDISCLI_AUTH='your-secret' redis-cli --user webapp PING
REDISCLI_AUTH='your-secret' redis-cli --user webapp CONFIG GET dir
ss should show Redis listening only on loopback or your private address, never a public one. If you took the multi-server path, the ufw listing should contain only the allow rules you created earlier, with the default incoming policy still deny. The two CONFIG GET calls as admin confirm that the running server matches the file you edited; if either value comes back different, the apply step was skipped. The webapp PING should succeed, and the webapp CONFIG GET dir should be denied: the application user must not be able to run CONFIG, FLUSHALL, MODULE, or other administrative commands. INFO is in the @dangerous category too, which is why the memory check runs as admin. Finally, from a separate internet host, a connection to port 6379 should time out or be refused.
Monitor used memory, evicted keys, rejected connections, hit rate, and persistence errors. Redis is low-maintenance when its network boundary and memory ceiling are explicit.
Check the client and the failure mode
Configure the application with a connect timeout, command timeout, and bounded retry policy. An infinite reconnect loop can turn a short Redis outage into a CPU and log storm. Use a separate database or key prefix per application, but remember that Redis logical databases are not a security boundary; ACLs and network policy are.
During a maintenance window, restore a copy of the persistence file into a disposable Redis instance and verify that the application can rebuild or read its expected keys.
Continue reading