Skip to content

Commit

Permalink
misc: add support for inline TLS settings for Prometheus-based compon…
Browse files Browse the repository at this point in the history
…ents (#3524)

This updates the prometheus/common dependency to use a fork until
prometheus/common#472 is merged.

Components which use the prometheus/common dependency for TLS configs
will automatically support inline TLS settings. Some components,
particularly loki.source.kafka and loki.source.syslog had to be updated
as they build their own TLS configs.
  • Loading branch information
rfratto authored and clayton-cornell committed Aug 14, 2023
1 parent 65e702f commit e06a366
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 81 deletions.
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Main (unreleased)
- `module.file` runs a Grafana Agent Flow module passed to the component by
an expression containing a file. (@erikbaranowski)
- `otelcol.auth.oauth2` performs OAuth 2.0 authentication for HTTP and gRPC
based OpenTelemetry exporters. (@ptodev)
based OpenTelemetry exporters. (@ptodev)
- `otelcol.extension.jaeger_remote_sampling` provides an endpoint from which to
pull Jaeger remote sampling documents. (@joe-elliott)
- `prometheus.exporter.blackbox` collects metrics from Blackbox exporter. (@marctc)
Expand Down Expand Up @@ -88,6 +88,27 @@ Main (unreleased)
- Agent Management: Introduces backpressure mechanism for remote config fetching (obeys 429 request
`Retry-After` header). (@spartan0x117)

- Flow: support client TLS settings (CA, client certificate, client key) being
provided from other components for the following components:

- `discovery.docker`
- `discovery.kubernetes`
- `loki.source.kafka`
- `loki.source.kubernetes`
- `loki.source.podlogs`
- `loki.write`
- `mimir.rules.kubernetes`
- `phlare.scrape`
- `phlare.write`
- `prometheus.remote_write`
- `prometheus.scrape`
- `remote.http`

- Flow: support server TLS settings (client CA, server certificate, server key)
being provided from other components for the following components:

- `loki.source.syslog`

### Bugfixes

- Flow: fix issue where Flow would return an error when trying to access a key
Expand Down
63 changes: 55 additions & 8 deletions component/common/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,12 @@ func (h *HTTPClientConfig) Convert() *config.HTTPClientConfig {
OAuth2: h.OAuth2.Convert(),
BearerToken: config.Secret(h.BearerToken),
BearerTokenFile: h.BearerTokenFile,
ProxyURL: h.ProxyURL.Convert(),
TLSConfig: *h.TLSConfig.Convert(),
FollowRedirects: h.FollowRedirects,
EnableHTTP2: h.EnableHTTP2,
ProxyConfig: config.ProxyConfig{
ProxyURL: h.ProxyURL.Convert(),
},
}
}

Expand Down Expand Up @@ -231,12 +233,26 @@ func (tv *TLSVersion) UnmarshalText(text []byte) error {

// TLSConfig sets up options for TLS connections.
type TLSConfig struct {
CAFile string `river:"ca_file,attr,optional"`
CertFile string `river:"cert_file,attr,optional"`
KeyFile string `river:"key_file,attr,optional"`
ServerName string `river:"server_name,attr,optional"`
InsecureSkipVerify bool `river:"insecure_skip_verify,attr,optional"`
MinVersion TLSVersion `river:"min_version,attr,optional"`
CA string `river:"ca_pem,attr,optional"`
CAFile string `river:"ca_file,attr,optional"`
Cert string `river:"cert_pem,attr,optional"`
CertFile string `river:"cert_file,attr,optional"`
Key rivertypes.Secret `river:"key_pem,attr,optional"`
KeyFile string `river:"key_file,attr,optional"`
ServerName string `river:"server_name,attr,optional"`
InsecureSkipVerify bool `river:"insecure_skip_verify,attr,optional"`
MinVersion TLSVersion `river:"min_version,attr,optional"`
}

// UnmarshalRiver implements river.Unmarshaler and reports whether the
// unmarshaled TLSConfig is valid.
func (t *TLSConfig) UnmarshalRiver(f func(interface{}) error) error {
type tlsConfig TLSConfig
if err := f((*tlsConfig)(t)); err != nil {
return err
}

return t.Validate()
}

// Convert converts our type to the native prometheus type
Expand All @@ -245,15 +261,44 @@ func (t *TLSConfig) Convert() *config.TLSConfig {
return nil
}
return &config.TLSConfig{
CA: t.CA,
CAFile: t.CAFile,
Cert: t.Cert,
CertFile: t.CertFile,
Key: config.Secret(t.Key),
KeyFile: t.KeyFile,
ServerName: t.ServerName,
InsecureSkipVerify: t.InsecureSkipVerify,
MinVersion: config.TLSVersion(t.MinVersion),
}
}

