Skip to content

Commit 4c9d903

Browse files
committed
Transport: Add the CACert configuration option to the client and transport
When the option is set, the transport will perform the neccessary ceremony for adding the custom certificate authority to the TLSClientConfig. It returns an error for transports other than http.Transport. An executable example has been added to _examples/security/tls_with_ca.go. Closes #106
1 parent e66ab8d commit 4c9d903

File tree

5 files changed

+193
-9
lines changed

5 files changed

+193
-9
lines changed

README.md

+30-9
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ log.Println(res)
106106
When you export the `ELASTICSEARCH_URL` environment variable,
107107
it will be used to set the cluster endpoint(s). Separate multiple adresses by a comma.
108108

109-
To set the cluster endpoint(s) programatically, pass them in the configuration object
109+
To set the cluster endpoint(s) programatically, pass a configuration object
110110
to the `elasticsearch.NewClient()` function.
111111

112112
```golang
@@ -115,34 +115,55 @@ cfg := elasticsearch.Config{
115115
"http://localhost:9200",
116116
"http://localhost:9201",
117117
},
118+
// ...
118119
}
119120
es, err := elasticsearch.NewClient(cfg)
120-
// ...
121121
```
122122

123-
To configure the HTTP settings, pass a [`http.Transport`](https://golang.org/pkg/net/http/#Transport)
124-
object in the configuration object (the values are for illustrative purposes only).
123+
To set the username and password, include them in the endpoint URL,
124+
or use the corresponding configuration options.
125+
126+
```golang
127+
cfg := elasticsearch.Config{
128+
// ...
129+
Username: "foo",
130+
Password: "bar",
131+
}
132+
```
133+
134+
To set a custom certificate authority used to sign the certificates of cluster nodes,
135+
use the `CACert` configuration option.
136+
137+
```golang
138+
cert, _ := ioutil.ReadFile(*cacert)
139+
140+
cfg := elasticsearch.Config{
141+
// ...
142+
CACert: cert,
143+
}
144+
```
145+
146+
To configure other HTTP settings, pass an [`http.Transport`](https://golang.org/pkg/net/http/#Transport)
147+
object in the configuration object.
125148

126149
```golang
127150
cfg := elasticsearch.Config{
128151
Transport: &http.Transport{
129152
MaxIdleConnsPerHost: 10,
130153
ResponseHeaderTimeout: time.Second,
131-
DialContext: (&net.Dialer{Timeout: time.Second}).DialContext,
132154
TLSClientConfig: &tls.Config{
133155
MinVersion: tls.VersionTLS11,
134156
// ...
135157
},
158+
// ...
136159
},
137160
}
138-
139-
es, err := elasticsearch.NewClient(cfg)
140-
// ...
141161
```
142162

143163
See the [`_examples/configuration.go`](_examples/configuration.go) and
144164
[`_examples/customization.go`](_examples/customization.go) files for
145165
more examples of configuration and customization of the client.
166+
See the [`_examples/security`](_examples/security) for an example of a security configuration.
146167

147168
The following example demonstrates a more complex usage. It fetches the Elasticsearch version from the cluster, indexes a couple of documents concurrently, and prints the search results, using a lightweight wrapper around the response body.
148169

@@ -335,7 +356,7 @@ The `esutil` package provides convenience helpers for working with the client. A
335356

336357
## Examples
337358

338-
The **[`_examples`](./_examples)** folder contains a number of recipes and comprehensive examples to get you started with the client, including configuration and customization of the client, mocking the transport for unit tests, embedding the client in a custom type, building queries, performing requests, and parsing the responses.
359+
The **[`_examples`](./_examples)** folder contains a number of recipes and comprehensive examples to get you started with the client, including configuration and customization of the client, using a custom certificate authority (CA) for security (TLS), mocking the transport for unit tests, embedding the client in a custom type, building queries, performing requests individually and in bulk, and parsing the responses.
339360

340361
<!-- ----------------------------------------------------------------------------------------------- -->
341362

_examples/security/README.md

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Example: Security
2+
3+
This example demonstrates how to use Transport Layer Security (TLS) for
4+
encrypting and verifying the communication with an Elasticsearch cluster
5+
by passing a custom certificate authority to the client.
6+
7+
## Creating the certificates for the cluster nodes
8+
9+
Generate the certificates using the
10+
[`elasticsearch-certutil`](https://www.elastic.co/guide/en/elasticsearch/reference/current/certutil.html)
11+
tool:
12+
13+
```bash
14+
make certificates
15+
```
16+
17+
> See the [_Encrypting communications in an Elasticsearch Docker Container_](https://www.elastic.co/guide/en/elasticsearch/reference/current/configuring-tls-docker.html) tutorial for a complete overview.
18+
19+
Start the cluster with full security configuration:
20+
21+
```bash
22+
make cluster
23+
```
24+
25+
See the [`elasticsearch-cluster.yml`](elasticsearch-cluster.yml) file for details.
26+
27+
Use `curl` to verify access to the cluster:
28+
29+
```
30+
curl --cacert certificates/ca/ca.crt https://elastic:elastic@localhost:9200
31+
```
32+
33+
> NOTE: On Mac OS X, you may need to add the certificate to the Keychain with `security add-trusted-cert -p ssl certificates/ca/ca.crt`. To remove it, run `security remove-trusted-cert certificates/ca/ca.crt`.
34+
35+
36+
## Using the client configuration option
37+
38+
To pass the certificate authority (CA) to the client, so it can verify the server certificate,
39+
use the `elasticsearch.Config.CACert` configuration option:
40+
41+
```go
42+
// --> Read the certificate from file
43+
cert, _ := ioutil.ReadFile(*cacert)
44+
45+
es, _ := elasticsearch.NewClient(
46+
elasticsearch.Config{
47+
// ...
48+
49+
// --> Pass the certificate to the client
50+
CACert: cert,
51+
})
52+
```
53+
54+
Run the full example:
55+
56+
```bash
57+
go run tls_with_ca.go
58+
59+
# [200 OK] {
60+
# ...
61+
```
62+
63+
64+
## Manual transport configuration
65+
66+
To configure the transport manually, use the
67+
`(*http.Transport).TLSClientConfig.RootCAs.AppendCertsFromPEM()` method.
68+
69+
Run the full example:
70+
71+
```bash
72+
go run tls_configure_ca.go
73+
74+
# [200 OK] {
75+
# ...
76+
```

_examples/security/tls_with_ca.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Licensed to Elasticsearch B.V. under one or more agreements.
2+
// Elasticsearch B.V. licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
// +build ignore
6+
7+
package main
8+
9+
import (
10+
"flag"
11+
"io/ioutil"
12+
"log"
13+
14+
"github.com/elastic/go-elasticsearch/v8"
15+
)
16+
17+
func main() {
18+
log.SetFlags(0)
19+
20+
var (
21+
err error
22+
23+
// --> Configure the path to the certificate authority and the password
24+
//
25+
cacert = flag.String("cacert", "certificates/ca/ca.crt", "Path to the file with certificate authority")
26+
password = flag.String("password", "elastic", "Elasticsearch password")
27+
)
28+
flag.Parse()
29+
30+
// --> Read the certificate from file
31+
//
32+
cert, err := ioutil.ReadFile(*cacert)
33+
if err != nil {
34+
log.Fatalf("ERROR: Unable to read CA from %q: %s", *cacert, err)
35+
}
36+
37+
es, err := elasticsearch.NewClient(
38+
elasticsearch.Config{
39+
Addresses: []string{"https://localhost:9200"},
40+
Username: "elastic",
41+
Password: *password,
42+
43+
// --> Pass the certificate to the client
44+
//
45+
CACert: cert,
46+
})
47+
if err != nil {
48+
log.Fatalf("ERROR: Unable to create client: %s", err)
49+
}
50+
51+
res, err := es.Info()
52+
if err != nil {
53+
log.Fatalf("ERROR: Unable to get response: %s", err)
54+
}
55+
56+
log.Println(res)
57+
}

elasticsearch.go

+10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ type Config struct {
3737
CloudID string // Endpoint for the Elastic Service (https://elastic.co/cloud).
3838
APIKey string // Base64-encoded token for authorization; if set, overrides username and password.
3939

40+
// PEM-encoded certificate authorities.
41+
// When set, an empty certificate pool will be created, and the certificates will be appended to it.
42+
// The option is only valid when the transport is not specified, or when it's http.Transport.
43+
CACert []byte
44+
4045
RetryOnStatus []int // List of status codes for retry. Default: 502, 503, 504.
4146
DisableRetry bool // Default: false.
4247
EnableRetryOnTimeout bool // Default: false.
@@ -134,6 +139,8 @@ func NewClient(cfg Config) (*Client, error) {
134139
Password: cfg.Password,
135140
APIKey: cfg.APIKey,
136141

142+
CACert: cfg.CACert,
143+
137144
RetryOnStatus: cfg.RetryOnStatus,
138145
DisableRetry: cfg.DisableRetry,
139146
EnableRetryOnTimeout: cfg.EnableRetryOnTimeout,
@@ -150,6 +157,9 @@ func NewClient(cfg Config) (*Client, error) {
150157
Selector: cfg.Selector,
151158
ConnectionPoolFunc: cfg.ConnectionPoolFunc,
152159
})
160+
if err != nil {
161+
return nil, fmt.Errorf("error creating transport: %s", err)
162+
}
153163

154164
client := &Client{Transport: tp, API: esapi.New(tp)}
155165

estransport/estransport.go

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package estransport
66

77
import (
88
"bytes"
9+
"crypto/x509"
10+
"errors"
911
"fmt"
1012
"io"
1113
"io/ioutil"
@@ -52,6 +54,8 @@ type Config struct {
5254
Password string
5355
APIKey string
5456

57+
CACert []byte
58+
5559
RetryOnStatus []int
5660
DisableRetry bool
5761
EnableRetryOnTimeout bool
@@ -105,6 +109,22 @@ func New(cfg Config) (*Client, error) {
105109
cfg.Transport = http.DefaultTransport
106110
}
107111

112+
if cfg.CACert != nil {
113+
httpTransport, ok := cfg.Transport.(*http.Transport)
114+
if !ok {
115+
return nil, fmt.Errorf("unable to set CA certificate for transport of type %T", cfg.Transport)
116+
}
117+
118+
httpTransport = httpTransport.Clone()
119+
httpTransport.TLSClientConfig.RootCAs = x509.NewCertPool()
120+
121+
if ok := httpTransport.TLSClientConfig.RootCAs.AppendCertsFromPEM(cfg.CACert); !ok {
122+
return nil, errors.New("unable to add CA certificate")
123+
}
124+
125+
cfg.Transport = httpTransport
126+
}
127+
108128
if len(cfg.RetryOnStatus) == 0 {
109129
cfg.RetryOnStatus = defaultRetryOnStatus[:]
110130
}

0 commit comments

Comments
 (0)