Skip to content

Commit

Permalink
Support TLS-encrypted communication with Consul (linkerd#1577) (linke…
Browse files Browse the repository at this point in the history
…rd#1842)

Problem
Consul supports TLS-encrypted communication, but there wasn't option to configure Linkerd to work with it.

Solution
Added TLS support for Consul namer.

Validation
Tests were done on a locally running Linkerd instance connected to Consul running on vagrant. Consul has been configured to work only with encrypted connection. Linked was able to connect to the Consul and fetch data from the service discovery.

Signed-off-by: Lukasz Marchewka <[email protected]>
  • Loading branch information
LukaszMarchewka authored and adleong committed Mar 20, 2018
1 parent 1425f0c commit b8efcd4
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 5 deletions.
39 changes: 39 additions & 0 deletions linkerd/docs/namer.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ consistencyMode | `default` | Select between [Consul API consistency modes](http
failFast | `false` | If `false`, disable fail fast and failure accrual for Consul client. Keep it `false` when using a local agent but change it to `true` when talking directly to an HA Consul API.
preferServiceAddress | `true` | If `true` use the service address if defined and default to the node address. If `false` always use the node address.
weights | none | List of tag-weight configurations, for adjusting the weights of node addresses. When a node matches more than one tag, it gets the highest matching weight. In the absence of match or configuration, nodes get a default weight of `1.0`.
tls | no tls | Use TLS during connection with Consul. see [Consul Encryption](https://www.consul.io/docs/agent/encryption.html) and [TLS](#consul-tls).

### Consul Path Parameters

Expand All @@ -231,6 +232,44 @@ datacenter | yes | The Consul datacenter to use for this request. It can have a
tag | yes if includeTag is `true` | The Consul tag to use for this request.
serviceName | yes | The Consul service name to use for this request.

### Consul TLS

>Linkerd supports encrypted communication via TLS to Consul.

```yaml
namers:
- kind: io.l5d.consul
host: localhost
port: 8500
tls:
disableValidation: false
commonName: consul.io
trustCerts:
- /certificates/cacert.pem
clientAuth:
certPath: /certificates/cert.pem
keyPath: /certificates/key.pem
```

A TLS object describes how Linkerd should use TLS when sending requests to Consul agent.

Key | Default Value | Description
----------------- | ------------------------------------------ | -----------
disableValidation | false | Enable this to skip hostname validation (unsafe). Setting `disableValidation: true` is incompatible with `clientAuth`.
commonName | _required_ unless disableValidation is set | The common name to use for all TLS requests.
trustCerts | empty list | A list of file paths of CA certs to use for common name validation.
clientAuth | none | A client auth object used to sign requests.

If present, a clientAuth object must contain two properties:

Key | Default Value | Description
---------|---------------|-------------
certPath | _required_ | File path to the TLS certificate file.
keyPath | _required_ | File path to the TLS key file. Must be in PKCS#8 format.

<aside class="warning">
Setting `disableValidation: true` will force the use of the JDK SSL provider which does not support client auth. Therefore, `disableValidation: true` and `clientAuth` are incompatible.
</aside>

<a name="k8s"></a>
## Kubernetes service discovery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.buoyant.namer.consul

import com.fasterxml.jackson.annotation.JsonIgnore
import com.twitter.finagle._
import com.twitter.finagle.buoyant.TlsClientConfig
import com.twitter.finagle.service.Retries
import com.twitter.finagle.tracing.NullTracer
import io.buoyant.config.types.Port
Expand Down Expand Up @@ -30,6 +31,14 @@ import io.buoyant.namer.{NamerConfig, NamerInitializer}
* weights:
* - tag: primary
* weight: 100
* tls:
* disableValidation: false
* commonName: consul.io
* trustCerts:
* - /certificates/cacert.pem
* clientAuth:
* certPath: /certificates/cert.pem
* keyPath: /certificates/key.pem
* </pre>
*/
class ConsulInitializer extends NamerInitializer {
Expand All @@ -52,7 +61,8 @@ case class ConsulConfig(
consistencyMode: Option[ConsistencyMode] = None,
failFast: Option[Boolean] = None,
preferServiceAddress: Option[Boolean] = None,
weights: Option[Seq[TagWeight]] = None
weights: Option[Seq[TagWeight]] = None,
tls: Option[TlsClientConfig] = None
) extends NamerConfig {

@JsonIgnore
Expand All @@ -69,11 +79,13 @@ case class ConsulConfig(
*/
@JsonIgnore
def newNamer(params: Stack.Params): Namer = {
val tlsParams = tls.map(_.params).getOrElse(Stack.Params.empty)

val service = Http.client
// Removes the default client requeues module,
// (retries are handled in BaseApi.infiniteRetryFilter)
.withStack(Http.client.stack.remove(Retries.Role))
.withParams(Http.client.params ++ params)
.withParams(Http.client.params ++ tlsParams ++ params)
.withLabel("client")
.interceptInterrupts
.failFast(failFast)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package io.buoyant.namer.consul

import com.twitter.finagle.Stack
import com.twitter.finagle.buoyant.{ClientAuth, TlsClientConfig}
import com.twitter.finagle.util.LoadService
import io.buoyant.config.Parser
import io.buoyant.config.types.Port
import io.buoyant.consul.v1.ConsistencyMode
import io.buoyant.consul.v1.HealthStatus
import io.buoyant.consul.v1.{ConsistencyMode, HealthStatus}
import io.buoyant.namer.{NamerConfig, NamerInitializer}
import org.scalatest.FunSuite

Expand Down Expand Up @@ -49,6 +49,14 @@ class ConsulTest extends FunSuite {
|weights:
| - tag: primary
| weight: 100
|tls:
| disableValidation: false
| commonName: consul.io
| trustCerts:
| - /certificates/cacert.pem
| clientAuth:
| certPath: /certificates/cert.pem
| keyPath: /certificates/key.pem
""".stripMargin

val mapper = Parser.objectMapper(yaml, Iterable(Seq(ConsulInitializer)))
Expand All @@ -64,6 +72,9 @@ class ConsulTest extends FunSuite {
assert(consul.failFast == Some(true))
assert(consul.preferServiceAddress == Some(false))
assert(consul.weights == Some(Seq(TagWeight("primary", 100.0))))
val clientAuth = ClientAuth("/certificates/cert.pem", "/certificates/key.pem")
val tlsConfig = TlsClientConfig(Some(false), Some("consul.io"), Some(List("/certificates/cacert.pem")), Some(clientAuth))
assert(consul.tls == Some(tlsConfig))
assert(!consul.disabled)
}
}
38 changes: 38 additions & 0 deletions namerd/docs/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,5 +184,43 @@ readConsistencyMode | `default` | Select between [Consul API consistency modes](
writeConsistencyMode | `default` | Select between [Consul API consistency modes](https://www.consul.io/docs/agent/http.html) such as `default`, `stale` and `consistent` for writes.
failFast | `false` | If `false`, disable fail fast and failure accrual for Consul client. Keep it `false` when using a local agent but change it to `true` when talking directly to an HA Consul API.
backoff | exponential backoff from 1ms to 1min | Object that determines which backoff algorithm should be used. See [retry backoff](https://linkerd.io/config/head/linkerd#retry-backoff-parameters)
tls | no tls | Use TLS during connection with Consul. see [Consul Encryption](https://www.consul.io/docs/agent/encryption.html) and [TLS](#consul-tls).

### Consul TLS

>Linkerd supports encrypted communication via TLS to Consul.

```yaml
namers:
- kind: io.l5d.consul
host: localhost
port: 8500
tls:
disableValidation: false
commonName: consul.io
trustCerts:
- /certificates/cacert.pem
clientAuth:
certPath: /certificates/cert.pem
keyPath: /certificates/key.pem
```

A TLS object describes how Linkerd should use TLS when sending requests to Consul agent.

Key | Default Value | Description
----------------- | ------------------------------------------ | -----------
disableValidation | false | Enable this to skip hostname validation (unsafe). Setting `disableValidation: true` is incompatible with `clientAuth`.
commonName | _required_ unless disableValidation is set | The common name to use for all TLS requests.
trustCerts | empty list | A list of file paths of CA certs to use for common name validation.
clientAuth | none | A client auth object used to sign requests.

If present, a clientAuth object must contain two properties:

Key | Default Value | Description
---------|---------------|-------------
certPath | _required_ | File path to the TLS certificate file.
keyPath | _required_ | File path to the TLS key file. Must be in PKCS#8 format.

<aside class="warning">
Setting `disableValidation: true` will force the use of the JDK SSL provider which does not support client auth. Therefore, `disableValidation: true` and `clientAuth` are incompatible.
</aside>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.buoyant.consul.v1.{ConsistencyMode, KvApi}
import io.buoyant.namer.BackoffConfig
import com.twitter.conversions.time._
import com.twitter.finagle.Stack
import com.twitter.finagle.buoyant.TlsClientConfig
import io.buoyant.namerd.{DtabStore, DtabStoreConfig, DtabStoreInitializer}

case class ConsulConfig(
Expand All @@ -21,7 +22,8 @@ case class ConsulConfig(
readConsistencyMode: Option[ConsistencyMode] = None,
writeConsistencyMode: Option[ConsistencyMode] = None,
failFast: Option[Boolean] = None,
backoff: Option[BackoffConfig] = None
backoff: Option[BackoffConfig] = None,
tls: Option[TlsClientConfig] = None
) extends DtabStoreConfig {
import ConsulConfig._

Expand All @@ -30,13 +32,15 @@ case class ConsulConfig(
val serviceHost = host.getOrElse(DefaultHost)
val servicePort = port.getOrElse(DefaultPort).port
val backoffs = backoff.map(_.mk).getOrElse(DefaultBackoff)
val tlsParams = tls.map(_.params).getOrElse(Stack.Params.empty)

val service = Http.client
.interceptInterrupts
.failFast(failFast)
.setAuthToken(token)
.ensureHost(host, port)
.withTracer(NullTracer)
.withParams(tlsParams)
.newService(s"/$$/inet/$serviceHost/$servicePort")
new ConsulDtabStore(
KvApi(service, backoffs),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.buoyant.namerd.storage.consul

import com.twitter.finagle.buoyant.{ClientAuth, TlsClientConfig}
import com.twitter.finagle.{Path, Stack}
import io.buoyant.config.Parser
import io.buoyant.config.types.Port
Expand All @@ -23,6 +24,14 @@ class ConsulConfigTest extends FunSuite with OptionValues {
|datacenter: us-east-42
|readConsistencyMode: stale
|writeConsistencyMode: consistent
|tls:
| disableValidation: false
| commonName: consul.io
| trustCerts:
| - /certificates/cacert.pem
| clientAuth:
| certPath: /certificates/cert.pem
| keyPath: /certificates/key.pem
""".stripMargin
val mapper = Parser.objectMapper(yaml, Iterable(Seq(ConsulDtabStoreInitializer)))
val consul = mapper.readValue[DtabStoreConfig](yaml).asInstanceOf[ConsulConfig]
Expand All @@ -33,6 +42,9 @@ class ConsulConfigTest extends FunSuite with OptionValues {
assert(consul.datacenter == Some("us-east-42"))
assert(consul.readConsistencyMode == Some(ConsistencyMode.Stale))
assert(consul.writeConsistencyMode == Some(ConsistencyMode.Consistent))
val clientAuth = ClientAuth("/certificates/cert.pem", "/certificates/key.pem")
val tlsConfig = TlsClientConfig(Some(false), Some("consul.io"), Some(List("/certificates/cacert.pem")), Some(clientAuth))
assert(consul.tls == Some(tlsConfig))
}

}

0 comments on commit b8efcd4

Please sign in to comment.