Redis
One shared Redis instance with per-site ACL users. Each site gets:
- A dedicated DB number (1–15; DB 0 is reserved for admin)
- A unique key prefix (the site's unix-safe name)
- An ACL user restricted to that DB and keys with that prefix
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:
| token | meaning |
|---|---|
user web_example_com | Username. |
on | Account 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. |
+@all | Allow all command categories… |
-@dangerous | …except FLUSHALL, FLUSHDB, KEYS, SHUTDOWN, DEBUG, MIGRATE, etc. |
-select | First 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:
| setting | default | meaning |
|---|---|---|
max_keys_soft | 100000 | Don't grow your keyspace beyond this without checking. |
max_value_bytes | 1 MiB | Reject larger SETs at the app layer. |
default_ttl_seconds | 3600 | Always set an expiry. Forever keys clog evictions. |
ttl_short / medium / long / day | 60 / 600 / 3600 / 86400 | Suggested 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).