// Validate reports whether t is valid.
func (t *TLSConfig) Validate() error {
if len(t.CA) > 0 && len(t.CAFile) > 0 {
return fmt.Errorf("at most one of ca_pem and ca_file must be configured")
}
if len(t.Cert) > 0 && len(t.CertFile) > 0 {
return fmt.Errorf("at most one of cert_pem and cert_file must be configured")
}
if len(t.Key) > 0 && len(t.KeyFile) > 0 {
return fmt.Errorf("at most one of key_pem and key_file must be configured")
}

var (
usingClientCert = len(t.Cert) > 0 || len(t.CertFile) > 0
usingClientKey = len(t.Key) > 0 || len(t.KeyFile) > 0
)

if usingClientCert && !usingClientKey {
return fmt.Errorf("exactly one of key_pem or key_file must be configured when a client certificate is configured")
} else if usingClientKey && !usingClientCert {
return fmt.Errorf("exactly one of cert_pem or cert_file must be configured when a client key is configured")
}

return nil
}

// OAuth2Config sets up the OAuth2 client.
type OAuth2Config struct {
ClientID string `river:"client_id,attr,optional"`
Expand All @@ -278,7 +323,9 @@ func (o *OAuth2Config) Convert() *config.OAuth2 {
Scopes: o.Scopes,
TokenURL: o.TokenURL,
EndpointParams: o.EndpointParams,
ProxyURL: o.ProxyURL.Convert(),
TLSConfig: *o.TLSConfig.Convert(),
ProxyConfig: config.ProxyConfig{
ProxyURL: o.ProxyURL.Convert(),
},
}
}
30 changes: 0 additions & 30 deletions component/loki/source/internal/kafkatarget/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,10 @@ package kafkatarget
import (
"crypto/sha256"
"crypto/sha512"
"crypto/tls"
"crypto/x509"
"os"

promconfig "github.com/prometheus/common/config"
"github.com/xdg-go/scram"
)

func createTLSConfig(cfg promconfig.TLSConfig) (*tls.Config, error) {
tc := &tls.Config{
InsecureSkipVerify: cfg.InsecureSkipVerify,
ServerName: cfg.ServerName,
}
// load ca cert
if len(cfg.CAFile) > 0 {
caCert, err := os.ReadFile(cfg.CAFile)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tc.RootCAs = caCertPool
}
// load client cert
if len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0 {
cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
if err != nil {
return nil, err
}
tc.Certificates = []tls.Certificate{cert}
}
return tc, nil
}

