Skip to content

Commit

Permalink
metricbeat/module/redis: Add TLS and username support for Redis module (
Browse files Browse the repository at this point in the history
  • Loading branch information
shmsr authored Jun 15, 2023
1 parent e0150c5 commit 139a8bf
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-developer.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ The list below covers the major changes between 7.0.0-rc2 and main only.
- Add Okta API package for entity analytics. {pull}35478[35478]
- Add benchmarking to HTTPJSON input testing. {pull}35138[35138]
- Allow non-AWS endpoints for testing Filebeat awss3 input. {issue}35496[35496] {pull}35520[35520]
- Add AUTH (username) and SSL/TLS support for Redis module {pull}35240[35240]

==== Deprecated

Expand Down
17 changes: 16 additions & 1 deletion metricbeat/docs/modules/redis.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,23 @@ metricbeat.modules:
# - include_fields:
# fields: ["beat", "metricset", "redis.info.stats"]
# Redis AUTH username (Redis 6.0+). Empty by default.
#username: user
# Redis AUTH password. Empty by default.
#password: foobared
#password: pass
# Optional SSL/TLS (Redis 6.0+). By default is false.
#ssl.enabled: true
# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]
# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"
# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"
----

[float]
Expand Down
17 changes: 16 additions & 1 deletion metricbeat/metricbeat.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -930,8 +930,23 @@ metricbeat.modules:
# - include_fields:
# fields: ["beat", "metricset", "redis.info.stats"]

# Redis AUTH username (Redis 6.0+). Empty by default.
#username: user

# Redis AUTH password. Empty by default.
#password: foobared
#password: pass

# Optional SSL/TLS (Redis 6.0+). By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"

#------------------------------- Traefik Module -------------------------------
- module: traefik
Expand Down
17 changes: 16 additions & 1 deletion metricbeat/module/redis/_meta/config.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,20 @@
# - include_fields:
# fields: ["beat", "metricset", "redis.info.stats"]

# Redis AUTH username (Redis 6.0+). Empty by default.
#username: user

# Redis AUTH password. Empty by default.
#password: foobared
#password: pass

# Optional SSL/TLS (Redis 6.0+). By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"
17 changes: 16 additions & 1 deletion metricbeat/module/redis/_meta/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,20 @@
# Max number of concurrent connections. Default: 10
#maxconn: 10

# Redis AUTH username (Redis 6.0+). Empty by default.
#username: user

# Redis AUTH password. Empty by default.
#password: foobared
#password: pass

# Optional SSL/TLS (Redis 6.0+). By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"
39 changes: 39 additions & 0 deletions metricbeat/module/redis/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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 redis

import (
"crypto/tls"
"time"

"github.com/elastic/elastic-agent-libs/transport/tlscommon"
)

type Config struct {
IdleTimeout time.Duration `config:"idle_timeout"`
Network string `config:"network"`
MaxConn int `config:"maxconn" validate:"min=1"`
TLS *tlscommon.Config `config:"ssl"`

UseTLSConfig *tls.Config
}

// DefaultConfig return default config for the redis module.
func DefaultConfig() Config {
return Config{Network: "tcp", MaxConn: 10}
}
78 changes: 54 additions & 24 deletions metricbeat/module/redis/metricset.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
package redis

import (
"fmt"
"net/url"
"strconv"
"strings"
"time"

rd "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"

"github.com/elastic/beats/v7/metricbeat/mb"
"github.com/elastic/elastic-agent-libs/transport/tlscommon"
)

// MetricSet for fetching Redis server information and statistics.
Expand All @@ -35,32 +35,39 @@ type MetricSet struct {
pool *Pool
}

// NewMetricSet creates the base for Redis metricsets
// NewMetricSet creates the base for Redis metricsets.
func NewMetricSet(base mb.BaseMetricSet) (*MetricSet, error) {
// Unpack additional configuration options.
config := struct {
IdleTimeout time.Duration `config:"idle_timeout"`
Network string `config:"network"`
MaxConn int `config:"maxconn" validate:"min=1"`
}{
Network: "tcp",
MaxConn: 10,
}
config := DefaultConfig()

err := base.Module().UnpackConfig(&config)
if err != nil {
return nil, errors.Wrap(err, "failed to read configuration")
return nil, fmt.Errorf("failed to read configuration: %w", err)
}

password, dbNumber, err := getPasswordDBNumber(base.HostData())
username, password, dbNumber, err := getUsernamePasswordDBNumber(base.HostData())
if err != nil {
return nil, errors.Wrap(err, "failed to getPasswordDBNumber from URI")
return nil, fmt.Errorf("failed to parse username, password and dbNumber from URI: %w", err)
}

if config.TLS.IsEnabled() {
tlsConfig, err := tlscommon.LoadTLSConfig(config.TLS)
if err != nil {
return nil, fmt.Errorf("could not load provided TLS configuration: %w", err)
}
config.UseTLSConfig = tlsConfig.ToConfig()
}

return &MetricSet{
BaseMetricSet: base,
pool: CreatePool(base.Host(), password, config.Network, dbNumber,
config.MaxConn, config.IdleTimeout, base.Module().Config().Timeout),
pool: CreatePool(
base.Host(),
username,
password,
dbNumber,
&config,
base.Module().Config().Timeout,
),
}, nil
}

Expand All @@ -80,11 +87,27 @@ func (m *MetricSet) OriginalDBNumber() uint {
return uint(m.pool.DBNumber())
}

func getPasswordDBNumber(hostData mb.HostData) (string, int, error) {
// If there are more than one place specified password/db-number, use password/db-number in query
// getUserPasswordDBNumber parses username, password and dbNumber from URI or else default
// is used (mentioned in config).
//
// As per security consideration RFC-2396: Uniform Resource Identifiers (URI): Generic Syntax
// https://www.rfc-editor.org/rfc/rfc2396.html#section-7
//
// """
// It is clearly unwise to use a URL that contains a password which is
// intended to be secret. In particular, the use of a password within
// the 'userinfo' component of a URL is strongly disrecommended except
// in those rare cases where the 'password' parameter is intended to be
// public.
// """
//
// In some environments, this is safe but not all. We shouldn't ideally take
// username and password from URI's userinfo or query parameters.
func getUsernamePasswordDBNumber(hostData mb.HostData) (string, string, int, error) {
// If there are more than one place specified username/password/db-number, use username/password/db-number in query
uriParsed, err := url.Parse(hostData.URI)
if err != nil {
return "", 0, errors.Wrapf(err, "failed to parse URL '%s'", hostData.URI)
return "", "", 0, fmt.Errorf("failed to parse URL '%s': %w", hostData.URI, err)
}

// get db-number from URI if it exists
Expand All @@ -94,17 +117,23 @@ func getPasswordDBNumber(hostData mb.HostData) (string, int, error) {
if db != "" {
database, err = strconv.Atoi(db)
if err != nil {
return "", 0, errors.Wrapf(err, "redis database in url should be an integer, found: %s", db)
return "", "", 0, fmt.Errorf("redis database in url should be an integer, found: %s: %w", db, err)
}
}
}

// get password from query and also check db-number
// get username and password from query and also check db-number
password := hostData.Password
username := hostData.User
if uriParsed.RawQuery != "" {
queryParsed, err := url.ParseQuery(uriParsed.RawQuery)
if err != nil {
return "", 0, errors.Wrapf(err, "failed to parse query string in '%s'", hostData.URI)
return "", "", 0, fmt.Errorf("failed to parse query string in '%s': %w", hostData.URI, err)
}

usr := queryParsed.Get("username")
if usr != "" {
username = usr
}

pw := queryParsed.Get("password")
Expand All @@ -116,9 +145,10 @@ func getPasswordDBNumber(hostData mb.HostData) (string, int, error) {
if db != "" {
database, err = strconv.Atoi(db)
if err != nil {
return "", 0, errors.Wrapf(err, "redis database in query should be an integer, found: %s", db)
return "", "", 0, fmt.Errorf("redis database in query should be an integer, found: %s: %w", db, err)
}
}
}
return password, database, nil

return username, password, database, nil
}
37 changes: 30 additions & 7 deletions metricbeat/module/redis/metricset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,57 +31,80 @@ func TestGetPasswordDBNumber(t *testing.T) {
cases := []struct {
title string
hostData mb.HostData
expectedUser string
expectedPassword string
expectedDatabase int
}{
{
"test redis://127.0.0.1:6379 without password",
mb.HostData{URI: "redis://127.0.0.1:6379", Password: ""},
"",
"",
0,
},
{
"test redis uri with password in URI user info field",
"test redis URI with password in userinfo",
mb.HostData{URI: "redis://:[email protected]:6379", Password: "password"},
"",
"password",
0,
},
{
"test redis uri with password in query field",
"test redis URI with password in query parameter",
mb.HostData{URI: "redis://127.0.0.1:6379?password=test", Password: ""},
"",
"test",
0,
},
{
"test redis uri with password and db in query field",
"test redis URI with password and db in query parameter",
mb.HostData{URI: "redis://127.0.0.1:6379?password=test&db=1", Password: ""},
"",
"test",
1,
},
{
"test redis uri with password in URI user info field and query field",
"test redis URI with password in userinfo and URI's query parameter",
mb.HostData{URI: "redis://:[email protected]:6379?password=password2", Password: "password1"},
"",
"password2",
0,
},
{
"test redis uri with db number in URI",
"test redis URI with db number in URI's query parameter and password in userinfo",
mb.HostData{URI: "redis://:[email protected]:6379/1", Password: "password1"},
"",
"password1",
1,
},
{
"test redis uri with db number in URI and query field",
"test redis URI with db number and password in URI's query parameter and password in userinfo",
mb.HostData{URI: "redis://:[email protected]:6379/1?password=password2&db=2", Password: "password1"},
"",
"password2",
2,
},
{
"test redis URI with db number, user and password in URI's query parameter and password in userinfo",
mb.HostData{URI: "redis://antirez:[email protected]:6379/1?password=password2&db=2", User: "antirez", Password: "password1"},
"antirez",
"password2",
2,
},
{
"test redis URI with db number, user & password in URI's query parameter and user & password in userinfo",
mb.HostData{URI: "redis://antirez:[email protected]:6379/1?username=salvatore&password=password2&db=2", User: "antirez", Password: "password1"},
"salvatore",
"password2",
2,
},
}

for _, c := range cases {
t.Run(c.title, func(t *testing.T) {
password, database, err := getPasswordDBNumber(c.hostData)
username, password, database, err := getUsernamePasswordDBNumber(c.hostData)
assert.NoError(t, err)
assert.Equal(t, c.expectedUser, username)
assert.Equal(t, c.expectedPassword, password)
assert.Equal(t, c.expectedDatabase, database)
})
Expand Down
Loading

0 comments on commit 139a8bf

Please sign in to comment.