diff --git a/deploy/production/README.md b/deploy/production/README.md index ca1e2af..dac1ad9 100644 --- a/deploy/production/README.md +++ b/deploy/production/README.md @@ -13,4 +13,4 @@ SSH 隧道账号只接受由注册服务 CA 签发的短期证书,只允许转发到服务器 `127.0.0.1:3307`,并关闭终端、TTY、代理转发和密码认证。生产主机的 SSH 管理账号使用独立密钥,关闭密码登录。 -Redis 原始端口不映射到宿主机。公网 `6380` 只接受 TLS,注册服务为每台设备创建独立 ACL 账号;停用安装会删除该账号。证书文件应归属 HAProxy 容器的 `99:99` 并保持 `0600`,续期后需重新生成 `/opt/cangku/shared/redis-tls.pem`、恢复该属主并重启 TLS 网关。 +Redis 原始端口不映射到宿主机。公网 `6380` 只接受 TLS,注册服务为每台设备创建独立 ACL 账号;停用安装会删除该账号。ACL 保存在 Redis 数据卷的 `users.acl`,注册与撤销后立即执行 `ACL SAVE`,容器重启不会丢失设备账号。证书文件应归属 HAProxy 容器的 `99:99` 并保持 `0600`,续期后需重新生成 `/opt/cangku/shared/redis-tls.pem`、恢复该属主并重启 TLS 网关。 diff --git a/deploy/production/docker-compose.yml b/deploy/production/docker-compose.yml index e55a3f0..c248136 100644 --- a/deploy/production/docker-compose.yml +++ b/deploy/production/docker-compose.yml @@ -31,7 +31,7 @@ services: restart: unless-stopped env_file: - /opt/cangku/shared/redis.env - command: ["sh", "-c", "exec redis-server --appendonly yes --save 60 1000 --requirepass \"$$REDIS_PASSWORD\""] + command: ["sh", "-c", "if [ ! -s /data/users.acl ]; then printf 'user default on >%s ~* +@all\\n' \"$$REDIS_PASSWORD\" > /data/users.acl; fi; exec redis-server --appendonly yes --save 60 1000 --aclfile /data/users.acl"] volumes: - /opt/cangku/data/redis:/data healthcheck: diff --git a/remote-access/server/bootstrap_service.py b/remote-access/server/bootstrap_service.py index c90b6e5..d279fe0 100644 --- a/remote-access/server/bootstrap_service.py +++ b/remote-access/server/bootstrap_service.py @@ -188,18 +188,22 @@ class Broker: def _create_redis_user(self, username, password): if not re.fullmatch(r'cangku_[a-f0-9]{16}', username): raise ValueError('Refusing to create an unexpected Redis username') - self._redis_connection().execute_command( + client = self._redis_connection() + client.execute_command( 'ACL', 'SETUSER', username, 'reset', 'on', f'>{password}', '~*', '+ping', '+get', '+set', '+setex', '+del', '+unlink', '+exists', '+expire', '+ttl', '+scan', '+eval', '+quit', '+client|setname', '+client|setinfo', ) + client.execute_command('ACL', 'SAVE') def _drop_redis_user(self, username): if not username or not re.fullmatch(r'cangku_[a-f0-9]{16}', username): return - self._redis_connection().execute_command('ACL', 'DELUSER', username) + client = self._redis_connection() + client.execute_command('ACL', 'DELUSER', username) + client.execute_command('ACL', 'SAVE') def _create_application_admin(self, username, password, label): password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)).decode()