From 14bb28c3494e8edf90d2e684dac095f066232dab Mon Sep 17 00:00:00 2001 From: Alwyn Davis Date: Thu, 28 Oct 2021 18:59:45 +1100 Subject: [PATCH] Allow setting ACLs for znodes in Zookeeper (#2086) Add a configuration option (`set_acls`) for Zookeeper DCS so that Kazoo will apply a default ACL for each znode that it creates. The intention is to improve security of the znodes when a single Zookeeper cluster is used as the DCS for multiple Patroni clusters. Zookeeper [does not apply an ACL to child znodes](https://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#sc_ZooKeeperAccessControl), so permissions can't be set at the `scope` level and then be inherited by other znodes that Patroni creates. Kazoo instead [provides an option for configuring a default_acl](https://kazoo.readthedocs.io/en/latest/api/client.html#kazoo.client.KazooClient.__init__) that will be applied on node creation. Example configuration in Patroni might then be: ``` zookeeper: set_acls: CN=principal1: [ALL] CN=principal2: - READ ``` --- docs/ENVIRONMENT.rst | 1 + docs/SETTINGS.rst | 1 + patroni/config.py | 4 ++-- patroni/dcs/zookeeper.py | 14 ++++++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/ENVIRONMENT.rst b/docs/ENVIRONMENT.rst index 41613787..dd78d35e 100644 --- a/docs/ENVIRONMENT.rst +++ b/docs/ENVIRONMENT.rst @@ -84,6 +84,7 @@ ZooKeeper - **PATRONI\_ZOOKEEPER\_KEY**: (optional) File with the client key. - **PATRONI\_ZOOKEEPER\_KEY\_PASSWORD**: (optional) The client key password. - **PATRONI\_ZOOKEEPER\_VERIFY**: (optional) Whether to verify certificate or not. Defaults to ``true``. +- **PATRONI\_ZOOKEEPER\_SET\_ACLS**: (optional) If set, configure Kazoo to apply a default ACL to each ZNode that it creates. ACLs will assume 'x509' schema and should be specified as a dictionary with the principal as the key and one or more permissions as a list in the value. Permissions may be one of ``CREATE``, ``READ``, ``WRITE``, ``DELETE`` or ``ADMIN``. For example, ``set_acls: {CN=principal1: [CREATE, READ], CN=principal2: [ALL]}``. .. note:: It is required to install ``kazoo>=2.6.0`` to support SSL. diff --git a/docs/SETTINGS.rst b/docs/SETTINGS.rst index d304b04c..53227c8b 100644 --- a/docs/SETTINGS.rst +++ b/docs/SETTINGS.rst @@ -182,6 +182,7 @@ ZooKeeper - **key**: (optional) File with the client key. - **key_password**: (optional) The client key password. - **verify**: (optional) Whether to verify certificate or not. Defaults to ``true``. +- **set_acls**: (optional) If set, configure Kazoo to apply a default ACL to each ZNode that it creates. ACLs will assume 'x509' schema and should be specified as a dictionary with the principal as the key and one or more permissions as a list in the value. Permissions may be one of ``CREATE``, ``READ``, ``WRITE``, ``DELETE`` or ``ADMIN``. For example, ``set_acls: {CN=principal1: [CREATE, READ], CN=principal2: [ALL]}``. .. note:: It is required to install ``kazoo>=2.6.0`` to support SSL. diff --git a/patroni/config.py b/patroni/config.py index d265c511..2161a7c6 100644 --- a/patroni/config.py +++ b/patroni/config.py @@ -352,13 +352,13 @@ class Config(object): 'CACERT', 'CERT', 'KEY', 'VERIFY', 'TOKEN', 'CHECKS', 'DC', 'CONSISTENCY', 'REGISTER_SERVICE', 'SERVICE_CHECK_INTERVAL', 'NAMESPACE', 'CONTEXT', 'USE_ENDPOINTS', 'SCOPE_LABEL', 'ROLE_LABEL', 'POD_IP', 'PORTS', 'LABELS', - 'BYPASS_API_SERVICE', 'KEY_PASSWORD', 'USE_SSL') and name: + 'BYPASS_API_SERVICE', 'KEY_PASSWORD', 'USE_SSL', 'SET_ACLS') and name: value = os.environ.pop(param) if suffix == 'PORT': value = value and parse_int(value) elif suffix in ('HOSTS', 'PORTS', 'CHECKS'): value = value and _parse_list(value) - elif suffix == 'LABELS': + elif suffix in ('LABELS', 'SET_ACLS'): value = _parse_dict(value) elif suffix in ('USE_PROXIES', 'REGISTER_SERVICE', 'USE_ENDPOINTS', 'BYPASS_API_SERVICE', 'VERIFY'): value = parse_bool(value) diff --git a/patroni/dcs/zookeeper.py b/patroni/dcs/zookeeper.py index f3b9ac38..9cbff65e 100644 --- a/patroni/dcs/zookeeper.py +++ b/patroni/dcs/zookeeper.py @@ -7,6 +7,7 @@ from kazoo.client import KazooClient, KazooState, KazooRetry from kazoo.exceptions import NoNodeError, NodeExistsError, SessionExpiredError from kazoo.handlers.threading import SequentialThreadingHandler from kazoo.protocol.states import KeeperState +from kazoo.security import make_acl from . import AbstractDCS, ClusterConfig, Cluster, Failover, Leader, Member, SyncState, TimelineHistory from ..exceptions import DCSError @@ -83,6 +84,19 @@ class ZooKeeper(AbstractDCS): 'cert': 'certfile', 'key': 'keyfile', 'key_password': 'keyfile_password'} kwargs = {v: config[k] for k, v in mapping.items() if k in config} + if 'set_acls' in config: + kwargs['default_acl'] = [] + for principal, permissions in config['set_acls'].items(): + normalizedPermissions = [p.upper() for p in permissions] + kwargs['default_acl'].append(make_acl(scheme='x509', + credential=principal, + read='READ' in normalizedPermissions, + write='WRITE' in normalizedPermissions, + create='CREATE' in normalizedPermissions, + delete='DELETE' in normalizedPermissions, + admin='ADMIN' in normalizedPermissions, + all='ALL' in normalizedPermissions)) + self._client = PatroniKazooClient(hosts, handler=PatroniSequentialThreadingHandler(config['retry_timeout']), timeout=config['ttl'], connection_retry=KazooRetry(max_delay=1, max_tries=-1, sleep_func=time.sleep), command_retry=KazooRetry(max_delay=1, max_tries=-1,