Skip to content

Commit 1ad64cf

Browse files
committed
Send cross cluster api key signature as headers
1 parent b007f4f commit 1ad64cf

File tree

23 files changed

+761
-107
lines changed

23 files changed

+761
-107
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9187000
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
esql_plan_with_no_columns,9186000
1+
add_cross_cluster_api_key_signature,9187000

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CrossClusterAccessSubjectInfo.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,6 @@ public void writeToContext(final ThreadContext ctx) throws IOException {
7676
ctx.putHeader(CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY, encode());
7777
}
7878

79-
public static CrossClusterAccessSubjectInfo readFromContext(final ThreadContext ctx) throws IOException {
80-
final String header = ctx.getHeader(CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY);
81-
if (header == null) {
82-
throw new IllegalArgumentException(
83-
"cross cluster access header [" + CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY + "] is required"
84-
);
85-
}
86-
return decode(header);
87-
}
88-
8979
public Authentication getAuthentication() {
9080
return authentication;
9181
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/CrossClusterAccessSubjectInfoTests.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ public void testWriteReadContextRoundtrip() throws IOException {
4848
);
4949

5050
expectedCrossClusterAccessSubjectInfo.writeToContext(ctx);
51-
final CrossClusterAccessSubjectInfo actual = CrossClusterAccessSubjectInfo.readFromContext(ctx);
51+
final CrossClusterAccessSubjectInfo actual = CrossClusterAccessSubjectInfo.decode(
52+
ctx.getHeader(CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY)
53+
);
5254

5355
assertThat(actual.getAuthentication(), equalTo(expectedCrossClusterAccessSubjectInfo.getAuthentication()));
5456
final List<Set<RoleDescriptor>> roleDescriptorsList = new ArrayList<>();
@@ -70,17 +72,6 @@ public void testRoleDescriptorsBytesToRoleDescriptors() throws IOException {
7072
assertThat(actualRoleDescriptors, equalTo(expectedRoleDescriptors));
7173
}
7274

73-
public void testThrowsOnMissingEntry() {
74-
var actual = expectThrows(
75-
IllegalArgumentException.class,
76-
() -> CrossClusterAccessSubjectInfo.readFromContext(new ThreadContext(Settings.EMPTY))
77-
);
78-
assertThat(
79-
actual.getMessage(),
80-
equalTo("cross cluster access header [" + CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY + "] is required")
81-
);
82-
}
83-
8475
public void testCleanWithValidationForApiKeys() {
8576
final Map<String, Object> initialMetadata = newHashMapWithRandomMetadata();
8677
final AuthenticationTestHelper.AuthenticationTestBuilder builder = AuthenticationTestHelper.builder()

x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityBWCToRCS2ClusterRestIT.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import org.elasticsearch.test.cluster.ElasticsearchCluster;
1111
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
12+
import org.elasticsearch.test.cluster.util.resource.Resource;
1213
import org.junit.Before;
1314
import org.junit.ClassRule;
1415
import org.junit.rules.RuleChain;
@@ -54,6 +55,10 @@ public class RemoteClusterSecurityBWCToRCS2ClusterRestIT extends AbstractRemoteC
5455
.apply(commonClusterConfig)
5556
.setting("xpack.security.remote_cluster_client.ssl.enabled", "true")
5657
.setting("xpack.security.remote_cluster_client.ssl.certificate_authorities", "remote-cluster-ca.crt")
58+
.configFile("signing.crt", Resource.fromClasspath("signing/signing.crt"))
59+
.setting("cluster.remote.my_remote_cluster.signing.certificate", "signing.crt")
60+
.configFile("signing.key", Resource.fromClasspath("signing/signing.key"))
61+
.setting("cluster.remote.my_remote_cluster.signing.key", "signing.key")
5762
.keystore("cluster.remote.my_remote_cluster.credentials", () -> {
5863
if (API_KEY_MAP_REF.get() == null) {
5964
final Map<String, Object> apiKeyMap = createCrossClusterAccessApiKey("""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.remotecluster;
9+
10+
import io.netty.handler.codec.http.HttpMethod;
11+
12+
import org.elasticsearch.action.search.SearchResponse;
13+
import org.elasticsearch.client.Request;
14+
import org.elasticsearch.client.RequestOptions;
15+
import org.elasticsearch.client.Response;
16+
import org.elasticsearch.client.ResponseException;
17+
import org.elasticsearch.common.settings.Settings;
18+
import org.elasticsearch.core.Strings;
19+
import org.elasticsearch.search.SearchHit;
20+
import org.elasticsearch.search.SearchResponseUtils;
21+
import org.elasticsearch.test.cluster.ElasticsearchCluster;
22+
import org.elasticsearch.test.cluster.util.resource.Resource;
23+
import org.junit.ClassRule;
24+
import org.junit.rules.RuleChain;
25+
import org.junit.rules.TestRule;
26+
27+
import java.io.IOException;
28+
import java.util.Arrays;
29+
import java.util.List;
30+
import java.util.Locale;
31+
import java.util.Map;
32+
import java.util.concurrent.atomic.AtomicReference;
33+
import java.util.stream.Collectors;
34+
35+
import static org.hamcrest.Matchers.containsInAnyOrder;
36+
import static org.hamcrest.Matchers.containsString;
37+
import static org.hamcrest.Matchers.equalTo;
38+
39+
public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRemoteClusterSecurityTestCase {
40+
41+
private static final AtomicReference<Map<String, Object>> API_KEY_MAP_REF = new AtomicReference<>();
42+
43+
static {
44+
fulfillingCluster = ElasticsearchCluster.local()
45+
.name("fulfilling-cluster")
46+
.apply(commonClusterConfig)
47+
.setting("remote_cluster_server.enabled", "true")
48+
.setting("remote_cluster.port", "0")
49+
.setting("xpack.security.remote_cluster_server.ssl.enabled", "true")
50+
.setting("xpack.security.remote_cluster_server.ssl.key", "remote-cluster.key")
51+
.setting("xpack.security.remote_cluster_server.ssl.certificate", "remote-cluster.crt")
52+
.configFile("signing_ca.crt", Resource.fromClasspath("signing/root.crt"))
53+
.setting("cluster.remote.signing.certificate_authorities", "signing_ca.crt")
54+
.keystore("xpack.security.remote_cluster_server.ssl.secure_key_passphrase", "remote-cluster-password")
55+
.build();
56+
57+
queryCluster = ElasticsearchCluster.local()
58+
.name("query-cluster")
59+
.apply(commonClusterConfig)
60+
.setting("xpack.security.remote_cluster_client.ssl.enabled", "true")
61+
.setting("xpack.security.remote_cluster_client.ssl.certificate_authorities", "remote-cluster-ca.crt")
62+
.configFile("signing.crt", Resource.fromClasspath("signing/signing.crt"))
63+
.setting("cluster.remote.my_remote_cluster.signing.certificate", "signing.crt")
64+
.configFile("signing.key", Resource.fromClasspath("signing/signing.key"))
65+
.setting("cluster.remote.my_remote_cluster.signing.key", "signing.key")
66+
.keystore("cluster.remote.my_remote_cluster.credentials", () -> {
67+
if (API_KEY_MAP_REF.get() == null) {
68+
final Map<String, Object> apiKeyMap = createCrossClusterAccessApiKey("""
69+
{
70+
"search": [
71+
{
72+
"names": ["index*", "not_found_index"]
73+
}
74+
]
75+
}""");
76+
API_KEY_MAP_REF.set(apiKeyMap);
77+
}
78+
return (String) API_KEY_MAP_REF.get().get("encoded");
79+
})
80+
.keystore("cluster.remote.invalid_remote.credentials", randomEncodedApiKey())
81+
.build();
82+
}
83+
84+
@ClassRule
85+
// Use a RuleChain to ensure that fulfilling cluster is started before query cluster
86+
public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster);
87+
88+
public void testCrossClusterSearchWithCrossClusterApiKeySigning() throws Exception {
89+
indexTestData();
90+
assertCrossClusterSearchSuccessfulWithResult();
91+
92+
// Change the CA to something that doesn't trust the signing cert
93+
updateClusterSettingsFulfillingCluster(
94+
Settings.builder().put("cluster.remote.signing.certificate_authorities", "transport-ca.crt").build()
95+
);
96+
assertCrossClusterAuthFail();
97+
98+
// Update settings on query cluster to ignore unavailable remotes
99+
updateClusterSettings(Settings.builder().put("cluster.remote.my_remote_cluster.skip_unavailable", Boolean.toString(true)).build());
100+
101+
assertCrossClusterSearchSuccessfulWithoutResult();
102+
103+
// TODO add test for certificate identity configured for API key but no signature provided (should 401)
104+
105+
// TODO add test for certificate identity not configured for API key but signature provided (should 200)
106+
107+
// TODO add test for certificate identity not configured for API key but wrong signature provided (should 401)
108+
109+
// TODO add test for certificate identity regex matching (should 200)
110+
}
111+
112+
private void assertCrossClusterAuthFail() {
113+
var responseException = assertThrows(ResponseException.class, () -> simpleCrossClusterSearch(randomBoolean()));
114+
assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(401));
115+
assertThat(responseException.getMessage(), containsString("Failed to verify cross cluster api key signature certificate from [("));
116+
}
117+
118+
private void assertCrossClusterSearchSuccessfulWithoutResult() throws IOException {
119+
boolean alsoSearchLocally = randomBoolean();
120+
final Response response = simpleCrossClusterSearch(alsoSearchLocally);
121+
assertOK(response);
122+
}
123+
124+
private void assertCrossClusterSearchSuccessfulWithResult() throws IOException {
125+
boolean alsoSearchLocally = randomBoolean();
126+
final Response response = simpleCrossClusterSearch(alsoSearchLocally);
127+
assertOK(response);
128+
final SearchResponse searchResponse;
129+
try (var parser = responseAsParser(response)) {
130+
searchResponse = SearchResponseUtils.parseSearchResponse(parser);
131+
}
132+
try {
133+
final List<String> actualIndices = Arrays.stream(searchResponse.getHits().getHits())
134+
.map(SearchHit::getIndex)
135+
.collect(Collectors.toList());
136+
if (alsoSearchLocally) {
137+
assertThat(actualIndices, containsInAnyOrder("index1", "local_index"));
138+
} else {
139+
assertThat(actualIndices, containsInAnyOrder("index1"));
140+
}
141+
} finally {
142+
searchResponse.decRef();
143+
}
144+
}
145+
146+
private Response simpleCrossClusterSearch(boolean alsoSearchLocally) throws IOException {
147+
final var searchRequest = new Request(
148+
"GET",
149+
String.format(
150+
Locale.ROOT,
151+
"/%s%s:%s/_search?ccs_minimize_roundtrips=%s",
152+
alsoSearchLocally ? "local_index," : "",
153+
randomFrom("my_remote_cluster", "*", "my_remote_*"),
154+
randomFrom("index1", "*"),
155+
randomBoolean()
156+
)
157+
);
158+
return performRequestWithRemoteAccessUser(searchRequest);
159+
}
160+
161+
private void indexTestData() throws Exception {
162+
configureRemoteCluster();
163+
164+
// Fulfilling cluster
165+
{
166+
// Index some documents, so we can attempt to search them from the querying cluster
167+
final Request bulkRequest = new Request("POST", "/_bulk?refresh=true");
168+
bulkRequest.setJsonEntity(Strings.format("""
169+
{ "index": { "_index": "index1" } }
170+
{ "foo": "bar" }
171+
{ "index": { "_index": "index2" } }
172+
{ "bar": "foo" }
173+
{ "index": { "_index": "prefixed_index" } }
174+
{ "baz": "fee" }\n"""));
175+
assertOK(performRequestAgainstFulfillingCluster(bulkRequest));
176+
}
177+
178+
// Query cluster
179+
{
180+
// Index some documents, to use them in a mixed-cluster search
181+
final var indexDocRequest = new Request("POST", "/local_index/_doc?refresh=true");
182+
indexDocRequest.setJsonEntity("{\"local_foo\": \"local_bar\"}");
183+
assertOK(client().performRequest(indexDocRequest));
184+
185+
// Create user role with privileges for remote and local indices
186+
final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE);
187+
putRoleRequest.setJsonEntity("""
188+
{
189+
"description": "role with privileges for remote and local indices",
190+
"cluster": ["manage_own_api_key"],
191+
"indices": [
192+
{
193+
"names": ["local_index"],
194+
"privileges": ["read"]
195+
}
196+
],
197+
"remote_indices": [
198+
{
199+
"names": ["index1", "not_found_index", "prefixed_index"],
200+
"privileges": ["read", "read_cross_cluster"],
201+
"clusters": ["my_remote_cluster"]
202+
}
203+
]
204+
}""");
205+
assertOK(adminClient().performRequest(putRoleRequest));
206+
final var putUserRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER);
207+
putUserRequest.setJsonEntity("""
208+
{
209+
"password": "x-pack-test-password",
210+
"roles" : ["remote_search"]
211+
}""");
212+
assertOK(adminClient().performRequest(putUserRequest));
213+
}
214+
}
215+
216+
private void updateClusterSettingsFulfillingCluster(Settings settings) throws IOException {
217+
final var request = newXContentRequest(HttpMethod.PUT, "/_cluster/settings", (builder, params) -> {
218+
builder.startObject("persistent");
219+
settings.toXContent(builder, params);
220+
return builder.endObject();
221+
});
222+
223+
performRequestWithAdminUser(fulfillingClusterClient, request);
224+
}
225+
226+
private Response performRequestWithRemoteAccessUser(final Request request) throws IOException {
227+
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue(REMOTE_SEARCH_USER, PASS)));
228+
return client().performRequest(request);
229+
}
230+
}

x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ private static MockTransportService startTransport(
619619
action,
620620
SystemUser.crossClusterAccessSubjectInfo(TransportVersion.current(), nodeName)
621621
)
622-
).writeToContext(threadContext);
622+
).writeToContext(threadContext, null);
623623
connection.sendRequest(requestId, action, request, options);
624624
}
625625
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDSzCCAjOgAwIBAgIUPw9V/LIrB5Y+Krhqp1mXhK/BQDIwDQYJKoZIhvcNAQEL
3+
BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l
4+
cmF0ZWQgQ0EwIBcNMjUwOTE4MDczMzQ2WhgPMjA1MzAyMDMwNzMzNDZaMDQxMjAw
5+
BgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2VuZXJhdGVkIENB
6+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuQkSVLDqxWT83K+gfljq
7+
WWL5KQxiN/7XZ8ug6e5b+kY0MZQnaAUWNaj5RFgSMDB+2N6EWJMk5cmDXK7NB/xq
8+
gTbC/o3o7B9AZMTpu5Wbj8chRBCTTirRaIh79/VdLWDHriIgxGBLdMm/A2b3IW6H
9+
YeUGUdOszEjytCjslrrktnIMIQHlQQ5o/fSPslCunsm7P+rDyf3GjapflKtpcrIV
10+
cZKExaaCaqJtQYn66JCyr/ZFmjiTRPjPBcFx2SqAvkPXSK4SghvhKX69K+qDQWjF
11+
rPx9BUWgLnE00bJ27CCSCRzZ3dTgcZ86ou/2mOJqqpeCMacJGWnn7Cu1P3+LcgjT
12+
cQIDAQABo1MwUTAdBgNVHQ4EFgQUlL1P7M7/YCDULUeMUHPxDMtHT9swHwYDVR0j
13+
BBgwFoAUlL1P7M7/YCDULUeMUHPxDMtHT9swDwYDVR0TAQH/BAUwAwEB/zANBgkq
14+
hkiG9w0BAQsFAAOCAQEATCKw10zkCI21nuNppQZKFbHf/m3IZR9mZYYU0tKBSIy7
15+
KoCTHZUTadbJuDzJ8eDRiqnUuXHUXNijykEphvfpckNDhb6ty5g707kET3EYDfkh
16+
S1EKet2clM9DRqqcmFt3cyOmLJE3we7NjrNOuKNiwuXbGrqTTqNkqiiB3gWYOpSM
17+
uwtCz1Syyl4y5sjocedkikqaeIKtl2htN3tEYd0BfLNVo5hN/syP8WDT6FdpCDpY
18+
lZ2nqT622KDuusORCMTiC1qgUVR3RghPHy55Jq6Qq1+a1//E/Q9OfCs98JeUnoSp
19+
W/q1hUVlSN0Edsn1T5LehGMjiH3UVszWvEThUqNHuA==
20+
-----END CERTIFICATE-----
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDJTCCAg2gAwIBAgIVAIXaEONwJyih5d7KhnPYfqW5eNgKMA0GCSqGSIb3DQEB
3+
CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu
4+
ZXJhdGVkIENBMCAXDTI1MDkxODA5NDQwMVoYDzIwNTMwMjAzMDk0NDAxWjATMREw
5+
DwYDVQQDEwhpbnN0YW5jZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
6+
AJCOS/v9I34GaRD4BoMpIa/zhL1TBc1tqIDFKDm4Y/g4a+KUMJlaqHFRZkNI35LF
7+
TkgMkRJUrWEunXJ2wf2A2s+7xWykSoa+nJ2qghwDgBtoSBWSm6I2Hi9n400Mmde/
8+
xg912NzlfLJZH3la3/w3u7ENUY3GTNLeE5s5CpZAcOk+KQ/2/1Y7TgKPxyhbNtRA
9+
2whWD862pnJypskQ9UGgB3Zq5h+2llQ2sB367pE77DyvXReKLHfCtA3lmTob6pLm
10+
fK2cIBEJDwkFaAgrcWH5MwMkn+4v/Xw1PjAI4AVMOge+Rxt6waWxqQIJvOoyccXY
11+
Vdvo8swUAjMPnR6E/5+bwykCAwEAAaNNMEswHQYDVR0OBBYEFH1XQX26JBIwvu95
12+
xPhSCqOFrz9IMB8GA1UdIwQYMBaAFJS9T+zO/2Ag1C1HjFBz8QzLR0/bMAkGA1Ud
13+
EwQCMAAwDQYJKoZIhvcNAQELBQADggEBAIF/LkOYm52Q+buBqGS380HWkNitTLG2
14+
8qtICtXtLYd9673+c3RNIrW2CGFq3Z3TJ60FNvVT1z6NKiR8ZPUeqN+Avq5qN+dB
15+
u9SPRFOrszlD6+2ZkNaZyRs2w6NQa6zBZWs0Zp3+ouu4fUEdsa/UmKud6njLaAGA
16+
Rq8Sc7ckssykh1HKk8dOJt83GlvsBGXKALNv3vfHnMj+5XHC2NzZS5bn1IXWQE5z
17+
0z5cHHD4NHiuGBnTl7MI8KzrF/Axwc2krsVO7WIQ/GpDVVwrCoKyvNm54GfpIAE/
18+
ndH7bu9hGVM6swzpAdhQC/HK6Vc0NoGfoARXVRtxuEZmoq2amixHJJU=
19+
-----END CERTIFICATE-----
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEogIBAAKCAQEAkI5L+/0jfgZpEPgGgykhr/OEvVMFzW2ogMUoObhj+Dhr4pQw
3+
mVqocVFmQ0jfksVOSAyRElStYS6dcnbB/YDaz7vFbKRKhr6cnaqCHAOAG2hIFZKb
4+
ojYeL2fjTQyZ17/GD3XY3OV8slkfeVrf/De7sQ1RjcZM0t4TmzkKlkBw6T4pD/b/
5+
VjtOAo/HKFs21EDbCFYPzramcnKmyRD1QaAHdmrmH7aWVDawHfrukTvsPK9dF4os
6+
d8K0DeWZOhvqkuZ8rZwgEQkPCQVoCCtxYfkzAySf7i/9fDU+MAjgBUw6B75HG3rB
7+
pbGpAgm86jJxxdhV2+jyzBQCMw+dHoT/n5vDKQIDAQABAoIBAA1EKeAF4sB5mSHY
8+
CUz3NOK/cAKqAGHSewDaVy8451/L2cbQ/8bLJaNEq6RoJzCCkAUXtiafA8xj6Uos
9+
cPAxZ6Nh4aPvTfGgw6HKmKc2gQbC4r6sFkFkQw/pslgLXIEK1gPsNktLek6p1DQg
10+
bWbpvH1qsf3XYYyGmfkIWprgbhxRl0Z/9dji+T7f23JvwMm+18e4RG4ic4Xb40gU
11+
J6oYQ4ogev4f0VML+r2YUke+wx5NdwoM2L3uMUM7tf/T75AJBxQdQGiBSHB5zakX
12+
z2/LtdWYxb7IDKKrFkdY99TWPIgtHtzZzdYr4/FUGx6cRs25F9okP2ucQ0U6UU1V
13+
161bbsECgYEAwmGOGAdXaRXlOSfvENjf6sf9npz2KGlv8kEuBU9w0Gtxfx1q8Xx9
14+
WUw4iI01LbYfBR/BWbCxOsn3SH5EQjQcReG0NyOo1G3a6S+NCfBKZms4DLWRA7fG
15+
fF2ly9kvbvPz9089O7BWous9EqEnOEC+hkzMsb1lXYRDk8OQa2mlj1kCgYEAvmFR
16+
EwcXuzfqJxHi6cEU+3bYrja3NOstWSBfvsulXc1G8tOcbqS4OGIRviQpA4+2JaK0
17+
S4/YMiT3hUF+2lzZcnGSSrToKKeKrxNLUXoE4QLRjcVNmOIBSQdN+xbZVdFXeRCM
18+
UnqBuw+gGmOhHeVicVWEbSjUce0FhIdHiQd/qFECgYAFU68VMX5Pvu3dNx7yEz9v
19+
q7NjmWGVke4jcW3Vb2vkCk298gxwOb0lqVUTSOtgKVGITmp6DsGMnuRL9EnilpL/
20+
x0OtDykdSTVqlocC8rbXP7D1iDRFKdAisF5Oy9Dk9YKGEIHZFOgK5u9xh0EP5ZZT
21+
D9+8LziL64f+kKlwiCClYQKBgCB63+ccJatWPceOoKT6wQap3wvR3+3SVblH8a3O
22+
dpcLR5h0C9NAnQFZkedbqfemlA/Vs2bU0rCzZ9s/MlI01xBUWf4O4TDWbK2z3/y1
23+
kZGF9pR2XefAXzHDYkV9P3UJsx+/eAE2T13Hq6v05W8BTItDaMVq2tvY8UEMB2NU
24+
eS4RAoGASViwa3uI8avQts1Jf4M4jIHTLzyqY8hfA+06BALe69nw4vWNsIFQlNot
25+
IA2+276jZp0eoFtnleo+y9PD+5zZBWXYypGfw3XgVW2m7RI0x7Bic9zGEJlmgyZH
26+
hUEbMlQjY1F6xv7+WWQCuiZf4ns7uHeseu8bczp0d/jT2ZQtMWE=
27+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)