Skip to content

Commit 8447ed9

Browse files
committed
cloud/amazon: add option to skip TLS verification for backup/restore
Previously, backup/restore jobs running against secure, on-premises S3 object stores that use self signed certificate would fail, unless cloudstorage.http.custom_ca is set. However, customers want the ability to bypass the certificate validation in test environments. To address this, we added a new URL parameter (AWS_SKIP_TLS_VERIFY) that can be set in the backup URL to bypass certificate validation. Note that a CA certificate is still required; this parameter means that the client will not verify the certificate. Warning: Use this query parameter with caution, as it creates MITM vulnerabilities. This change also adds a test case in roachtest for verification against a microCeph cluster. Epic: none Fixes: 147910 Release note: None
1 parent 941632b commit 8447ed9

File tree

10 files changed

+191
-58
lines changed

10 files changed

+191
-58
lines changed

pkg/cloud/amazon/aws_kms.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,11 @@ func MakeAWSKMS(ctx context.Context, uri string, env cloud.KMSEnv) (cloud.KMS, e
144144
return nil, errors.New(
145145
"custom endpoints disallowed for aws kms due to --aws-kms-disable-http flag")
146146
}
147-
client, err := cloud.MakeHTTPClient(env.ClusterSettings(), cloud.NilMetrics, "aws", "KMS", "")
147+
client, err := cloud.MakeHTTPClient(env.ClusterSettings(), cloud.NilMetrics,
148+
cloud.Config{
149+
Bucket: "KMS",
150+
Cloud: "aws",
151+
})
148152
if err != nil {
149153
return nil, err
150154
}

pkg/cloud/amazon/s3_storage.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ const (
5959
AWSUsePathStyle = "AWS_USE_PATH_STYLE"
6060
// AWSSkipChecksumParam is the query parameter for SkipChecksum in S3 options.
6161
AWSSkipChecksumParam = "AWS_SKIP_CHECKSUM"
62+
// AWSSkipTLSVerify is the query parameter for skipping certificate verification.
63+
AWSSkipTLSVerify = "AWS_SKIP_TLS_VERIFY"
6264

6365
// AWSServerSideEncryptionMode is the query parameter in an AWS URI, for the
6466
// mode to be used for server side encryption. It can either be AES256 or
@@ -202,7 +204,8 @@ type s3ClientConfig struct {
202204
assumeRoleProvider roleProvider
203205
delegateRoleProviders []roleProvider
204206

205-
skipChecksum bool
207+
skipChecksum bool
208+
skipTLSVerify bool
206209
// log.V(2) decides session init params so include it in key.
207210
verbose bool
208211
}
@@ -236,6 +239,7 @@ func clientConfig(conf *cloudpb.ExternalStorage_S3) s3ClientConfig {
236239
endpoint: conf.Endpoint,
237240
usePathStyle: conf.UsePathStyle,
238241
skipChecksum: conf.SkipChecksum,
242+
skipTLSVerify: conf.SkipTLSVerify,
239243
region: conf.Region,
240244
bucket: conf.Bucket,
241245
accessKey: conf.AccessKey,
@@ -341,7 +345,15 @@ func parseS3URL(uri *url.URL) (cloudpb.ExternalStorage, error) {
341345
return cloudpb.ExternalStorage{}, errors.Wrapf(err, "cannot parse %s as bool", AWSSkipChecksumParam)
342346
}
343347
}
344-
348+
skipTLSVerifyStr := s3URL.ConsumeParam(AWSSkipTLSVerify)
349+
skipTLSVerifyBool := false
350+
if skipTLSVerifyStr != "" {
351+
var err error
352+
skipTLSVerifyBool, err = strconv.ParseBool(skipTLSVerifyStr)
353+
if err != nil {
354+
return cloudpb.ExternalStorage{}, errors.Wrapf(err, "cannot parse %s as bool", AWSSkipTLSVerify)
355+
}
356+
}
345357
conf.S3Config = &cloudpb.ExternalStorage_S3{
346358
Bucket: s3URL.Host,
347359
Prefix: s3URL.Path,
@@ -351,6 +363,7 @@ func parseS3URL(uri *url.URL) (cloudpb.ExternalStorage, error) {
351363
Endpoint: s3URL.ConsumeParam(AWSEndpointParam),
352364
UsePathStyle: pathStyleBool,
353365
SkipChecksum: skipChecksumBool,
366+
SkipTLSVerify: skipTLSVerifyBool,
354367
Region: s3URL.ConsumeParam(S3RegionParam),
355368
Auth: s3URL.ConsumeParam(cloud.AuthParam),
356369
ServerEncMode: s3URL.ConsumeParam(AWSServerSideEncryptionMode),
@@ -573,7 +586,13 @@ func (s *s3Storage) newClient(ctx context.Context) (s3Client, string, error) {
573586
loadOptions = append(loadOptions, option)
574587
}
575588

576-
client, err := cloud.MakeHTTPClient(s.settings, s.metrics, "aws", s.opts.bucket, s.storageOptions.ClientName)
589+
client, err := cloud.MakeHTTPClient(s.settings, s.metrics,
590+
cloud.Config{
591+
Bucket: s.opts.bucket,
592+
Client: s.storageOptions.ClientName,
593+
Cloud: "aws",
594+
InsecureSkipVerify: s.opts.skipTLSVerify,
595+
})
577596
if err != nil {
578597
return s3Client{}, "", err
579598
}

pkg/cloud/amazon/s3_storage_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,53 @@ func TestPutS3Endpoint(t *testing.T) {
442442
t.Fatal(err)
443443
}
444444
})
445+
446+
t.Run("skip-tls-verify", func(t *testing.T) {
447+
ctx := context.Background()
448+
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
449+
defer srv.Close()
450+
q := make(url.Values)
451+
// Convert IP address to `localhost` to exercise the DNS-defining path in the S3 client.
452+
localhostURL := strings.Replace(srv.URL, "127.0.0.1", "localhost", -1)
453+
q.Add(AWSEndpointParam, localhostURL)
454+
q.Add(AWSAccessKeyParam, "key")
455+
q.Add(AWSSecretParam, "secret")
456+
q.Add(S3RegionParam, "region")
457+
q.Add(AWSUsePathStyle, "true")
458+
// Set AWSSkipTLSVerify to use with HTTPS Server with self signed certificates.
459+
q.Add(AWSSkipTLSVerify, "true")
460+
461+
u := url.URL{
462+
Scheme: "s3",
463+
Host: "bucket",
464+
Path: "subdir1/subdir2",
465+
RawQuery: q.Encode(),
466+
}
467+
468+
user := username.RootUserName()
469+
ioConf := base.ExternalIODirConfig{}
470+
471+
conf, err := cloud.ExternalStorageConfFromURI(u.String(), user)
472+
if err != nil {
473+
t.Fatal(err)
474+
}
475+
476+
// Setup a sink for the given args.
477+
testSettings := cluster.MakeTestingClusterSettings()
478+
clientFactory := blobs.TestBlobServiceClient("")
479+
480+
storage, err := cloud.MakeExternalStorage(ctx, conf, ioConf, testSettings, clientFactory,
481+
nil, nil, cloud.NilMetrics)
482+
if err != nil {
483+
t.Fatal(err)
484+
}
485+
defer storage.Close()
486+
require.True(t, storage.Conf().S3Config.SkipTLSVerify)
487+
_, _, err = storage.ReadFile(ctx, "test file", cloud.ReadOptions{NoFileSize: true})
488+
if err != nil {
489+
t.Fatal(err)
490+
}
491+
})
445492
}
446493

