Skip to content

Commit 2a89b59

Browse files
authored
Properly support TLS host verification with multiple static endpoints (#33)
* Properly support TLS host verification with multiple static endpoints Also allow explicit override of authority and deprecate ComposeTrustManagerFactory Resolves #32 * Update doc to reflect overrideAuthority addition
1 parent 1a04a71 commit 2a89b59

5 files changed

Lines changed: 90 additions & 26 deletions

File tree

etcd-json-schema.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,30 @@ Example JSON config doc:
44

55
```json
66
{
7-
"compose_deployment": "etcd-development-01",
87
"endpoints": "https://dev3-9.compose.direct:15182,https://dev3-11.compose.direct:15182",
98
"userid": "userid",
109
"password": "password",
1110
"root_prefix": "aka-chroot-or-namespace",
12-
"certificate_file": "etcd-dev.pem"
11+
"certificate_file": "etcd-dev.pem",
12+
"override_authority": "etcd-development-01"
1313
}
1414
```
1515

1616
- All attributes apart from `endpoints` are optional.
1717
- The `root_prefix` attribute currently has **no effect** on clients created via `EtcdClientConfig.getClient()`. It's included in the configuration for use by application code (to query via `EtcdClientConfig.getRootPrefix()`). In future full chroot-like functionality at the client level might be supported.
1818
- `certificate_file` is the name of a pem-format (public) cert to use for TLS server-auth, either an absolute path or a filename assumed to be in the same directory as the json config file itself.
1919
- A `certificate` attribute may be included _instead of_ `certificate_file`, whose value is an embedded string UTF-8 pem format certificate. This allows a single json doc to hold all of the necessary connection info.
20-
- The `compose_deployment` attribute is only required when using an IBM Compose etcd deployment with provided TLS certificates. It must be set to the name of the deployment, which is the CN of the TLS cert.
20+
- The `override_authority` is optional and may be used to override the authority used for TLS hostname verification for _all_ endpoints.
2121

2222
Example with embedded (trunctated) TLS cert:
2323

2424
```json
2525
{
26-
"compose_deployment": "etcd-development-01",
2726
"endpoints": "https://dev3-9.compose.direct:15182,https://dev3-11.compose.direct:15182",
2827
"userid": "userid",
2928
"password": "password",
3029
"root_prefix": "aka-chroot-or-namespace",
31-
"certificate": "-----BEGIN CERTIFICATE-----\nMIIDaTCCA ... MP0u6J/xasx14IW4A==\n-----END CERTIFICATE-----\n"
30+
"certificate": "-----BEGIN CERTIFICATE-----\nMIIDaTCCA ... MP0u6J/xasx14IW4A==\n-----END CERTIFICATE-----\n",
31+
"override_authority": "etcd-development-01"
3232
}
3333
```

src/main/java/com/ibm/etcd/client/EtcdClient.java

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.IOException;
2222
import java.io.InputStream;
2323
import java.util.Arrays;
24+
import java.util.Collections;
2425
import java.util.List;
2526
import java.util.Set;
2627
import java.util.concurrent.BrokenBarrierException;
@@ -120,8 +121,12 @@ public class EtcdClient implements KvStoreClient {
120121
private volatile PersistentLease sessionLease; // lazy-instantiated
121122

122123
public static class Builder {
123-
private final NettyChannelBuilder chanBuilder;
124+
private final List<String> endpoints;
125+
private boolean plainText;
126+
private int maxInboundMessageSize;
124127
private SslContextBuilder sslContextBuilder;
128+
private SslContext sslContext;
129+
private String overrideAuthority;
125130
private ByteString name, password;
126131
private long defaultTimeoutMs = DEFAULT_TIMEOUT_MS;
127132
private boolean preemptAuth;
@@ -130,8 +135,9 @@ public static class Builder {
130135
private boolean sendViaEventLoop = true; // default true
131136
private int sessTimeoutSecs = DEFAULT_SESSION_TIMEOUT_SECS;
132137

133-
Builder(NettyChannelBuilder chanBuilder) {
134-
this.chanBuilder = chanBuilder;
138+
Builder(List<String> endpoints) {
139+
this.endpoints = Preconditions.checkNotNull(endpoints);
140+
Preconditions.checkArgument(!endpoints.isEmpty(), "empty endpoints");
135141
}
136142

137143
/**
@@ -224,7 +230,16 @@ private SslContextBuilder sslBuilder() {
224230
* Disable TLS - to connect to insecure servers in development contexts
225231
*/
226232
public Builder withPlainText() {
227-
chanBuilder.usePlaintext();
233+
plainText = true;
234+
return this;
235+
}
236+
237+
/**
238+
* Override the authority used for TLS hostname verification. Applies
239+
* to all endpoints and does not otherwise affect DNS name resolution.
240+
*/
241+
public Builder overrideAuthority(String authority) {
242+
this.overrideAuthority = authority;
228243
return this;
229244
}
230245

@@ -237,7 +252,7 @@ public Builder withPlainText() {
237252
*/
238253
public Builder withCaCert(ByteSource certSource) throws IOException, SSLException {
239254
try (InputStream cert = certSource.openStream()) {
240-
chanBuilder.sslContext(sslBuilder().trustManager(cert).build());
255+
sslContext = sslBuilder().trustManager(cert).build();
241256
}
242257
return this;
243258
}
@@ -250,7 +265,7 @@ public Builder withCaCert(ByteSource certSource) throws IOException, SSLExceptio
250265
* @throws SSLException
251266
*/
252267
public Builder withTrustManager(TrustManagerFactory tmf) throws SSLException {
253-
chanBuilder.sslContext(sslBuilder().trustManager(tmf).build());
268+
sslContext = sslBuilder().trustManager(tmf).build();
254269
return this;
255270
}
256271

@@ -265,7 +280,7 @@ public Builder withTrustManager(TrustManagerFactory tmf) throws SSLException {
265280
public Builder withTlsConfig(Consumer<SslContextBuilder> contextBuilder) throws SSLException {
266281
SslContextBuilder sslBuilder = sslBuilder();
267282
contextBuilder.accept(sslBuilder);
268-
chanBuilder.sslContext(sslBuilder.build());
283+
sslContext = sslBuilder.build();
269284
return this;
270285
}
271286

@@ -287,15 +302,36 @@ public Builder withSessionTimeoutSecs(int timeoutSecs) {
287302
* @param sizeInBytes
288303
*/
289304
public Builder withMaxInboundMessageSize(int sizeInBytes) {
290-
chanBuilder.maxInboundMessageSize(sizeInBytes);
305+
this.maxInboundMessageSize = sizeInBytes;
291306
return this;
292307
}
293308

294309
/**
295310
* @return the built {@link EtcdClient} instance
296311
*/
297312
public EtcdClient build() {
298-
return new EtcdClient(chanBuilder, defaultTimeoutMs, name, password,
313+
NettyChannelBuilder ncb;
314+
if (endpoints.size() == 1) {
315+
ncb = NettyChannelBuilder.forTarget(endpoints.get(0));
316+
if (overrideAuthority != null) {
317+
ncb.overrideAuthority(overrideAuthority);
318+
}
319+
} else {
320+
ncb = NettyChannelBuilder
321+
.forTarget(StaticEtcdNameResolverFactory.ETCD)
322+
.nameResolverFactory(new StaticEtcdNameResolverFactory(
323+
endpoints, overrideAuthority));
324+
}
325+
if (plainText) {
326+
ncb.usePlaintext();
327+
}
328+
if (sslContext != null) {
329+
ncb.sslContext(sslContext);
330+
}
331+
if (maxInboundMessageSize != 0) {
332+
ncb.maxInboundMessageSize(maxInboundMessageSize);
333+
}
334+
return new EtcdClient(ncb, defaultTimeoutMs, name, password,
299335
preemptAuth, threads, executor, sendViaEventLoop, sessTimeoutSecs);
300336
}
301337
}
@@ -312,7 +348,7 @@ private static int defaultThreadCount() {
312348
*/
313349
public static Builder forEndpoint(String host, int port) {
314350
String target = GrpcUtil.authorityFromHostAndPort(host, port);
315-
return new Builder(NettyChannelBuilder.forTarget(target));
351+
return new Builder(Collections.singletonList(target));
316352
}
317353

318354
/**
@@ -321,10 +357,7 @@ public static Builder forEndpoint(String host, int port) {
321357
* @return
322358
*/
323359
public static Builder forEndpoints(List<String> endpoints) {
324-
NettyChannelBuilder ncb = NettyChannelBuilder
325-
.forTarget(StaticEtcdNameResolverFactory.ETCD)
326-
.nameResolverFactory(new StaticEtcdNameResolverFactory(endpoints));
327-
return new Builder(ncb);
360+
return new Builder(endpoints);
328361
}
329362

330363
/**

src/main/java/com/ibm/etcd/client/StaticEtcdNameResolverFactory.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
*/
1616
package com.ibm.etcd.client;
1717

18+
import static io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE;
19+
import static java.util.stream.Collectors.toList;
20+
1821
import java.net.URI;
1922
import java.util.Arrays;
2023
import java.util.Collections;
2124
import java.util.List;
2225
import java.util.regex.Matcher;
2326
import java.util.regex.Pattern;
24-
import java.util.stream.Collectors;
2527
import java.util.stream.Stream;
2628

2729
import io.grpc.Attributes;
@@ -51,14 +53,32 @@ static class SubResolver {
5153
public SubResolver(URI uri, NameResolver.Helper helper) {
5254
this.resolver = DNS_PROVIDER.newNameResolver(uri, helper);
5355
}
56+
57+
void updateEagList(List<EquivalentAddressGroup> servers, boolean ownAuthority) {
58+
if (ownAuthority) {
59+
// Use this endpoint address' authority for its subchannel
60+
String authority = resolver.getServiceAuthority();
61+
eagList = servers.stream().map(eag -> new EquivalentAddressGroup(
62+
eag.getAddresses(), eag.getAttributes().toBuilder()
63+
.set(ATTR_AUTHORITY_OVERRIDE, authority).build())).collect(toList());
64+
} else {
65+
eagList = servers;
66+
}
67+
}
5468
}
5569

5670
private final URI[] uris;
71+
private final String overrideAuthority;
5772

5873
public StaticEtcdNameResolverFactory(List<String> endpoints) {
74+
this(endpoints, null);
75+
}
76+
77+
public StaticEtcdNameResolverFactory(List<String> endpoints, String overrideAuthority) {
5978
if (endpoints == null || endpoints.isEmpty()) {
6079
throw new IllegalArgumentException("endpoints");
6180
}
81+
this.overrideAuthority = overrideAuthority;
6282
int count = endpoints.size();
6383
uris = new URI[count];
6484
for (int i = 0; i < count; i++) {
@@ -95,9 +115,11 @@ public void start(Listener listener) {
95115
@Override
96116
public void onAddresses(List<EquivalentAddressGroup> servers, Attributes attributes) {
97117
synchronized (resolvers) {
98-
sr.eagList = servers;
118+
// Update this subresolver's servers
119+
sr.updateEagList(servers, overrideAuthority == null);
120+
// Advertise the complete list of EAGs
99121
List<EquivalentAddressGroup> newList = Stream.of(resolvers)
100-
.flatMap(r -> r.eagList.stream()).collect(Collectors.toList());
122+
.flatMap(r -> r.eagList.stream()).collect(toList());
101123
currentCount = newList.size();
102124
listener.onAddresses(newList, Attributes.EMPTY);
103125
}
@@ -122,7 +144,7 @@ public void refresh() {
122144
}
123145
@Override
124146
public String getServiceAuthority() {
125-
return ETCD;
147+
return overrideAuthority != null ? overrideAuthority : ETCD;
126148
}
127149
@Override
128150
public void shutdown() {

src/main/java/com/ibm/etcd/client/config/ComposeTrustManagerFactory.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@
3737
import io.netty.util.internal.EmptyArrays;
3838

3939
/**
40-
* Custom trust manager to use with multi-endpoint IBM Compose etcd deployments.
41-
* Works around grpc-java TLS issues.
42-
*
40+
* @deprecated This is no longer required and shouldn't be used
4341
*/
42+
@Deprecated
4443
public class ComposeTrustManagerFactory extends SimpleTrustManagerFactory {
4544

4645
private static final Logger logger = LoggerFactory.getLogger(ComposeTrustManagerFactory.class);

src/main/java/com/ibm/etcd/client/config/EtcdClusterConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ public enum TlsMode { TLS, PLAINTEXT, AUTO }
6969
TlsMode tlsMode;
7070
ByteString user, password;
7171
ByteString rootPrefix; // a.k.a namespace
72+
@Deprecated
7273
String composeDeployment;
7374
ByteSource certificate;
75+
String overrideAuthority;
7476

7577
protected EtcdClusterConfig() {}
7678

@@ -91,6 +93,9 @@ private EtcdClient newClient() throws IOException, CertificateException {
9193
EtcdClient.Builder builder = EtcdClient.forEndpoints(endpointList)
9294
.withCredentials(user, password).withImmediateAuth()
9395
.withMaxInboundMessageSize(maxMessageSize);
96+
if (overrideAuthority != null) {
97+
builder.overrideAuthority(overrideAuthority);
98+
}
9499
TlsMode ssl = tlsMode;
95100
if (ssl == TlsMode.AUTO || ssl == null) {
96101
String ep = endpointList.get(0);
@@ -147,6 +152,7 @@ public static EtcdClusterConfig fromProperties(ByteSource source) throws IOExcep
147152
}
148153
config.certificate = Files.asByteSource(certFile);
149154
}
155+
config.overrideAuthority = props.getProperty("override_authority");
150156
return config;
151157
}
152158

@@ -184,6 +190,7 @@ public static EtcdClusterConfig fromJson(ByteSource source, File dir) throws IOE
184190
config.certificate = ByteSource.wrap(jsonConfig.certificate.getBytes(UTF_8));
185191
}
186192
}
193+
config.overrideAuthority = jsonConfig.overrideAuthority;
187194
return config;
188195
}
189196

@@ -286,11 +293,14 @@ static class JsonConfig {
286293
String password;
287294
@SerializedName("root_prefix") // a.k.a namespace
288295
String rootPrefix;
296+
@Deprecated
289297
@SerializedName("compose_deployment")
290298
String composeDeployment;
291299
@SerializedName("certificate")
292300
String certificate;
293301
@SerializedName("certificate_file")
294302
String certificateFile;
303+
@SerializedName("override_authority")
304+
String overrideAuthority;
295305
}
296306
}

0 commit comments

Comments
 (0)