When your application needs to access ApsaraDB for Redis, you can store the passwords of ApsaraDB for Redis database accounts in the ApsaraDB for Redis secrets managed by Key Management Service (KMS). Business applications dynamically retrieve the account passwords from KMS by integrating Alibaba Cloud SDKs, KMS SDKs, or secret SDKs. You can configure secret rotation to reduce the risks of account password leaks.
Feature description
When you manage ApsaraDB for Redis database accounts in KMS, you do not need to configure static database accounts in applications. After you create an ApsaraDB for Redis secret for an account of an ApsaraDB for Redis database in KMS, applications can call the GetSecretValue operation to retrieve the secret to access the ApsaraDB for Redis database.
For example, if the name of the ApsaraDB for Redis secret that you create in KMS is username, KMS creates the username and username_clone accounts on the ApsaraDB for Redis instance and uses the accounts to access the ApsaraDB for Redis instance. Compared with the single-account mode, the dual-account mode ensures higher availability and security for your applications. You can configure account rotation policies in the KMS console. By default, KMS performs account rotation every 24 hours. Different accounts are used to log on to the ApsaraDB for Redis instance to improve security. For more information, see Manage and use ApsaraDB for Redis secrets.
Do not modify or delete account passwords in ApsaraDB for Redis to prevent service failures.
Limits
You cannot change the password of a managed account created by KMS in the ApsaraDB for Redis console. However, you can change the password by manually rotating the password or configuring an automatic rotation policy in the KMS console. For more information, see Rotate an ApsaraDB for Redis secret.
You cannot delete a managed account created by KMS in the ApsaraDB for Redis console. To delete a managed account, go to the KMS console. For more information, see Delete an ApsaraDB for Redis secret.
You cannot modify the description of a managed account created by KMS in the ApsaraDB for Redis console.
Prerequisites
An Elastic Compute Service (ECS) instance is created to connect to the ApsaraDB for Redis instance that you want to manage. In the example, the operating system of the ECS instance is Alibaba Cloud Linux 3.2104 LTS 64-bit, and Java 1.8.0 is installed on the ECS instance.
If you use a Resource Access Management (RAM) user or RAM role to manage ApsaraDB for Redis secrets, you must attach the AliyunKMSSecretAdminAccess system policy to the role. For more information, see Grant permissions to a RAM user.
Procedure
Purchase and enable a KMS instance. For more information, see Purchase and enable a KMS instance.
When you create a KMS instance, select the same virtual private cloud (VPC) as the ECS instance.
If you already created a KMS instance, add the VPC of the ECS instance to the KMS instance. For more information, see Access a KMS instance from multiple VPCs in the same region.
Create an application access point (AAP). For more information, see Create an AAP.
After an AAP is created, the browser automatically downloads the client key information. The client key information includes the content and password of the client key. The client key content is stored in a JSON file. Keep the information confidential.
Download the Certificate Authority (CA) certificate of the KMS instance on the Instances page in the KMS console. For more information, see Obtain the CA certificate of a KMS instance.
Create a customer master key (CMK). For more information, see Getting started with keys.
Create an ApsaraDB for Redis secret. For more information, see Create an ApsaraDB for Redis secret.
Write Java test code.
Add Maven dependencies to your project. Then, a Java package is automatically downloaded from the Maven repository. You can also add the following <build> section to package the project dependencies into a single JAR file.
<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>alibabacloud-dkms-gcs-sdk</artifactId> <version>0.5.2</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>tea</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.10</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.9</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <mainClass> com.aliyun.KMSJedisTest </mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>assemble-all</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
Write code to implement the KMSJedisTest.java main class.
NoteIn this example, the credentialCacheTime parameter is added to prevent the system from retrieving the password from KMS every time a new connection is established. The default value of the credentialCacheTime parameter is 600 seconds. During the cache time, the cached password is returned without accessing KMS. You can call the setCredentialCacheTime operation to adjust the cache duration. We recommend that you set the cache duration to at least 10 minutes.
package com.aliyun; import java.time.Duration; import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; public class KMSJedisTest { public static void main(String[] args) throws Exception { if (args.length < 2) { System.out.println( "Please input kmsEndpoint, clientKeyFilePath, clientKeyPass, caCertPath, secretName, redisHost"); return; } String endpoint = args[0]; String clientKeyFilePath = args[1]; String clientKeyPass = args[2]; String caCertPath = args[3]; String secretName = args[4]; KMSRedisCredentialsProvider kmsRedisCredentialsProvider = new KMSRedisCredentialsProvider(endpoint, clientKeyFilePath, clientKeyPass, caCertPath, secretName); kmsRedisCredentialsProvider.setCredentialCacheTime(Duration.ofSeconds(10)); // Specify a cache duration to prevent frequent requests to KMS. String redisHost = args[5]; JedisPool jedisPool = new JedisPool(HostAndPort.from(redisHost), DefaultJedisClientConfig.builder().credentialsProvider(kmsRedisCredentialsProvider).build()); for (int i = 0; i < Integer.MAX_VALUE; i++) { Thread.sleep(1000); try (Jedis jedis = jedisPool.getResource()) { System.out.println(jedis.set("" + i, "" + i)); System.out.println(jedis.get("" + i)); } catch (Exception e) { System.out.println(e); } } } }
Write code to implement the KMSRedisCredentialsProvider.java class.
package com.aliyun; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.DefaultRedisCredentials; import redis.clients.jedis.RedisCredentials; import redis.clients.jedis.RedisCredentialsProvider; import com.aliyun.dkms.gcs.openapi.models.Config; import com.aliyun.dkms.gcs.sdk.Client; import com.aliyun.dkms.gcs.sdk.models.*; public class KMSRedisCredentialsProvider implements RedisCredentialsProvider { private static final Logger logger = LoggerFactory.getLogger(KMSRedisCredentialsProvider.class); private final String endpoint; private final String clientKeyFilePath; private final String clientKeyPass; private final String caCertPath; private final String secretName; private static Client client = null; // credential cache time private Duration credentialCacheTime = Duration.ofSeconds(600); private DefaultRedisCredentials cachedCredentials = null; private LocalDateTime credentialsExpiration = null; public KMSRedisCredentialsProvider(String endpoint, String clientKeyFilePath, String clientKeyPass, String caCertPath, String secretName) { this.endpoint = endpoint; this.clientKeyFilePath = clientKeyFilePath; this.clientKeyPass = clientKeyPass; this.caCertPath = caCertPath; this.secretName = secretName; createClientInstance(endpoint, clientKeyFilePath, clientKeyPass, caCertPath); } public void setCredentialCacheTime(Duration credentialCacheTime) { this.credentialCacheTime = credentialCacheTime; } private static synchronized void createClientInstance(String endpoint, String clientKeyFilePath, String clientKeyPass, String caCertPath) { if (client == null) { try { client = new Client(new Config() .setProtocol("https") .setEndpoint(endpoint) .setCaFilePath(caCertPath) .setClientKeyFile(clientKeyFilePath) .setPassword(clientKeyPass)); } catch (Exception e) { logger.error("Init kms client failed", e); throw new RuntimeException(e); } } } @Override public RedisCredentials get() { try { LocalDateTime now = LocalDateTime.now(); // Check cache if (cachedCredentials != null && now.isBefore(credentialsExpiration)) { return cachedCredentials; } GetSecretValueRequest request = new GetSecretValueRequest().setSecretName(secretName); GetSecretValueResponse getSecretValueResponse = client.getSecretValue(request); logger.debug("Now: " + now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + ", getSecretValueRequest: " + request); String secretData = getSecretValueResponse.getSecretData(); JSONObject secretObject = new JSONObject(secretData); if (secretObject.get("AccountName") == null || secretObject.get("AccountPassword") == null) { throw new IllegalArgumentException("secretData must contain AccountName and AccountPassword"); } cachedCredentials = new DefaultRedisCredentials(secretObject.get("AccountName").toString(), secretObject.get("AccountPassword").toString()); credentialsExpiration = now.plusSeconds(credentialCacheTime.getSeconds()); return cachedCredentials; } catch (Exception e) { logger.error("get secret failed", e); throw new RuntimeException(e); } } @Override public void prepare() { // do nothing } @Override public void cleanUp() { // do nothing } }
Run the
mvn package
command to package the entire project into a JAR file.
On the ECS instance, use the SDK for Java to connect to the ApsaraDB for Redis instance.
Sample syntax:
java -jar <kms-redis-jar-with-dependencies.jar> <kmsEndpoint> <clientKeyFilePath> <clientKeyPass> <caCertPath> <secretName> <redisHost>
Parameters:
kms-redis-jar-with-dependencies.jar: the JAR file generated from the Maven packaging process. Make sure that you use the JAR file that has the jar-with-dependencies suffix.
kmsEndpoint: the VPC endpoint of the KMS instance. You can obtain the endpoint on the KMS instance details page.
clientKeyFilePath: the path to the client key file. This is the JSON file downloaded in Step 2.
clientKeyPass: the password for the client key. The password is stored in the TXT file downloaded in Step 2.
caCertPath: the CA certificate of the KMS instance, which is the PEM file downloaded in Step 3.
secretName: the name of the ApsaraDB for Redis secret created in Step 5.
redisHost: the VPC endpoint and port number of the ApsaraDB for Redis instance, such as r-bp1g727yrai5yh****.redis.rds.aliyuncs.com:6379.
Example:
java -jar kms-redis-samples-1.0-SNAPSHOT-jar-with-dependencies.jar kst-hzz6674e7fbw21x9x****.cryptoservice.kms.aliyuncs.com /root/clientKey_KAAP.6432ddc6-f23a-4d78-ac84-****4598206b.json 267d1****1cda4415058e1d72ec49e0a /root/PrivateKmsCA_kst-hzz6674e7fbw21x9x****.pem kms-redis r-bp1g727yrai5yh****.redis.rds.aliyuncs.com:6379
The following output is returned, which indicates that the connection is established:
0 OK 1 OK 2 OK 3 OK 4 OK
Immediately rotate the secret in the KMS console. For more information, see Rotate an ApsaraDB for Redis secret.
During the rotation, KMS uses another account (username or username_clone) to access the ApsaraDB for Redis instance.
If the ECS instance can connect to the ApsaraDB for Redis instance, the ApsaraDB for Redis password rotation feature works as expected.
... 30 OK 31 OK 32 OK 33 OK
Perform a master-slave switchover or high-availability (HA) switchover on the ApsaraDB for Redis instance and check the status of the client.
The following output is returned, which indicates that a transient connection occurs during the HA switchover, and the KMS instance updates the secret and reconnects to the ApsaraDB for Redis instance:
138 OK 139 redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream. OK 142 OK 143 OK