Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom CA support for azure #4974

Merged
merged 3 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ We use *breaking :warning:* to mark changes that are not backward compatible (re
- [#4917](https://github.com/thanos-io/thanos/pull/4917) Query: add initial query pushdown for a subset of aggregations. Can be enabled with `--enable-feature=query-pushdown` on Thanos Query.
- [#4888](https://github.com/thanos-io/thanos/pull/4888) Cache: support redis cache backend.
- [#4946](https://github.com/thanos-io/thanos/pull/4946) Store: Support tls_config configuration for the s3 minio client.
- [#4974](https://github.com/thanos-io/thanos/pull/4974) Store: Support tls_config configuration for connecting with Azure storage.

### Fixed

Expand Down
6 changes: 6 additions & 0 deletions docs/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,12 @@ config:
max_idle_conns_per_host: 0
max_conns_per_host: 0
disable_compression: false
tls_config:
ca_file: ""
cert_file: ""
key_file: ""
server_name: ""
insecure_skip_verify: false
```

If `msi_resource` is used, authentication is done via system-assigned managed identity. The value for Azure should be `https://<storage-account-name>.blob.core.windows.net`.
Expand Down
2 changes: 2 additions & 0 deletions pkg/objstore/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ type HTTPConfig struct {
MaxIdleConnsPerHost int `yaml:"max_idle_conns_per_host"`
MaxConnsPerHost int `yaml:"max_conns_per_host"`
DisableCompression bool `yaml:"disable_compression"`

TLSConfig objstore.TLSConfig `yaml:"tls_config"`
}

// Bucket implements the store.Bucket interface against Azure APIs.
Expand Down
40 changes: 40 additions & 0 deletions pkg/objstore/azure/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,43 @@ func TestParseConfig_DefaultHTTPConfig(t *testing.T) {
t.Errorf("parsing of insecure_skip_verify failed: got %v, expected %v", cfg.HTTPConfig.InsecureSkipVerify, false)
}
}

func TestParseConfig_CustomHTTPConfigWithTLS(t *testing.T) {
input := []byte(`storage_account: "myStorageAccount"
storage_account_key: "abc123"
container: "MyContainer"
endpoint: "blob.core.windows.net"
http_config:
tls_config:
ca_file: /certs/ca.crt
cert_file: /certs/cert.crt
key_file: /certs/key.key
server_name: server
insecure_skip_verify: false
`)
cfg, err := parseConfig(input)
testutil.Ok(t, err)

testutil.Equals(t, "/certs/ca.crt", cfg.HTTPConfig.TLSConfig.CAFile)
testutil.Equals(t, "/certs/cert.crt", cfg.HTTPConfig.TLSConfig.CertFile)
testutil.Equals(t, "/certs/key.key", cfg.HTTPConfig.TLSConfig.KeyFile)
testutil.Equals(t, "server", cfg.HTTPConfig.TLSConfig.ServerName)
testutil.Equals(t, false, cfg.HTTPConfig.TLSConfig.InsecureSkipVerify)
}

func TestParseConfig_CustomLegacyInsecureSkipVerify(t *testing.T) {
input := []byte(`storage_account: "myStorageAccount"
storage_account_key: "abc123"
container: "MyContainer"
endpoint: "blob.core.windows.net"
http_config:
insecure_skip_verify: true
tls_config:
insecure_skip_verify: false
`)
cfg, err := parseConfig(input)
testutil.Ok(t, err)
transport, err := DefaultTransport(cfg)
testutil.Ok(t, err)
testutil.Equals(t, true, transport.TLSClientConfig.InsecureSkipVerify)
}
22 changes: 17 additions & 5 deletions pkg/objstore/azure/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package azure

import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
Expand All @@ -19,6 +18,7 @@ import (
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/thanos-io/thanos/pkg/objstore"
)

// DirDelim is the delimiter used to model a directory structure in an object store bucket.
Expand Down Expand Up @@ -104,8 +104,12 @@ func getContainerURL(ctx context.Context, logger log.Logger, conf Config) (blob.
retryOptions.TryTimeout = time.Until(deadline)
}

dt, err := DefaultTransport(conf)
if err != nil {
return blob.ContainerURL{}, err
}
client := http.Client{
Transport: DefaultTransport(conf),
Transport: dt,
}

p := blob.NewPipeline(credentials, blob.PipelineOptions{
Expand Down Expand Up @@ -136,7 +140,15 @@ func getContainerURL(ctx context.Context, logger log.Logger, conf Config) (blob.
return service.NewContainerURL(conf.ContainerName), nil
}

func DefaultTransport(config Config) *http.Transport {
func DefaultTransport(config Config) (*http.Transport, error) {
tlsConfig, err := objstore.NewTLSConfig(&config.HTTPConfig.TLSConfig)
if err != nil {
return nil, err
}

if config.HTTPConfig.InsecureSkipVerify {
tlsConfig.InsecureSkipVerify = true
}
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Expand All @@ -154,8 +166,8 @@ func DefaultTransport(config Config) *http.Transport {

ResponseHeaderTimeout: time.Duration(config.HTTPConfig.ResponseHeaderTimeout),
DisableCompression: config.HTTPConfig.DisableCompression,
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.HTTPConfig.InsecureSkipVerify},
}
TLSClientConfig: tlsConfig,
}, nil
}

func getContainer(ctx context.Context, logger log.Logger, conf Config) (blob.ContainerURL, error) {
Expand Down
82 changes: 2 additions & 80 deletions pkg/objstore/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ package s3

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -119,91 +117,15 @@ type HTTPConfig struct {
// Allow upstream callers to inject a round tripper
Transport http.RoundTripper `yaml:"-"`

TLSConfig TLSConfig `yaml:"tls_config"`
}

// NewTLSConfig creates a new tls.Config from the given TLSConfig.
func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) {
tlsConfig := &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify}

// If a CA cert is provided then let's read it in.
if len(cfg.CAFile) > 0 {
b, err := readCAFile(cfg.CAFile)
if err != nil {
return nil, err
}
if !updateRootCA(tlsConfig, b) {
return nil, fmt.Errorf("unable to use specified CA cert %s", cfg.CAFile)
}
}

if len(cfg.ServerName) > 0 {
tlsConfig.ServerName = cfg.ServerName
}
// If a client cert & key is provided then configure TLS config accordingly.
if len(cfg.CertFile) > 0 && len(cfg.KeyFile) == 0 {
return nil, fmt.Errorf("client cert file %q specified without client key file", cfg.CertFile)
} else if len(cfg.KeyFile) > 0 && len(cfg.CertFile) == 0 {
return nil, fmt.Errorf("client key file %q specified without client cert file", cfg.KeyFile)
} else if len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0 {
// Verify that client cert and key are valid.
if _, err := cfg.getClientCertificate(nil); err != nil {
return nil, err
}
tlsConfig.GetClientCertificate = cfg.getClientCertificate
}

return tlsConfig, nil
}

// readCAFile reads the CA cert file from disk.
func readCAFile(f string) ([]byte, error) {
data, err := ioutil.ReadFile(f)
if err != nil {
return nil, fmt.Errorf("unable to load specified CA cert %s: %s", f, err)
}
return data, nil
}

// updateRootCA parses the given byte slice as a series of PEM encoded certificates and updates tls.Config.RootCAs.
func updateRootCA(cfg *tls.Config, b []byte) bool {
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(b) {
return false
}
cfg.RootCAs = caCertPool
return true
}

// getClientCertificate reads the pair of client cert and key from disk and returns a tls.Certificate.
func (c *TLSConfig) getClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
if err != nil {
return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", c.CertFile, c.KeyFile, err)
}
return &cert, nil
}

// TLSConfig configures the options for TLS connections.
type TLSConfig struct {
// The CA cert to use for the targets.
CAFile string `yaml:"ca_file"`
// The client cert file for the targets.
CertFile string `yaml:"cert_file"`
// The client key file for the targets.
KeyFile string `yaml:"key_file"`
// Used to verify the hostname for the targets.
ServerName string `yaml:"server_name"`
// Disable target certificate validation.
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
TLSConfig objstore.TLSConfig `yaml:"tls_config"`
}

// DefaultTransport - this default transport is based on the Minio
// DefaultTransport up until the following commit:
// https://github.com/minio/minio-go/commit/008c7aa71fc17e11bf980c209a4f8c4d687fc884
// The values have since diverged.
func DefaultTransport(config Config) (*http.Transport, error) {
tlsConfig, err := NewTLSConfig(&config.HTTPConfig.TLSConfig)
tlsConfig, err := objstore.NewTLSConfig(&config.HTTPConfig.TLSConfig)
if err != nil {
return nil, err
}
Expand Down
87 changes: 87 additions & 0 deletions pkg/objstore/tlsconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) The Thanos Authors.
// Licensed under the Apache License 2.0.

package objstore

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
)

// NewTLSConfig creates a new tls.Config from the given TLSConfig.
func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) {
tlsConfig := &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify}

// If a CA cert is provided then let's read it in.
if len(cfg.CAFile) > 0 {
b, err := readCAFile(cfg.CAFile)
if err != nil {
return nil, err
}
if !updateRootCA(tlsConfig, b) {
return nil, fmt.Errorf("unable to use specified CA cert %s", cfg.CAFile)
}
}

if len(cfg.ServerName) > 0 {
tlsConfig.ServerName = cfg.ServerName
}
// If a client cert & key is provided then configure TLS config accordingly.
if len(cfg.CertFile) > 0 && len(cfg.KeyFile) == 0 {
return nil, fmt.Errorf("client cert file %q specified without client key file", cfg.CertFile)
} else if len(cfg.KeyFile) > 0 && len(cfg.CertFile) == 0 {
return nil, fmt.Errorf("client key file %q specified without client cert file", cfg.KeyFile)
} else if len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0 {
// Verify that client cert and key are valid.
if _, err := cfg.getClientCertificate(nil); err != nil {
return nil, err
}
tlsConfig.GetClientCertificate = cfg.getClientCertificate
}

return tlsConfig, nil
}

// readCAFile reads the CA cert file from disk.
func readCAFile(f string) ([]byte, error) {
data, err := ioutil.ReadFile(f)
if err != nil {
return nil, fmt.Errorf("unable to load specified CA cert %s: %s", f, err)
}
return data, nil
}

// updateRootCA parses the given byte slice as a series of PEM encoded certificates and updates tls.Config.RootCAs.
func updateRootCA(cfg *tls.Config, b []byte) bool {
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(b) {
return false
}
cfg.RootCAs = caCertPool
return true
}

// getClientCertificate reads the pair of client cert and key from disk and returns a tls.Certificate.
func (c *TLSConfig) getClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
if err != nil {
return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", c.CertFile, c.KeyFile, err)
}
return &cert, nil
}

// TLSConfig configures the options for TLS connections.
type TLSConfig struct {
// The CA cert to use for the targets.
CAFile string `yaml:"ca_file"`
// The client cert file for the targets.
CertFile string `yaml:"cert_file"`
// The client key file for the targets.
KeyFile string `yaml:"key_file"`
// Used to verify the hostname for the targets.
ServerName string `yaml:"server_name"`
// Disable target certificate validation.
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
}