Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion OPERATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,30 @@ spec:
...
```

In lean mode it is recommended to set at least `configs.controller.javaOpts=-Xmx2048M`
In lean mode it is recommended to set at least `configs.controller.javaOpts=-Xmx2048M`

## Redis ACL persistence

With `redis.persistence-enabled: true`, Redis stores its ACLs in
`/bitnami/redis/data/users.acl` on the existing Redis data PVC. Account creation,
reconciliation, quota permission changes and successful revocation save the
current authorized state. An RDB data snapshot alone does not contain ACL users.

On a fresh installation an init container creates the initial default-user ACL
with the configured administrator password hash. It never overwrites an existing
ACL file and refuses an empty existing file. The system and tenant accounts are
then provisioned and saved through the normal operator lifecycle.

On operator resume, an existing persistent Redis first saves its live ACL state,
then updates the managed ConfigMap to load that file on the next Redis restart.
Because `aclfile` is immutable at runtime, account changes made before that
first restart also update the file atomically through protected stdin. This
preparation does not restart Redis or replay old ACL exports. Credentials,
key/channel grants, read-only state and successful revocations are preserved.
A failed save stops configuration migration rather than enabling a missing file.
Existing explicitly configured ACL files retain their configured storage path.

When data persistence is disabled, this change does not introduce an ACL volume
or enable file persistence; ephemeral storage retains its existing semantics.
This fix concerns identity persistence, not the independent RDB snapshot schedule
or recovery of data that was never saved.
77 changes: 72 additions & 5 deletions openserverless/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import urllib.parse
import hashlib
import contextlib
import subprocess
import shlex
import redis as redislib
from openserverless.redis_acl import tenant_rules
from openserverless.datastore_connection import service_endpoint
Expand All @@ -36,6 +38,41 @@
from openserverless.user_config import UserConfig
from openserverless.user_metadata import UserMetadata

ACL_FILE = '/bitnami/redis/data/users.acl'


def _prepare_existing_acl_storage(data):
"""Persist live identities before an existing Redis can load the new config."""
if not data['persistence']:
return
existing = kube.kubectl('get', 'statefulset', 'redis', '--ignore-not-found', '-o', 'name')
if not existing.strip():
return
wait_for_redis_ready()
with admin_client() as client, client.lock('__ops_acl_lock__', timeout=120, blocking_timeout=15):
path = _save_acl_if_configured(client)
# Explicitly configured ACL files retain their existing configuration.
if path != ACL_FILE:
return
cm = kube.get('configmap/redis-cm')
if not cm or 'redis-conf' not in cm.get('data', {}):
raise RuntimeError('Redis configuration is unavailable')
text = cm['data']['redis-conf']
directives = []
for line in text.splitlines():
fields = line.strip().split(None, 1)
if fields and fields[0].lower() == 'aclfile':
directives.append(shlex.split(line)[1:])
directive = 'aclfile ' + ACL_FILE
if directives:
if any(fields != [ACL_FILE] for fields in directives):
raise RuntimeError('Redis ACL file configuration does not match the running server')
return
patch = {'metadata': {'resourceVersion': cm['metadata']['resourceVersion']},
'data': {'redis-conf': text.rstrip() + '\n' + directive + '\n'}}
kube.kubectl('patch', 'configmap', 'redis-cm', '--type=merge', '--patch-file=/dev/stdin',
input=json.dumps(patch), timeout=30)


def _add_redis_user_metadata(ucfg: UserConfig, user_metadata:UserMetadata):
"""
Expand Down Expand Up @@ -67,6 +104,7 @@ def _add_redis_user_metadata(ucfg: UserConfig, user_metadata:UserMetadata):
def create(owner=None):
logging.info("create redis")
data = util.get_redis_config_data()
_prepare_existing_acl_storage(data)

tplp = ["security-set-attach.yaml","redis-set.yaml"]

Expand Down Expand Up @@ -97,6 +135,7 @@ def wait_for_redis_ready():

def restore_openserverless_db_user():
data = util.get_redis_config_data()
_prepare_existing_acl_storage(data)
create_openserverless_db_user(data)

def create_openserverless_db_user(data):
Expand All @@ -108,7 +147,10 @@ def create_openserverless_db_user(data):
pod_name = util.get_pod_name("{.items[?(@.metadata.labels.name == 'redis')].metadata.name}")

if(pod_name):
res = exec_redis_command(pod_name,path_to_script)
with admin_client() as client, client.lock('__ops_acl_lock__', timeout=120, blocking_timeout=15):
res = exec_redis_command(pod_name,path_to_script)
if res:
_save_acl_if_configured(client)

if(res):
redis_service = util.get_service("{.items[?(@.spec.selector.name == 'redis')]}")
Expand Down Expand Up @@ -218,11 +260,36 @@ def _tenant_identity(namespace, prefix=None):
return prefix


def _write_acl_snapshot(client):
"""Bootstrap an immutable aclfile setting without logging credential hashes."""
pod = util.get_pod_name("{.items[?(@.metadata.labels.name == 'redis')].metadata.name}")
if not pod:
raise RuntimeError('Redis pod is unavailable for ACL persistence')
script = ('umask 077; tmp=$(mktemp /bitnami/redis/data/.users.acl.XXXXXX); '
'trap \'rm -f "$tmp"\' EXIT; cat > "$tmp"; test -s "$tmp"; '
'mv "$tmp" /bitnami/redis/data/users.acl')
# kube.kubectl logs stdin on errors; ACL hashes must not enter that path.
try:
result = subprocess.run(['kubectl', '-n', 'openserverless', 'exec', '-i', pod,
'--', '/bin/sh', '-ec', script],
input=('\n'.join(client.acl_list()) + '\n').encode(),
capture_output=True, timeout=30)
except (subprocess.SubprocessError, OSError):
raise RuntimeError('Could not persist Redis ACL state') from None
if result.returncode:
raise RuntimeError('Could not persist Redis ACL state')


def _save_acl_if_configured(client):
# Preserve the 0.9.0 storage contract: persist when an ACL file is configured.
# With no ACL file, startup reconciliation recreates identities as before.
if client.config_get('aclfile').get('aclfile'):
path = client.config_get('aclfile').get('aclfile')
if not path and util.get_redis_config_data().get('persistence'):
# aclfile is immutable. Until the next Redis restart, save an atomic
# snapshot of the live authorized state instead of CONFIG SET.
_write_acl_snapshot(client)
return ACL_FILE
if path:
client.acl_save()
return path


def provision_user(namespace, password, prefix, readonly=False):
Expand All @@ -239,7 +306,7 @@ def provision_user(namespace, password, prefix, readonly=False):
*['#' + value for value in hashes], *tenant_rules(prefix, readonly))
try:
_save_acl_if_configured(client)
except redislib.RedisError:
except (redislib.RedisError, RuntimeError):
client.acl_deluser(namespace)
raise RuntimeError('Could not persist Redis tenant ACL') from None
return True
Expand Down
1 change: 1 addition & 0 deletions openserverless/templates/redis-conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ data:

# the directory where redis will store data.
dir /bitnami/redis/data
aclfile /bitnami/redis/data/users.acl
{% endif %}


Expand Down
18 changes: 18 additions & 0 deletions openserverless/templates/redis-set.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ spec:
fsGroup: 1001
runAsUser: 1001 #default BITNAMI user
{% endif %}
{% if persistence %}
initContainers:
- name: redis-acl-init
image: docker.io/bitnamilegacy/redis:8.2.1
command:
- /bin/sh
- -ec
- |
umask 077
if [ ! -e /bitnami/redis/data/users.acl ]; then
set -C
printf '%s\n' 'user default on #{{redis_password_sha256}} ~* &* +@all' > /bitnami/redis/data/users.acl
fi
test -s /bitnami/redis/data/users.acl
volumeMounts:
- name: redis-pvc
mountPath: /bitnami/redis/data
{% endif %}
containers:
- name: redis
image: docker.io/bitnamilegacy/redis:8.2.1
Expand Down
2 changes: 2 additions & 0 deletions openserverless/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#
# this module wraps utilities functions
import logging
import hashlib
import math
import random
import time
Expand Down Expand Up @@ -495,6 +496,7 @@ def get_redis_config_data():
"size": cfg.get("redis.volume-size", "REDIS_VOLUME_SIZE", 10),
"storageClass": cfg.get("openserverless.storageclass"),
"redis_password":cfg.get("redis.default.password") or "s0meP@ass3",
"redis_password_sha256": hashlib.sha256((cfg.get("redis.default.password") or "s0meP@ass3").encode()).hexdigest(),
"namespace":"openserverless",
"password":cfg.get("redis.openserverless.password") or "s0meP@ass3",
"prefix": prefix,
Expand Down
Loading
Loading