// copied from https://github.com/Shopify/sarama/blob/44627b731c60bb90efe25573e7ef2b3f8df3fa23/examples/sasl_scram_client/scram_client.go
var (
SHA256 scram.HashGeneratorFcn = sha256.New
Expand Down
5 changes: 3 additions & 2 deletions component/loki/source/internal/kafkatarget/target_syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
promconfig "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/model/labels"

Expand Down Expand Up @@ -136,7 +137,7 @@ func withAuthentication(cfg sarama.Config, authCfg Authentication) (*sarama.Conf

func withSSLAuthentication(cfg sarama.Config, authCfg Authentication) (*sarama.Config, error) {
cfg.Net.TLS.Enable = true
tc, err := createTLSConfig(authCfg.TLSConfig)
tc, err := promconfig.NewTLSConfig(&authCfg.TLSConfig)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -187,7 +188,7 @@ func withSASLAuthentication(cfg sarama.Config, authCfg Authentication) (*sarama.
}

if authCfg.SASLConfig.UseTLS {
tc, err := createTLSConfig(authCfg.SASLConfig.TLSConfig)
tc, err := promconfig.NewTLSConfig(&authCfg.SASLConfig.TLSConfig)
if err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ func Test_withAuthentication(t *testing.T) {
ServerName: "example.com",
InsecureSkipVerify: true,
}
expectedTLSConf, _ = createTLSConfig(config.TLSConfig{
expectedTLSConf, _ = config.NewTLSConfig(&config.TLSConfig{
CAFile: "testdata/example.com.ca.pem",
CertFile: "testdata/example.com.pem",
KeyFile: "testdata/example.com-key.pem",
Expand Down
65 changes: 56 additions & 9 deletions component/loki/source/syslog/internal/syslogtarget/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/influxdata/go-syslog/v3"
"github.com/prometheus/common/config"
"github.com/prometheus/prometheus/model/labels"

"github.com/grafana/loki/clients/pkg/promtail/scrapeconfig"
Expand Down Expand Up @@ -179,9 +180,18 @@ func (t *TCPTransport) Run() error {
return fmt.Errorf("error setting up syslog target: %w", err)
}

tlsEnabled := t.config.TLSConfig.CertFile != "" || t.config.TLSConfig.KeyFile != "" || t.config.TLSConfig.CAFile != ""
var (
tlsConfig = t.config.TLSConfig

configuredCA = len(tlsConfig.CA) > 0 || len(tlsConfig.CAFile) > 0
configuredCert = len(tlsConfig.Cert) > 0 || len(tlsConfig.CertFile) > 0
configuredKey = len(tlsConfig.Key) > 0 || len(tlsConfig.KeyFile) > 0

tlsEnabled = configuredCA || configuredCert || configuredKey
)

if tlsEnabled {
tlsConfig, err := newTLSConfig(t.config.TLSConfig.CertFile, t.config.TLSConfig.KeyFile, t.config.TLSConfig.CAFile)
tlsConfig, err := newTLSConfig(tlsConfig)
if err != nil {
return fmt.Errorf("error setting up syslog target: %w", err)
}
Expand All @@ -197,12 +207,42 @@ func (t *TCPTransport) Run() error {
return nil
}

func newTLSConfig(certFile string, keyFile string, caFile string) (*tls.Config, error) {
if certFile == "" || keyFile == "" {
return nil, fmt.Errorf("certificate and key files are required")
// newTLSConfig creates TLS server settings from a [config.TLSConfig]. Use this
// function to create TLS server settings, and [config.NewTLSConfig] to create
// TLS client settings.
func newTLSConfig(config config.TLSConfig) (*tls.Config, error) {
var (
configuredCert = len(config.Cert) > 0 || len(config.CertFile) > 0
configuredKey = len(config.Key) > 0 || len(config.KeyFile) > 0
)

if !configuredCert || !configuredKey {
return nil, fmt.Errorf("certificate and key must be configured")
}

certs, err := tls.LoadX509KeyPair(certFile, keyFile)
var certBytes, keyBytes []byte

if len(config.CertFile) > 0 {
bb, err := os.ReadFile(config.CertFile)
if err != nil {
return nil, fmt.Errorf("unable to load server certificate: %w", err)
}
certBytes = bb
} else if len(config.Cert) > 0 {
certBytes = []byte(config.Cert)
}

if len(config.KeyFile) > 0 {
bb, err := os.ReadFile(config.KeyFile)
if err != nil {
return nil, fmt.Errorf("unable to load server key: %w", err)
}
keyBytes = bb
} else if len(config.Key) > 0 {
keyBytes = []byte(config.Key)
}

certs, err := tls.X509KeyPair(certBytes, keyBytes)
if err != nil {
return nil, fmt.Errorf("unable to load server certificate or key: %w", err)
}
Expand All @@ -211,14 +251,21 @@ func newTLSConfig(certFile string, keyFile string, caFile string) (*tls.Config,
Certificates: []tls.Certificate{certs},
}

if caFile != "" {
caCert, err := os.ReadFile(caFile)
var caBytes []byte

if len(config.CAFile) > 0 {
bb, err := os.ReadFile(config.CAFile)
if err != nil {
return nil, fmt.Errorf("unable to load client CA certificate: %w", err)
}
caBytes = bb
} else if len(config.CA) > 0 {
caBytes = []byte(config.CA)
}

if len(caBytes) > 0 {
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
if ok := caCertPool.AppendCertsFromPEM(caBytes); !ok {
return nil, fmt.Errorf("unable to parse client CA certificate")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,13 @@ func TestGeneratePodMonitorConfig(t *testing.T) {
HTTPClientConfig: commonConfig.HTTPClientConfig{
FollowRedirects: falseVal,
EnableHTTP2: false,
ProxyURL: commonConfig.URL{URL: &url.URL{Scheme: "https", Host: "proxy:8080"}},
TLSConfig: commonConfig.TLSConfig{
ServerName: "foo.com",
InsecureSkipVerify: true,
},
ProxyConfig: commonConfig.ProxyConfig{
ProxyURL: commonConfig.URL{URL: &url.URL{Scheme: "https", Host: "proxy:8080"}},
},
},
ServiceDiscoveryConfigs: discovery.Configs{
&promk8s.SDConfig{
Expand Down
14 changes: 14 additions & 0 deletions docs/sources/shared/flow/reference/components/tls-config-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@ headless: true

Name | Type | Description | Default | Required
---- | ---- | ----------- | ------- | --------
`ca_pem` | `string` | CA PEM-encoded text to validate the server with. | | no
`ca_file` | `string` | CA certificate to validate the server with. | | no
`cert_pem` | `string` | Certificate PEM-encoded text for client authentication. | | no
`cert_file` | `string` | Certificate file for client authentication. | | no
`key_pem` | `secret` | Key PEM-encoded text for client authentication. | | no
`key_file` | `string` | Key file for client authentication. | | no
`server_name` | `string` | ServerName extension to indicate the name of the server. | | no
`insecure_skip_verify` | `bool` | Disables validation of the server certificate. | | no
`min_version` | `string` | Minimum acceptable TLS version. | | no

The following pairs of arguments are mutually exclusive and cannot both be set
simultaneously:

* `ca_pem` and `ca_file`
* `cert_pem` and `cert_file`
* `key_pem` and `key_file`

When configuring client authentication, both the client certificate (using
`cert_pem` or `cert_file`) and the client key (using `key_pem` or `key_file`)
must be provided.

When `min_version` is not provided, the minimum acceptable TLS version is
inherited from Go's default minimum version, TLS 1.2. If `min_version` is
provided, it must be set to one of the following strings:
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -704,3 +704,6 @@ replace github.com/github/smimesign => github.com/grafana/smimesign v0.2.1-0.202
// TODO(rfratto): remove once a new version of node_exporter is available that
// uses a newer version of procfs.
replace github.com/prometheus/procfs => github.com/prometheus/procfs v0.8.0

// TODO(rfratto): remove once prometheus/common#472 is merged.
replace github.com/prometheus/common => github.com/grafana/prometheus-common v0.39.1-0.20230411174203-bcb00f1c26d7
Loading

0 comments on commit e06a366

Please sign in to comment.