Redis

One shared Redis instance with per-site ACL users. Each site gets:

Site A literally cannot read or write site B's keys, even though both are in the same Redis process. This is Redis 7.x ACL behavior, configured via aclfile.

Server config

tulixhost edits /etc/redis/redis.conf in place on first setup.sh. The original is preserved as redis.conf.tulix.orig. Changes applied:

bind 127.0.0.1 ::1
protected-mode yes
requirepass <32-char random>
maxmemory-policy allkeys-lru
rename-command CONFIG "TULIX_CONFIG_<random12>"
aclfile /etc/redis/users.acl

The admin password is stored in /etc/tulixhost/tulixhost.conf as REDIS_ADMIN_PASS. CONFIG is renamed so an attacker who gets the admin password can't pivot via CONFIG SET dir /var/lib/redis/.ssh; SAVE without also knowing the rename target.

Maxmemory: shared, but bounded

Redis as installed has no maxmemory cap. On a multi-tenant box, set one explicitly in /etc/redis/redis.conf based on your host's RAM:

maxmemory 1gb
maxmemory-policy allkeys-lru

With allkeys-lru, any key in any site's keyspace becomes evictable when the cap is hit. A noisy-neighbor site can squeeze others out — there's no per-tenant memory cap in Redis. The per-site conf/redis.conf documents max_keys_soft and max_value_bytes as application-enforced hints. Apps that respect them stay neighborly.

DB number assignment

Redis ships with 16 logical databases (0–15). DB 0 is reserved by tulixhost for admin use. The remaining 15 are assigned to sites deterministically by hashing the site name:

db_number = (cksum(site_name) % 15) + 1

This is collision-prone — with more than ~5 sites, two will eventually share a DB number. That is fine, because the ACL key-prefix restriction means they still can't see each other's keys. The DB number is mostly a coarse-grained SELECT destination. If you have more than a dozen sites and want clean separation, switch to a single DB and prefix-only isolation by hand-editing /etc/redis/users.acl.

ACL grammar

Each site's ACL line in /etc/redis/users.acl looks like:

user web_example_com on ><password> ~web_example_com:* &web_example_com:* +@all -@dangerous -select +select|N

Breaking it down:

tokenmeaning
user web_example_comUsername.
onAccount enabled. off would lock it out.
><password>Plain-text password. (Redis also supports #<sha256> for hashed.)
~web_example_com:*Key pattern. The user can only touch keys matching this glob.
&web_example_com:*Pub/Sub channel pattern. Same restriction for SUBSCRIBE/PUBLISH.
+@allAllow all command categories…
-@dangerous…except FLUSHALL, FLUSHDB, KEYS, SHUTDOWN, DEBUG, MIGRATE, etc.
-selectFirst deny the SELECT command entirely…
+select|N…then re-allow only SELECT N (the assigned DB). Order matters: +select|N followed by -select would remove SELECT entirely and break the client's connection.

After editing /etc/redis/users.acl manually, reload with:

redis-cli -a "$REDIS_ADMIN_PASS" ACL LOAD

You can also check who's defined:

redis-cli -a "$REDIS_ADMIN_PASS" ACL LIST
redis-cli -a "$REDIS_ADMIN_PASS" ACL WHOAMI
redis-cli -a "$REDIS_ADMIN_PASS" ACL GETUSER web_example_com

How the app uses it

The site's .envtulix contains:

REDIS_HOST="127.0.0.1"
REDIS_PORT="6379"
REDIS_USER="web_example_com"
REDIS_PASS="..."
REDIS_DB="3"
REDIS_PREFIX="web_example_com"
REDIS_URL="redis://web_example_com:<pass>@127.0.0.1:6379/3"

In PHP (with phpredis):

$r = new Redis();
$r->connect($env['REDIS_HOST'], (int)$env['REDIS_PORT']);
$r->auth([$env['REDIS_USER'], $env['REDIS_PASS']]);   // ACL auth: array form
$r->select((int)$env['REDIS_DB']);

// All keys MUST be prefixed.
$key = $env['REDIS_PREFIX'] . ':user:42';
$r->set($key, $value, ['ex' => 600]);

Forgetting the prefix gives you NOPERM no permission to access keys matching pattern. Some PHP frameworks (Laravel, Symfony cache) support a global key prefix — set it to the REDIS_PREFIX value plus a colon.

Soft per-site limits

conf/redis.conf documents soft guidance the app should honor:

settingdefaultmeaning
max_keys_soft100000Don't grow your keyspace beyond this without checking.
max_value_bytes1 MiBReject larger SETs at the app layer.
default_ttl_seconds3600Always set an expiry. Forever keys clog evictions.
ttl_short / medium / long / day60 / 600 / 3600 / 86400Suggested TTL classes for cache entries.

Common operations

Inspect this site's keyspace

source /data/web/example.com/conf/.envtulix
redis-cli -u "$REDIS_URL" --scan --pattern "${REDIS_PREFIX}:*" | head

Wipe this site's keys (without touching others)

source /data/web/example.com/conf/.envtulix
redis-cli -u "$REDIS_URL" --scan --pattern "${REDIS_PREFIX}:*" \
  | xargs -r -n100 redis-cli -u "$REDIS_URL" DEL

FLUSHDB would do it in one call, but it's in @dangerous and denied by the per-site ACL — deliberately.

Rotate the password

Edit /etc/redis/users.acl, change the >... token, ACL LOAD, then update REDIS_PASS in the site's .envtulix and restart the app (or systemctl reload php8.4-fpm to drop persistent connections).