Skip to content

Commit a1d5e59

Browse files
committed
Connect MCS with Minio insecure TLS/Custom CAs
This PR adds support to connect MCS to minio instances running TLS with self-signed certificates or certificates signed by custom Certificate Authorities ``` export MCS_MINIO_SERVER_TLS_ROOT_CAS=file1,file2,file3 ``` Note: TLS Skip Verification is not supported unless there's a clear need for it
1 parent cf8472b commit a1d5e59

File tree

10 files changed

+206
-35
lines changed

10 files changed

+206
-35
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ export MCS_MINIO_SERVER=http://localhost:9000
6868
./mcs server
6969
```
7070

71+
## Connect MCS to a Minio using TLS and a self-signed certificate
72+
73+
```
74+
...
75+
export MCS_MINIO_SERVER_TLS_SKIP_VERIFICATION=on
76+
export MCS_MINIO_SERVER=https://localhost:9000
77+
./mcs server
78+
```
79+
7180
You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
7281

7382
# Contribute to mcs Project

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFE
1212
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
1313
github.com/Azure/go-autorest v11.7.1+incompatible h1:M2YZIajBBVekV86x0rr1443Lc1F/Ylxb9w+5EtSyX3Q=
1414
github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
15+
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
1516
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
1617
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
1718
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=

restapi/client-admin.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ func NewAdminClient(url, accessKey, secretKey string) (*madmin.AdminClient, *pro
4444
AppName: appName,
4545
AppVersion: McsVersion,
4646
AppComments: []string{appName, runtime.GOOS, runtime.GOARCH},
47+
Insecure: false,
4748
})
49+
s3Client.SetCustomTransport(STSClient.Transport)
4850
if err != nil {
4951
return nil, err.Trace(url)
5052
}
@@ -240,13 +242,15 @@ func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
240242

241243
// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
242244
func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) {
245+
tlsEnabled := getMinIOEndpointIsSecure()
243246
adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
244247
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
245-
Secure: getMinIOEndpointIsSecure(),
248+
Secure: tlsEnabled,
246249
})
247250
if err != nil {
248251
return nil, err
249252
}
253+
adminClient.SetCustomTransport(STSClient.Transport)
250254
return adminClient, nil
251255
}
252256

restapi/client.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package restapi
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223

2324
mc "github.com/minio/mc/cmd"
@@ -133,13 +134,45 @@ func (c mcsCredentials) Expire() {
133134
c.minioCredentials.Expire()
134135
}
135136

137+
// mcsSTSAssumeRole it's a STSAssumeRole wrapper, in general
138+
// there's no need to use this struct anywhere else in the project, it's only required
139+
// for passing a custom *http.Client to *credentials.STSAssumeRole
140+
type mcsSTSAssumeRole struct {
141+
stsAssumeRole *credentials.STSAssumeRole
142+
}
143+
144+
func (s mcsSTSAssumeRole) Retrieve() (credentials.Value, error) {
145+
return s.stsAssumeRole.Retrieve()
146+
}
147+
148+
func (s mcsSTSAssumeRole) IsExpired() bool {
149+
return s.stsAssumeRole.IsExpired()
150+
}
151+
152+
// STSClient contains http.client configuration need it by STSAssumeRole
153+
var STSClient = PrepareSTSClient()
154+
136155
func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
137-
return credentials.NewSTSAssumeRole(getMinIOServer(), credentials.STSAssumeRoleOptions{
156+
stsEndpoint := getMinIOServer()
157+
if stsEndpoint == "" {
158+
return nil, errors.New("STS endpoint cannot be empty")
159+
}
160+
if accessKey == "" || secretKey == "" {
161+
return nil, errors.New("AssumeRole credentials access/secretkey is mandatory")
162+
}
163+
opts := credentials.STSAssumeRoleOptions{
138164
AccessKey: accessKey,
139165
SecretKey: secretKey,
140166
Location: location,
141167
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
142-
})
168+
}
169+
stsAssumeRole := &credentials.STSAssumeRole{
170+
Client: STSClient,
171+
STSEndpoint: stsEndpoint,
172+
Options: opts,
173+
}
174+
mcsSTSWrapper := mcsSTSAssumeRole{stsAssumeRole: stsAssumeRole}
175+
return credentials.New(mcsSTSWrapper), nil
143176
}
144177

145178
// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
@@ -160,14 +193,15 @@ func newMinioClient(jwt string) (*minio.Client, error) {
160193
if err != nil {
161194
return nil, err
162195
}
163-
adminClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
196+
minioClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
164197
Creds: creds,
165198
Secure: getMinIOEndpointIsSecure(),
166199
})
167200
if err != nil {
168201
return nil, err
169202
}
170-
return adminClient, nil
203+
minioClient.SetCustomTransport(STSClient.Transport)
204+
return minioClient, nil
171205
}
172206

173207
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket

restapi/config.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,17 @@ func getSecretKey() string {
4848
}
4949

5050
func getMinIOServer() string {
51-
return env.Get(McsMinIOServer, "http://localhost:9000")
51+
return strings.TrimSpace(env.Get(McsMinIOServer, "http://localhost:9000"))
52+
}
53+
54+
// If MCS_MINIO_SERVER_TLS_ROOT_CAS is true mcs will load a list of certificates into the
55+
// http.client rootCAs store, this is useful for testing or when working with self-signed certificates
56+
func getMinioServerTLSRootCAs() []string {
57+
caCertFileNames := strings.TrimSpace(env.Get(McsMinIOServerTLSRootCAs, ""))
58+
if caCertFileNames == "" {
59+
return []string{}
60+
}
61+
return strings.Split(caCertFileNames, ",")
5262
}
5363

5464
func getMinIOEndpoint() string {
@@ -67,7 +77,7 @@ func getMinIOEndpointIsSecure() bool {
6777
if strings.Contains(server, "://") {
6878
parts := strings.Split(server, "://")
6979
if len(parts) > 1 {
70-
if parts[1] == "https" {
80+
if parts[0] == "https" {
7181
return true
7282
}
7383
}

restapi/consts.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ package restapi
1818

1919
const (
2020
// consts for common configuration
21-
McsVersion = `0.1.0`
22-
McsAccessKey = "MCS_ACCESS_KEY"
23-
McsSecretKey = "MCS_SECRET_KEY"
24-
McsMinIOServer = "MCS_MINIO_SERVER"
25-
McsProductionMode = "MCS_PRODUCTION_MODE"
26-
McsHostname = "MCS_HOSTNAME"
27-
McsPort = "MCS_PORT"
28-
McsTLSHostname = "MCS_TLS_HOSTNAME"
29-
McsTLSPort = "MCS_TLS_PORT"
21+
McsVersion = `0.1.0`
22+
McsAccessKey = "MCS_ACCESS_KEY"
23+
McsSecretKey = "MCS_SECRET_KEY"
24+
McsMinIOServer = "MCS_MINIO_SERVER"
25+
McsMinIOServerTLSRootCAs = "MCS_MINIO_SERVER_TLS_ROOT_CAS"
26+
McsProductionMode = "MCS_PRODUCTION_MODE"
27+
McsHostname = "MCS_HOSTNAME"
28+
McsPort = "MCS_PORT"
29+
McsTLSHostname = "MCS_TLS_HOSTNAME"
30+
McsTLSPort = "MCS_TLS_PORT"
3031

3132
// consts for Secure middleware
3233
McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS"

restapi/tls.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// This file is part of MinIO Orchestrator
2+
// Copyright (c) 2020 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package restapi
18+
19+
import (
20+
"crypto/tls"
21+
"crypto/x509"
22+
"fmt"
23+
"io/ioutil"
24+
"net"
25+
"net/http"
26+
"time"
27+
)
28+
29+
var (
30+
certDontExists = "File certificate doesn't exists: %s"
31+
)
32+
33+
func prepareSTSClientTransport() *http.Transport {
34+
// This takes github.com/minio/minio/pkg/madmin/transport.go as an example
35+
//
36+
// DefaultTransport - this default transport is similar to
37+
// http.DefaultTransport but with additional param DisableCompression
38+
// is set to true to avoid decompressing content with 'gzip' encoding.
39+
DefaultTransport := &http.Transport{
40+
Proxy: http.ProxyFromEnvironment,
41+
DialContext: (&net.Dialer{
42+
Timeout: 5 * time.Second,
43+
KeepAlive: 15 * time.Second,
44+
}).DialContext,
45+
MaxIdleConns: 1024,
46+
MaxIdleConnsPerHost: 1024,
47+
ResponseHeaderTimeout: 60 * time.Second,
48+
IdleConnTimeout: 60 * time.Second,
49+
TLSHandshakeTimeout: 10 * time.Second,
50+
ExpectContinueTimeout: 1 * time.Second,
51+
DisableCompression: true,
52+
}
53+
// If Minio instance is running with TLS enabled and it's using a self-signed certificate
54+
// or a certificate issued by a custom certificate authority we prepare a new custom *http.Transport
55+
if getMinIOEndpointIsSecure() {
56+
caCertFileNames := getMinioServerTLSRootCAs()
57+
tlsConfig := &tls.Config{
58+
// Can't use SSLv3 because of POODLE and BEAST
59+
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
60+
// Can't use TLSv1.1 because of RC4 cipher usage
61+
MinVersion: tls.VersionTLS12,
62+
}
63+
// If root CAs are configured we save them to the http.Client RootCAs store
64+
if len(caCertFileNames) > 0 {
65+
certs := x509.NewCertPool()
66+
for _, caCert := range caCertFileNames {
67+
// Validate certificate exists
68+
if FileExists(caCert) {
69+
pemData, err := ioutil.ReadFile(caCert)
70+
if err != nil {
71+
// if there was an error reading pem file stop mcs
72+
panic(err)
73+
}
74+
certs.AppendCertsFromPEM(pemData)
75+
} else {
76+
// if provided cert filename doesn't exists stop mcs
77+
panic(fmt.Sprintf(certDontExists, caCert))
78+
}
79+
}
80+
tlsConfig.RootCAs = certs
81+
}
82+
DefaultTransport.TLSClientConfig = tlsConfig
83+
}
84+
return DefaultTransport
85+
}
86+
87+
// PrepareSTSClient returns an http.Client with custom configurations need it by *credentials.STSAssumeRole
88+
// custom configurations include skipVerification flag, and root CA certificates
89+
func PrepareSTSClient() *http.Client {
90+
transport := prepareSTSClientTransport()
91+
// Return http client with default configuration
92+
return &http.Client{
93+
Transport: transport,
94+
}
95+
}

restapi/user_login.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import (
3232
)
3333

3434
var (
35-
errorGeneric = errors.New("an error occurred, please try again")
35+
errorGeneric = errors.New("an error occurred, please try again")
36+
errInvalidCredentials = errors.New("invalid Credentials")
3637
)
3738

3839
func registerLoginHandlers(api *operations.McsAPI) {
@@ -61,35 +62,35 @@ func registerLoginHandlers(api *operations.McsAPI) {
6162
})
6263
}
6364

64-
var errInvalidCredentials = errors.New("invalid minioCredentials")
65-
6665
// login performs a check of minioCredentials against MinIO
6766
func login(credentials MCSCredentials) (*string, error) {
6867
// try to obtain minioCredentials,
6968
tokens, err := credentials.Get()
7069
if err != nil {
70+
log.Println("error authenticating user", err)
7171
return nil, errInvalidCredentials
7272
}
7373
// if we made it here, the minioCredentials work, generate a jwt with claims
7474
jwt, err := auth.NewJWTWithClaimsForClient(&tokens, getMinIOServer())
7575
if err != nil {
76+
log.Println("error authenticating user", err)
7677
return nil, errInvalidCredentials
7778
}
7879
return &jwt, nil
7980
}
8081

81-
func getConfiguredRegion(client MinioAdmin) string {
82+
func getConfiguredRegionForLogin(client MinioAdmin) (string, error) {
8283
location := ""
8384
configuration, err := getConfig(client, "region")
8485
if err != nil {
8586
log.Println("error obtaining MinIO region:", err)
86-
return location
87+
return location, errorGeneric
8788
}
8889
// region is an array of 1 element
8990
if len(configuration) > 0 {
9091
location = configuration[0].Value
9192
}
92-
return location
93+
return location, nil
9394
}
9495

9596
// getLoginResponse performs login() and serializes it to the handler's output
@@ -102,16 +103,18 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, error) {
102103
adminClient := adminClient{client: mAdmin}
103104
// obtain the configured MinIO region
104105
// need it for user authentication
105-
location := getConfiguredRegion(adminClient)
106+
location, err := getConfiguredRegionForLogin(adminClient)
107+
if err != nil {
108+
return nil, err
109+
}
106110
creds, err := newMcsCredentials(*lr.AccessKey, *lr.SecretKey, location)
107111
if err != nil {
108112
log.Println("error login:", err)
109-
return nil, err
113+
return nil, errInvalidCredentials
110114
}
111115
credentials := mcsCredentials{minioCredentials: creds}
112116
sessionID, err := login(credentials)
113117
if err != nil {
114-
log.Println("error login:", err)
115118
return nil, err
116119
}
117120
// serialize output
@@ -131,7 +134,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
131134
// initialize new oauth2 client
132135
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
133136
if err != nil {
134-
return nil, err
137+
log.Println("error getting new oauth2 provider client", err)
138+
return nil, errorGeneric
135139
}
136140
// Validate user against IDP
137141
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
@@ -147,7 +151,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
147151
func loginOauth2Auth(ctx context.Context, provider *auth.IdentityProvider, code, state string) (*oauth2.User, error) {
148152
userIdentity, err := provider.VerifyIdentity(ctx, code, state)
149153
if err != nil {
150-
return nil, err
154+
log.Println("error validating user identity against idp:", err)
155+
return nil, errorGeneric
151156
}
152157
return userIdentity, nil
153158
}
@@ -166,8 +171,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
166171
// Validate user against IDP
167172
identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State)
168173
if err != nil {
169-
log.Println("error validating user identity against idp:", err)
170-
return nil, errorGeneric
174+
return nil, err
171175
}
172176
mAdmin, err := newSuperMAdminClient()
173177
if err != nil {
@@ -179,7 +183,10 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
179183
secretKey := utils.RandomCharString(32)
180184
// obtain the configured MinIO region
181185
// need it for user authentication
182-
location := getConfiguredRegion(adminClient)
186+
location, err := getConfiguredRegionForLogin(adminClient)
187+
if err != nil {
188+
return nil, err
189+
}
183190
// create user in MinIO
184191
if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil {
185192
log.Println("error adding user:", err)
@@ -207,8 +214,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
207214
credentials := mcsCredentials{minioCredentials: creds}
208215
jwt, err := login(credentials)
209216
if err != nil {
210-
log.Println("error login:", err)
211-
return nil, errorGeneric
217+
return nil, err
212218
}
213219
// serialize output
214220
loginResponse := &models.LoginResponse{

0 commit comments

Comments
 (0)