RocketMQ, as an excellent middleware, has a wide range of applications. There are large-scale applications in different fields such as finance, e-commerce, telecommunications, medical, social science, security and so on. There is no doubt that the security is very questionable, because there is no security-related business module in RocketMQ, and the sending and consumption of messages can not be well controlled. It requires the business side to encapsulate the security module itself, which increases the cost of use. ACL privilege control has been added to the upgrade of RocketMQ 4.4.0. The improvement of this function directly promotes the promotion and use of RocketMQ in various fields, especially in areas with high security requirements such as finance, e-commerce and security.
1. Simple Use
1.1. What is ACL?
ACL is short for access control list, commonly known as access control list. Access control basically involves the concepts of users, resources, privileges, roles and so on. What objects do the above-mentioned objects correspond to in RocketMQ?
User: User is the basic element of access control, RocketMQ ACL will inevitably introduce the concept of user, that is, support user name and password. Resources: The objects to be protected, Topic involved in message sending and consumption groups involved in message consumption should be protected, so they can be abstracted into resources. Privileges: Operations that can be performed for resources. Roles: In RocketMQ, only two roles are defined: whether they are administrators or not.
1.2. Configuring ACL in RocketMQ
Acl default configuration file name: plain_acl.yml, which needs to be placed in the ${ROCKETMQ_HOME}/store/config directory
To use acl, you must turn on this function on the server side, configure it in Broker's configuration file, and aclEnable = true turns on this function
Configure plain_acl.yml file
globalWhiteRemoteAddresses: - 10.10.15.* - 192.168.0.* accounts: - accessKey: RocketMQ secretKey: 12345678 whiteRemoteAddress: admin: false defaultTopicPerm: DENY defaultGroupPerm: SUB topicPerms: - topicA=DENY - topicB=PUB|SUB - topicC=SUB groupPerms: # the group should convert to retry topic - groupA=DENY - groupB=PUB|SUB - groupC=SUB - accessKey: rocketmq2 secretKey: 12345678 whiteRemoteAddress: 192.168.1.* # if it is admin, it could access all resources admin: true
Let's introduce the meaning and use of the parameters in plain_acl.yml file.
field | Value | Meaning |
---|---|---|
globalWhiteRemoteAddresses | *;192.168.*.*;192.168.0.1 | Global IP Whitelist |
accessKey | Character string | Access Key User Name |
secretKey | Character string | Secret Key password |
whiteRemoteAddress | *;192.168.*.*;192.168.0.1 | User IP Whitelist |
admin | true;false | Administrator Account |
defaultTopicPerm | DENY;PUB;SUB;PUB|SUB | Default Topic permissions |
defaultGroupPerm | DENY;PUB;SUB;PUB|SUB | Default ConstumerGroup permissions |
topicPerms | topic = permission | Permissions of each Topic |
groupPerms | group = permission | Permissions of individual Consumer Groups |
The Meaning of Permission Identifier
Jurisdiction | Meaning |
---|---|
DENY | refuse |
ANY | PUB or SUB permissions |
PUB | Send permission |
SUB | Subscription rights |
Processing flow
Special requests such as UPDATE_AND_CREATE_TOPIC can only be operated by admin accounts.
For a resource, if there are explicit configuration privileges, the configuration privileges are used; if there are no explicit configuration privileges, the default privileges are used.
The default implementation of RocketMQ's permission control storage is based on the yml configuration file. Users can dynamically modify the properties defined by permission control without restarting Broker service nodes
If ACL is enabled at the same time as High Availability Deployment (Master/Slave Architecture), then global whitelist information needs to be set in the ${ROCKETMQ_HOME}/store/conf/plain_acl.yml configuration file of Broker Master node, that is, to set the ip address of Slave node to the global whitelist of Master node plain_acl.yml configuration file.
1.3. Code examples
1.3.1, Producer Code
public class AclProducer { public static void main(String[] args) throws MQClientException, InterruptedException { DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name", getAclRPCHook()); producer.setNamesrvAddr("10.10.15.246:9876;10.10.15.247:9876"); producer.start(); for (int i = 0; i < 10; i++) { try { Message msg = new Message("topicA" ,"TagA" , ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } catch (Exception e) { e.printStackTrace(); Thread.sleep(1000); } } producer.shutdown(); } static RPCHook getAclRPCHook() { return new AclClientRPCHook(new SessionCredentials("RocketMQ","12345678")); } }
View the results
Error report indicates that topicA does not have permission. What we configure in plain_acl.yml file is RocketMQ user rejection, production and consumption topicA topic information. If we change the theme to topicB, we find that the message is sent successfully. topicB=PUB|SUB sets permission is production and consumption.
View the results
1.3.2, Consumer Code
public class AclConsumer { public static void main(String[] args) throws InterruptedException, MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("groupA", getAclRPCHook(),new AllocateMessageQueueAveragely()); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); consumer.subscribe("topicB", "*"); consumer.setNamesrvAddr("10.10.15.246:9876;10.10.15.247:9876"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } static RPCHook getAclRPCHook() { return new AclClientRPCHook(new SessionCredentials("RocketMQ","12345678")); } }
View results: We found that no message was consumed and no error message was reported. For RocketMQ users topicB settings, it can be produced and consumed, but we found that its groupA=DENY is rejected, indicating that the consumer group is groupA and refuses to consume any message. We changed to groupB or groupC to view the results.
2. Source code analysis
Broker ACL schematic
2.1. ACL-related operations during Broker initialization
Initial acl () is called when the Broker service is created at startup and initialize.
private void initialAcl() { //Whether ACL function is turned on in broker configuration file and turned off by default if (!this.brokerConfig.isAclEnable()) { log.info("The broker dose not enable acl"); return; } //Get a list of access validators and point to the loaded META-INF/service/org.apache.rocketmq.acl.AccessValidator file //org.apache.rocketmq.acl.plain.PlainAccessValidator, default only one List<AccessValidator> accessValidators = ServiceProvider.load(ServiceProvider.ACL_VALIDATOR_ID, AccessValidator.class); if (accessValidators == null || accessValidators.isEmpty()) { log.info("The broker dose not load the AccessValidator"); return; } for (AccessValidator accessValidator: accessValidators) { final AccessValidator validator = accessValidator; //Register the "hook" object on the server and verify the privileges this.registerServerRPCHook(new RPCHook() { @Override public void doBeforeRequest(String remoteAddr, RemotingCommand request) { //Do not catch the exception validator.validate(validator.parse(request, remoteAddr)); } @Override public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) { } }); } }
There are annotations in the source code. Let's take a look at the registerServer RPCHook method
public void registerServerRPCHook(RPCHook rpcHook) { //Service-side Netty Remoting Server Service Registration "hook" function getRemotingServer().registerRPCHook(rpcHook); this.fastRemotingServer.registerRPCHook(rpcHook); }
With regard to the use of Netty Remoting Server and Netty Remoting Client services, RocketMQ Remoting will focus on the following chapters
2.2. Plain Access Validator Privilege Verifier
PlainAccessValidator.parse(), which requires different inspection resources depending on the client's request for Code
switch (request.getCode()) { //Sending a message requires verifying that the top of the current account has PUB privileges case RequestCode.SEND_MESSAGE: accessResource.addResourceAndPerm(request.getExtFields().get("topic"), Permission.PUB); break; case RequestCode.SEND_MESSAGE_V2: accessResource.addResourceAndPerm(request.getExtFields().get("b"), Permission.PUB); break; case RequestCode.CONSUMER_SEND_MSG_BACK: accessResource.addResourceAndPerm(request.getExtFields().get("originTopic"), Permission.PUB); accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get("group")), Permission.SUB); break; //When pulling out interest, you need to know whether the top pulled down by the consumer account has SUB privileges, and whether the subscription group consumerGroup has subprivileges. case RequestCode.PULL_MESSAGE: accessResource.addResourceAndPerm(request.getExtFields().get("topic"), Permission.SUB); accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get("consumerGroup")), Permission.SUB); break; case RequestCode.QUERY_MESSAGE: accessResource.addResourceAndPerm(request.getExtFields().get("topic"), Permission.SUB); break; case RequestCode.HEART_BEAT: HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class); for (ConsumerData data : heartbeatData.getConsumerDataSet()) { accessResource.addResourceAndPerm(getRetryTopic(data.getGroupName()), Permission.SUB); for (SubscriptionData subscriptionData : data.getSubscriptionDataSet()) { accessResource.addResourceAndPerm(subscriptionData.getTopic(), Permission.SUB); } } break; case RequestCode.UNREGISTER_CLIENT: final UnregisterClientRequestHeader unregisterClientRequestHeader = (UnregisterClientRequestHeader) request .decodeCommandCustomHeader(UnregisterClientRequestHeader.class); accessResource.addResourceAndPerm(getRetryTopic(unregisterClientRequestHeader.getConsumerGroup()), Permission.SUB); break; case RequestCode.GET_CONSUMER_LIST_BY_GROUP: final GetConsumerListByGroupRequestHeader getConsumerListByGroupRequestHeader = (GetConsumerListByGroupRequestHeader) request .decodeCommandCustomHeader(GetConsumerListByGroupRequestHeader.class); accessResource.addResourceAndPerm(getRetryTopic(getConsumerListByGroupRequestHeader.getConsumerGroup()), Permission.SUB); break; case RequestCode.UPDATE_CONSUMER_OFFSET: final UpdateConsumerOffsetRequestHeader updateConsumerOffsetRequestHeader = (UpdateConsumerOffsetRequestHeader) request .decodeCommandCustomHeader(UpdateConsumerOffsetRequestHeader.class); accessResource.addResourceAndPerm(getRetryTopic(updateConsumerOffsetRequestHeader.getConsumerGroup()), Permission.SUB); accessResource.addResourceAndPerm(updateConsumerOffsetRequestHeader.getTopic(), Permission.SUB); break; default: break; }
Get the set of privilege identifiers required by the current operation according to request.getCode() for later verification of the privilege identifiers in the system's privilege configuration file plain_acl.yml
2.3. Plain Permission Loader Resource Loader
Broker created Plain Access Validator when initializing related services, and we found that its permission resource loader Plain Permission Loader was invoked in its default constructor.
public PlainAccessValidator() { aclPlugEngine = new PlainPermissionLoader(); }
Create PlainPermissionLoader objects
public PlainPermissionLoader() { //Load the server-side permission file plain_acl.yml load(); //Open threads check permission files every 500 ms to see if they have changed. If they have changed, load () is executed to load new permission files. watch(); }
View the load method flow
public void load() { Map<String, PlainAccessResource> plainAccessResourceMap = new HashMap<>(); List<RemoteAddressStrategy> globalWhiteRemoteAddressStrategy = new ArrayList<>(); JSONObject plainAclConfData = AclUtils.getYamlDataObject(fileHome + File.separator + fileName, JSONObject.class); if (plainAclConfData == null || plainAclConfData.isEmpty()) { throw new AclException(String.format("%s file is not data", fileHome + File.separator + fileName)); } log.info("Broker plain acl conf data is : ", plainAclConfData.toString()); //Get the global whitelist IP set JSONArray globalWhiteRemoteAddressesList = plainAclConfData.getJSONArray("globalWhiteRemoteAddresses"); if (globalWhiteRemoteAddressesList != null && !globalWhiteRemoteAddressesList.isEmpty()) { for (int i = 0; i < globalWhiteRemoteAddressesList.size(); i++) { globalWhiteRemoteAddressStrategy.add(remoteAddressStrategyFactory. getRemoteAddressStrategy(globalWhiteRemoteAddressesList.getString(i))); } } //Get the set of account permissions JSONArray accounts = plainAclConfData.getJSONArray("accounts"); if (accounts != null && !accounts.isEmpty()) { List<PlainAccessConfig> plainAccessConfigList = accounts.toJavaList(PlainAccessConfig.class); for (PlainAccessConfig plainAccessConfig : plainAccessConfigList) { //Building permission resources for each account PlainAccessResource plainAccessResource = buildPlainAccessResource(plainAccessConfig); //Put AccessKey in Map as key and the permission resource of the account as value plainAccessResourceMap.put(plainAccessResource.getAccessKey(),plainAccessResource); } } this.globalWhiteRemoteAddressStrategy = globalWhiteRemoteAddressStrategy; this.plainAccessResourceMap = plainAccessResourceMap; }
Load the resource file, parse the privilege identity, and wait for the privilege verifier PlainAccessValidator to call its validate() to verify the privilege
2.4. Privilege Checking Process
The core verification method Plain PermissionLoader. validate ()
public void validate(PlainAccessResource plainAccessResource) { //Global White List IP Check for (RemoteAddressStrategy remoteAddressStrategy : globalWhiteRemoteAddressStrategy) { //The matching success statement is a global whitelist IP with all privileges and returns directly. if (remoteAddressStrategy.match(plainAccessResource)) { return; } } //To determine whether the username is empty, null throws an AclException exception if (plainAccessResource.getAccessKey() == null) { throw new AclException(String.format("No accessKey is configured")); } //Check whether the account exists in plain_acl.yml in the server-side permission resource file and throw an exception if not if (!plainAccessResourceMap.containsKey(plainAccessResource.getAccessKey())) { throw new AclException(String.format("No acl config for %s", plainAccessResource.getAccessKey())); } PlainAccessResource ownedAccess = plainAccessResourceMap.get(plainAccessResource.getAccessKey()); //Check whether the whitelist IP of this account matches the upper client IP. The matching is successful with all permissions. Administrator permissions are required except for special permissions such as UPDATE_AND_CREATE_TOPIC. if (ownedAccess.getRemoteAddressStrategy().match(plainAccessResource)) { return; } //Check signature String signature = AclUtils.calSignature(plainAccessResource.getContent(), ownedAccess.getSecretKey()); if (!signature.equals(plainAccessResource.getSignature())) { throw new AclException(String.format("Check signature failed for accessKey=%s", plainAccessResource.getAccessKey())); } //Check resource permissions in accounts checkPerm(plainAccessResource, ownedAccess); }
View its resource validation within the current account
void checkPerm(PlainAccessResource needCheckedAccess, PlainAccessResource ownedAccess) { //Determine whether the Code of the requested command requires administrator privileges and whether the user is an administrator if (Permission.needAdminPerm(needCheckedAccess.getRequestCode()) && !ownedAccess.isAdmin()) { throw new AclException(String.format("Need admin permission for request code=%d, but accessKey=%s is not", needCheckedAccess.getRequestCode(), ownedAccess.getAccessKey())); } Map<String, Byte> needCheckedPermMap = needCheckedAccess.getResourcePermMap(); Map<String, Byte> ownedPermMap = ownedAccess.getResourcePermMap(); if (needCheckedPermMap == null) { // If the needCheckedPermMap is null,then return return; } for (Map.Entry<String, Byte> needCheckedEntry : needCheckedPermMap.entrySet()) { String resource = needCheckedEntry.getKey(); Byte neededPerm = needCheckedEntry.getValue(); //To determine whether it is a group, when building resourcePermMap, the key of the group = RETRY_GROUP_TOPIC_PREFIX + consumerGroup boolean isGroup = PlainAccessResource.isRetryTopic(resource); //The configuration item package in the permission profile of the system does not contain the permissions required for the client command request if (!ownedPermMap.containsKey(resource)) { //Determine whether it is a top or group privilege identifier, and what is the global privilege of this type? byte ownedPerm = isGroup ? needCheckedAccess.getDefaultGroupPerm() : needCheckedAccess.getDefaultTopicPerm(); //Check permissions if (!Permission.checkPermission(neededPerm, ownedPerm)) { throw new AclException(String.format("No default permission for %s", PlainAccessResource.printStr(resource, isGroup))); } continue; } //If the configuration item in the permission profile of the system contains the permissions required by the client's command request, the permissions will be judged directly. if (!Permission.checkPermission(neededPerm, ownedPermMap.get(resource))) { throw new AclException(String.format("No default permission for %s", PlainAccessResource.printStr(resource, isGroup))); } } }
All inspection processes throw AclException exceptions if one is not satisfied
2.5. Client sends request
The above figure only analyses the process flow of Broker server, and how the client calls us for specific analysis. Let's take sending a message as an example.
The core method of message sending that we have analyzed before is DefaultMQ Producer Impl. sendKernelImpl () method.
//Are "hooks" registered? if (this.hasSendMessageHook()) { context = new SendMessageContext(); context.setProducer(this); context.setProducerGroup(this.defaultMQProducer.getProducerGroup()); context.setCommunicationMode(communicationMode); context.setBornHost(this.defaultMQProducer.getClientIP()); context.setBrokerAddr(brokerAddr); context.setMessage(msg); context.setMq(mq); String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); if (isTrans != null && isTrans.equals("true")) { context.setMsgType(MessageType.Trans_Msg_Half); } if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) { context.setMsgType(MessageType.Delay_Msg); } //Encapsulate the parameter information of its ACL request this.executeSendMessageHookBefore(context); }
hasSendMessageHook(), which we created when we built the Producer, is added to the sendMessageHookList attribute of DefaultMQ Producer Impl.
Let's look at the data preparation before calling AclClientRPCHook.doBeforeRequest() in its sending message NettyRemotingClient class
public void doBeforeRequest(String remoteAddr, RemotingCommand request) { byte[] total = AclUtils.combineRequestContent(request, parseRequestContent(request, sessionCredentials.getAccessKey(), sessionCredentials.getSecurityToken())); String signature = AclUtils.calSignature(total, sessionCredentials.getSecretKey()); request.addExtField(SIGNATURE, signature); request.addExtField(ACCESS_KEY, sessionCredentials.getAccessKey()); // The SecurityToken value is unneccessary,user can choose this one. if (sessionCredentials.getSecurityToken() != null) { request.addExtField(SECURITY_TOKEN, sessionCredentials.getSecurityToken()); } }
Just build signature s and Token s, ready to change data for Broker-side authentication.