Skip to content

Commit

Permalink
Encode API key as base64 in common code (#18945)
Browse files Browse the repository at this point in the history
* Encode API key as base64 in common code

* Adding comment on API key field

* Adding CHANGELOG entries

* Adding test

* Base64-encode API key in constructor

* Move encodedAPIKey field to Connection

* Update doc on `api_key` setting value

* Adding API key format to setting section

* Compute entire API key auth header value up front
  • Loading branch information
ycombinator committed Jun 19, 2020
1 parent cc70451 commit 67e4bf4
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-developer.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The list below covers the major changes between 7.0.0-rc2 and master only.
Your magefile.go will require a change to adapt the devtool API. See the pull request for
more details. {pull}18148[18148]
- Introduce APM libbeat instrumentation. `Publish` method on `Client` interface now takes a Context as first argument. {pull}17938[17938]
- The Elasticsearch client settings expect the API key to be raw (not base64-encoded). {issue}18939[18939] {pull}18945[18945]

==== Bugfixes

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d
- Fix regression in `add_kubernetes_metadata`, so configured `indexers` and `matchers` are used if defaults are not disabled. {issue}18481[18481] {pull}18818[18818]
- Fix the `translate_sid` processor's handling of unconfigured target fields. {issue}18990[18990] {pull}18991[18991]
- Fixed a service restart failure under Windows. {issue}18914[18914] {pull}18916[18916]
- The `monitoring.elasticsearch.api_key` value is correctly base64-encoded before being sent to the monitoring Elasticsearch cluster. {issue}18939[18939] {pull}18945[18945]

*Auditbeat*

Expand Down
22 changes: 15 additions & 7 deletions libbeat/esleg/eslegclient/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package eslegclient

import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -48,8 +49,9 @@ type Connection struct {
Encoder BodyEncoder
HTTP esHTTPClient

version common.Version
log *logp.Logger
apiKeyAuthHeader string // Authorization HTTP request header with base64-encoded API key
version common.Version
log *logp.Logger
}

// ConnectionSettings are the settings needed for a Connection
Expand All @@ -60,7 +62,7 @@ type ConnectionSettings struct {

Username string
Password string
APIKey string
APIKey string // Raw API key, NOT base64-encoded
Headers map[string]string

TLS *tlscommon.TLSConfig
Expand Down Expand Up @@ -157,12 +159,18 @@ func NewConnection(s ConnectionSettings) (*Connection, error) {
logp.Info("kerberos client created")
}

return &Connection{
conn := Connection{
ConnectionSettings: s,
HTTP: httpClient,
Encoder: encoder,
log: logp.NewLogger("esclientleg"),
}, nil
}

if s.APIKey != "" {
conn.apiKeyAuthHeader = "ApiKey " + base64.StdEncoding.EncodeToString([]byte(s.APIKey))
}

return &conn, nil
}

func settingsWithDefaults(s ConnectionSettings) ConnectionSettings {
Expand Down Expand Up @@ -435,8 +443,8 @@ func (conn *Connection) execHTTPRequest(req *http.Request) (int, []byte, error)
req.SetBasicAuth(conn.Username, conn.Password)
}

if conn.APIKey != "" {
req.Header.Add("Authorization", "ApiKey "+conn.APIKey)
if conn.apiKeyAuthHeader != "" {
req.Header.Add("Authorization", conn.apiKeyAuthHeader)
}

for name, value := range conn.Headers {
Expand Down
66 changes: 66 additions & 0 deletions libbeat/esleg/eslegclient/connection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package eslegclient

import (
"bufio"
"bytes"
"encoding/base64"
"net/http"
"testing"

"github.com/stretchr/testify/require"
)

func TestAPIKeyEncoding(t *testing.T) {
apiKey := "foobar"
encoded := base64.StdEncoding.EncodeToString([]byte(apiKey))

conn, err := NewConnection(ConnectionSettings{
APIKey: apiKey,
})
require.NoError(t, err)

httpClient := newMockClient()
conn.HTTP = httpClient

req, err := http.NewRequest("GET", "http://fakehost/some/path", nil)
require.NoError(t, err)

_, _, err = conn.execHTTPRequest(req)
require.NoError(t, err)

require.Equal(t, "ApiKey "+encoded, httpClient.Req.Header.Get("Authorization"))
}

type mockClient struct {
Req *http.Request
}

func (c *mockClient) Do(req *http.Request) (*http.Response, error) {
c.Req = req

r := bytes.NewReader([]byte("HTTP/1.1 200 OK\n\nHello, world"))
return http.ReadResponse(bufio.NewReader(r), req)
}

func (c *mockClient) CloseIdleConnections() {}

func newMockClient() *mockClient {
return &mockClient{}
}
3 changes: 1 addition & 2 deletions libbeat/outputs/elasticsearch/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package elasticsearch

import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -84,7 +83,7 @@ func NewClient(
URL: s.URL,
Username: s.Username,
Password: s.Password,
APIKey: base64.StdEncoding.EncodeToString([]byte(s.APIKey)),
APIKey: s.APIKey,
Headers: s.Headers,
TLS: s.TLS,
Kerberos: s.Kerberos,
Expand Down
9 changes: 5 additions & 4 deletions libbeat/outputs/elasticsearch/docs/elasticsearch.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ output.elasticsearch:
password: "{pwd}"
------------------------------------------------------------------------------

To use an API key to connect to {es}, use `api_key`.
To use an API key to connect to {es}, use `api_key`. The value must be the ID of
the API key and the API key joined by a colon.

["source","yaml",subs="attributes,callouts"]
------------------------------------------------------------------------------
output.elasticsearch:
hosts: ["https://localhost:9200"]
api_key: "KnR6yE41RrSowb0kQ0HWoA"
api_key: "VuaCfGcBCdbkQm-e5aOx:ui2lp2axTNmsyakw9tvNnw"
------------------------------------------------------------------------------

If the Elasticsearch nodes are defined by `IP:PORT`, then add `protocol: https` to the yaml file.
Expand Down Expand Up @@ -135,8 +136,8 @@ The default value is 1.

===== `api_key`

Instead of using usernames and passwords,
you can use API keys to secure communication with {es}.
Instead of using usernames and passwords, you can use API keys to secure communication
with {es}. The value must be the ID of the API key and the API key joined by a colon.
For more information, see <<beats-api-keys>>.

===== `username`
Expand Down

0 comments on commit 67e4bf4

Please sign in to comment.