447494
func TestS3DisallowCustomEndpoints(t *testing.T) {

pkg/cloud/azure/azure_storage.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,12 @@ func makeAzureStorage(
221221
}
222222

223223
options := args.ExternalStorageOptions()
224-
t, err := cloud.MakeHTTPClient(args.Settings, args.MetricsRecorder, "azure", dest.AzureConfig.Container, options.ClientName)
224+
t, err := cloud.MakeHTTPClient(args.Settings, args.MetricsRecorder,
225+
cloud.Config{
226+
Bucket: dest.AzureConfig.Container,
227+
Client: options.ClientName,
228+
Cloud: "azure",
229+
})
225230
if err != nil {
226231
return nil, errors.Wrap(err, "azure: unable to create transport")
227232
}

pkg/cloud/cloud_io.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ import (
2828
"github.com/cockroachdb/errors"
2929
)
3030

31+
// Config defines the settings for building the underlying transport.
32+
type Config struct {
33+
// Bucket is name of the bucket. Used to label metrics.
34+
Bucket string
35+
// Client type (e.g. CDC vs backup). Used to label metrics.
36+
Client string
37+
// Cloud is the storage provider. Used to label metrics.
38+
Cloud string
39+
// InsecureSkipVerify controls whether a client verifies the server's
40+
// certificate chain and host name. If InsecureSkipVerify is true, crypto/tls
41+
// accepts any certificate presented by the server and any host name in that
42+
// certificate. In this mode, TLS is susceptible to machine-in-the-middle attacks.
43+
InsecureSkipVerify bool
44+
}
45+
3146
// Timeout is a cluster setting used for cloud storage interactions.
3247
var Timeout = settings.RegisterDurationSetting(
3348
settings.ApplicationLevel,
@@ -77,9 +92,9 @@ var httpMetrics = settings.RegisterBoolSetting(
7792
// MakeHTTPClient makes an http client configured with the common settings used
7893
// for interacting with cloud storage (timeouts, retries, CA certs, etc).
7994
func MakeHTTPClient(
80-
settings *cluster.Settings, metrics *Metrics, cloud, bucket, client string,
95+
settings *cluster.Settings, metrics *Metrics, config Config,
8196
) (*http.Client, error) {
82-
t, err := MakeTransport(settings, metrics, cloud, bucket, client)
97+
t, err := MakeTransport(settings, metrics, config)
8398
if err != nil {
8499
return nil, err
85100
}
@@ -99,10 +114,12 @@ func MakeHTTPClientForTransport(t http.RoundTripper) (*http.Client, error) {
99114
// used for interacting with cloud storage (timeouts, retries, CA certs, etc).
100115
// Prefer MakeHTTPClient where possible.
101116
func MakeTransport(
102-
settings *cluster.Settings, metrics *Metrics, cloud, bucket, client string,
117+
settings *cluster.Settings, metrics *Metrics, config Config,
103118
) (*http.Transport, error) {
104119
var tlsConf *tls.Config
105-
if pem := httpCustomCA.Get(&settings.SV); pem != "" {
120+
if config.InsecureSkipVerify {
121+
tlsConf = &tls.Config{InsecureSkipVerify: true}
122+
} else if pem := httpCustomCA.Get(&settings.SV); pem != "" {
106123
roots, err := x509.SystemCertPool()
107124
if err != nil {
108125
return nil, errors.Wrap(err, "could not load system root CA pool")
@@ -121,7 +138,7 @@ func MakeTransport(
121138
// most bulk jobs.
122139
t.MaxIdleConnsPerHost = 64
123140
if metrics != nil {
124-
t.DialContext = metrics.NetMetrics.Wrap(t.DialContext, cloud, bucket, client)
141+
t.DialContext = metrics.NetMetrics.Wrap(t.DialContext, config.Cloud, config.Bucket, config.Client)
125142
}
126143
return t, nil
127144
}

pkg/cloud/cloudpb/external_storage.proto

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ message ExternalStorage {
6363
string temp_token = 5;
6464
string endpoint = 6;
6565
bool use_path_style=16;
66-
bool skip_checksum=17;
66+
bool skip_checksum=17;
67+
bool skip_tls_verify=18 [(gogoproto.customname) = "SkipTLSVerify"];
6768
string region = 7;
6869
string auth = 8;
6970
string server_enc_mode = 9;
@@ -93,7 +94,7 @@ message ExternalStorage {
9394
// list so that the role specified in AssumeRoleProvider can be assumed.
9495
repeated AssumeRoleProvider delegate_role_providers = 15 [(gogoproto.nullable) = false];
9596

96-
// Next ID: 18;
97+
// Next ID: 19;
9798
}
9899

99100
message GCS {

pkg/cloud/gcp/gcs_storage.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,11 @@ func makeGCSStorage(
186186
}
187187

188188
clientName := args.ExternalStorageOptions().ClientName
189-
baseTransport, err := cloud.MakeTransport(args.Settings, args.MetricsRecorder, "gcs", conf.Bucket, clientName)
189+
baseTransport, err := cloud.MakeTransport(args.Settings, args.MetricsRecorder, cloud.Config{
190+
Bucket: conf.Bucket,
191+
Client: clientName,
192+
Cloud: "gcs",
193+
})
190194
if err != nil {
191195
return nil, errors.Wrap(err, "failed to create http transport")
192196
}

pkg/cloud/httpsink/http_storage.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,12 @@ func MakeHTTPStorage(
6767
}
6868

6969
clientName := args.ExternalStorageOptions().ClientName
70-
client, err := cloud.MakeHTTPClient(args.Settings, args.MetricsRecorder, "http", base, clientName)
70+
client, err := cloud.MakeHTTPClient(args.Settings, args.MetricsRecorder,
71+
cloud.Config{
72+
Cloud: "http",
73+
Bucket: "base",
74+
Client: clientName,
75+
})
7176
if err != nil {
7277
return nil, err
7378
}

pkg/cmd/roachtest/tests/s3_clone_backup_restore.go

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,48 +24,76 @@ import (
2424
"github.com/cockroachdb/errors"
2525
)
2626

27+
type s3CloneSecureOption int
28+
29+
const (
30+
s3ClonePlain s3CloneSecureOption = iota // Use HTTP
31+
s3CloneRequireTLS // Use HTTPS, but skip certification verification.
32+
s3CloneVerifyTLS // Use HTTPS and verify certificates.
33+
)
34+
35+
var s3CloneSecureOptions = []s3CloneSecureOption{s3ClonePlain, s3CloneRequireTLS, s3CloneVerifyTLS}
36+
37+
func (o s3CloneSecureOption) String() string {
38+
switch o {
39+
case s3ClonePlain:
40+
return "plain"
41+
case s3CloneRequireTLS:
42+
return "require"
43+
case s3CloneVerifyTLS:
44+
return "verify"
45+
default:
46+
panic("invalid option")
47+
}
48+
}
49+
2750
// registerBackupS3Clones validates backup/restore compatibility with S3 clones.
2851
func registerBackupS3Clones(r registry.Registry) {
2952
// Running against a microceph cluster deployed on a GCE instance.
3053
for _, cephVersion := range []string{"reef", "squid"} {
31-
r.Add(registry.TestSpec{
32-
Name: fmt.Sprintf("backup/ceph/%s", cephVersion),
33-
Owner: registry.OwnerFieldEng,
34-
Cluster: r.MakeClusterSpec(4, spec.WorkloadNodeCount(1)),
35-
EncryptionSupport: registry.EncryptionMetamorphic,
36-
Leases: registry.MetamorphicLeases,
37-
CompatibleClouds: registry.Clouds(spec.GCE),
38-
Suites: registry.Suites(registry.Nightly),
39-
TestSelectionOptOutSuites: registry.Suites(registry.Nightly),
40-
Run: func(ctx context.Context, t test.Test, c cluster.Cluster) {
41-
v := s3BackupRestoreValidator{
42-
t: t,
43-
c: c,
44-
crdbNodes: c.CRDBNodes(),
45-
csvPort: 8081,
46-
importNode: c.Node(1),
47-
rows: 1000,
48-
workloadNode: c.WorkloadNode(),
49-
}
50-
v.startCluster(ctx)
51-
ceph := cephManager{
52-
t: t,
53-
c: c,
54-
bucket: backupTestingBucket,
55-
// For now, we use the workload node as the cephNode
56-
cephNodes: c.Node(c.Spec().NodeCount),
57-
key: randomString(32),
58-
secret: randomString(64),
59-
// reef `microceph enable rgw` does not support `--ssl-certificate`
60-
// so we'll test a non-secure version.
61-
secure: cephVersion != "reef",
62-
version: cephVersion,
63-
}
64-
ceph.install(ctx)
65-
defer ceph.cleanup(ctx)
66-
v.validateBackupRestore(ctx, ceph)
67-
},
68-
})
54+
for _, secureOption := range s3CloneSecureOptions {
55+
if cephVersion == "reef" && secureOption != s3ClonePlain {
56+
// reef `microceph enable rgw` does not support `--ssl-certificate`
57+
// so we'll test only a non-secure version.
58+
continue
59+
}
60+
r.Add(registry.TestSpec{
61+
Name: fmt.Sprintf("backup/ceph/%s/%s", cephVersion, secureOption),
62+
Owner: registry.OwnerFieldEng,
63+
Cluster: r.MakeClusterSpec(4, spec.WorkloadNodeCount(1)),
64+
EncryptionSupport: registry.EncryptionMetamorphic,
65+
Leases: registry.MetamorphicLeases,
66+
CompatibleClouds: registry.Clouds(spec.GCE),
67+
Suites: registry.Suites(registry.Nightly),
68+
TestSelectionOptOutSuites: registry.Suites(registry.Nightly),
69+
Run: func(ctx context.Context, t test.Test, c cluster.Cluster) {
70+
v := s3BackupRestoreValidator{
71+
t: t,
72+
c: c,
73+
crdbNodes: c.CRDBNodes(),
74+
csvPort: 8081,
75+
importNode: c.Node(1),
76+
rows: 1000,
77+
workloadNode: c.WorkloadNode(),
78+
}
79+
v.startCluster(ctx)
80+
ceph := cephManager{
81+
t: t,
82+
c: c,
83+
bucket: backupTestingBucket,
84+
// For now, we use the workload node as the cephNode
85+
cephNodes: c.Node(c.Spec().NodeCount),
86+
key: randomString(32),
87+
secret: randomString(64),
88+
secure: secureOption,
89+
version: cephVersion,
90+
}
91+
ceph.install(ctx)
92+
defer ceph.cleanup(ctx)
93+
v.validateBackupRestore(ctx, ceph)
94+
},
95+
})
96+
}
6997
}
7098

7199
r.Add(registry.TestSpec{

0 commit comments

Comments
 (0)