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

Connect MCS with Minio TLS/Custom CAs #102

Merged
merged 1 commit into from
May 9, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ export MCS_MINIO_SERVER=http://localhost:9000
./mcs server
```

## Connect MCS to a Minio using TLS and a self-signed certificate

```
...
export MCS_MINIO_SERVER_TLS_SKIP_VERIFICATION=on
Alevsk marked this conversation as resolved.
Show resolved Hide resolved
export MCS_MINIO_SERVER=https://localhost:9000
./mcs server
```

You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`

# Contribute to mcs Project
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFE
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
github.com/Azure/go-autorest v11.7.1+incompatible h1:M2YZIajBBVekV86x0rr1443Lc1F/Ylxb9w+5EtSyX3Q=
github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
Expand Down
6 changes: 5 additions & 1 deletion restapi/client-admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ func NewAdminClient(url, accessKey, secretKey string) (*madmin.AdminClient, *pro
AppName: appName,
AppVersion: McsVersion,
AppComments: []string{appName, runtime.GOOS, runtime.GOARCH},
Insecure: false,
})
s3Client.SetCustomTransport(STSClient.Transport)
if err != nil {
return nil, err.Trace(url)
}
Expand Down Expand Up @@ -240,13 +242,15 @@ func newMAdminClient(jwt string) (*madmin.AdminClient, error) {

// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) {
tlsEnabled := getMinIOEndpointIsSecure()
adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
Secure: getMinIOEndpointIsSecure(),
Secure: tlsEnabled,
})
if err != nil {
return nil, err
}
adminClient.SetCustomTransport(STSClient.Transport)
return adminClient, nil
}

Expand Down
42 changes: 38 additions & 4 deletions restapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package restapi

import (
"context"
"errors"
"fmt"

mc "github.com/minio/mc/cmd"
Expand Down Expand Up @@ -133,13 +134,45 @@ func (c mcsCredentials) Expire() {
c.minioCredentials.Expire()
}

// mcsSTSAssumeRole it's a STSAssumeRole wrapper, in general
// there's no need to use this struct anywhere else in the project, it's only required
// for passing a custom *http.Client to *credentials.STSAssumeRole
type mcsSTSAssumeRole struct {
stsAssumeRole *credentials.STSAssumeRole
}

func (s mcsSTSAssumeRole) Retrieve() (credentials.Value, error) {
return s.stsAssumeRole.Retrieve()
}

func (s mcsSTSAssumeRole) IsExpired() bool {
return s.stsAssumeRole.IsExpired()
}

// STSClient contains http.client configuration need it by STSAssumeRole
var STSClient = PrepareSTSClient()

func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
return credentials.NewSTSAssumeRole(getMinIOServer(), credentials.STSAssumeRoleOptions{
stsEndpoint := getMinIOServer()
if stsEndpoint == "" {
Copy link
Collaborator

@cesnietor cesnietor May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if stsEndpoint == "" {
if strings.TrimSpace(stsEndpoint) == "" {

else " " will not enter here, same for the ones below

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this solved?

return nil, errors.New("STS endpoint cannot be empty")
}
if accessKey == "" || secretKey == "" {
return nil, errors.New("AssumeRole credentials access/secretkey is mandatory")
}
opts := credentials.STSAssumeRoleOptions{
AccessKey: accessKey,
SecretKey: secretKey,
Location: location,
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
})
}
stsAssumeRole := &credentials.STSAssumeRole{
Client: STSClient,
STSEndpoint: stsEndpoint,
Options: opts,
}
mcsSTSWrapper := mcsSTSAssumeRole{stsAssumeRole: stsAssumeRole}
return credentials.New(mcsSTSWrapper), nil
}

// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
Expand All @@ -160,14 +193,15 @@ func newMinioClient(jwt string) (*minio.Client, error) {
if err != nil {
return nil, err
}
adminClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
minioClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
Creds: creds,
Secure: getMinIOEndpointIsSecure(),
})
if err != nil {
return nil, err
}
return adminClient, nil
minioClient.SetCustomTransport(STSClient.Transport)
return minioClient, nil
}

// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
Expand Down
14 changes: 12 additions & 2 deletions restapi/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,17 @@ func getSecretKey() string {
}

func getMinIOServer() string {
return env.Get(McsMinIOServer, "http://localhost:9000")
return strings.TrimSpace(env.Get(McsMinIOServer, "http://localhost:9000"))
}

// If MCS_MINIO_SERVER_TLS_ROOT_CAS is true mcs will load a list of certificates into the
// http.client rootCAs store, this is useful for testing or when working with self-signed certificates
func getMinioServerTLSRootCAs() []string {
caCertFileNames := strings.TrimSpace(env.Get(McsMinIOServerTLSRootCAs, ""))
if caCertFileNames == "" {
cesnietor marked this conversation as resolved.
Show resolved Hide resolved
return []string{}
}
return strings.Split(caCertFileNames, ",")
}

func getMinIOEndpoint() string {
Expand All @@ -67,7 +77,7 @@ func getMinIOEndpointIsSecure() bool {
if strings.Contains(server, "://") {
parts := strings.Split(server, "://")
if len(parts) > 1 {
Alevsk marked this conversation as resolved.
Show resolved Hide resolved
if parts[1] == "https" {
if parts[0] == "https" {
return true
}
}
Expand Down
19 changes: 10 additions & 9 deletions restapi/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ package restapi

const (
// consts for common configuration
McsVersion = `0.1.0`
McsAccessKey = "MCS_ACCESS_KEY"
McsSecretKey = "MCS_SECRET_KEY"
McsMinIOServer = "MCS_MINIO_SERVER"
McsProductionMode = "MCS_PRODUCTION_MODE"
McsHostname = "MCS_HOSTNAME"
McsPort = "MCS_PORT"
McsTLSHostname = "MCS_TLS_HOSTNAME"
McsTLSPort = "MCS_TLS_PORT"
McsVersion = `0.1.0`
McsAccessKey = "MCS_ACCESS_KEY"
McsSecretKey = "MCS_SECRET_KEY"
McsMinIOServer = "MCS_MINIO_SERVER"
McsMinIOServerTLSRootCAs = "MCS_MINIO_SERVER_TLS_ROOT_CAS"
McsProductionMode = "MCS_PRODUCTION_MODE"
McsHostname = "MCS_HOSTNAME"
McsPort = "MCS_PORT"
McsTLSHostname = "MCS_TLS_HOSTNAME"
McsTLSPort = "MCS_TLS_PORT"

// consts for Secure middleware
McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS"
Expand Down
95 changes: 95 additions & 0 deletions restapi/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// This file is part of MinIO Orchestrator
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package restapi

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
)

var (
certDontExists = "File certificate doesn't exists: %s"
)

func prepareSTSClientTransport() *http.Transport {
// This takes github.com/minio/minio/pkg/madmin/transport.go as an example
//
// DefaultTransport - this default transport is similar to
// http.DefaultTransport but with additional param DisableCompression
// is set to true to avoid decompressing content with 'gzip' encoding.
DefaultTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 15 * time.Second,
}).DialContext,
MaxIdleConns: 1024,
MaxIdleConnsPerHost: 1024,
ResponseHeaderTimeout: 60 * time.Second,
IdleConnTimeout: 60 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: true,
}
// If Minio instance is running with TLS enabled and it's using a self-signed certificate
// or a certificate issued by a custom certificate authority we prepare a new custom *http.Transport
if getMinIOEndpointIsSecure() {
caCertFileNames := getMinioServerTLSRootCAs()
tlsConfig := &tls.Config{
// Can't use SSLv3 because of POODLE and BEAST
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
}
// If root CAs are configured we save them to the http.Client RootCAs store
if len(caCertFileNames) > 0 {
certs := x509.NewCertPool()
for _, caCert := range caCertFileNames {
// Validate certificate exists
if FileExists(caCert) {
pemData, err := ioutil.ReadFile(caCert)
if err != nil {
// if there was an error reading pem file stop mcs
panic(err)
}
certs.AppendCertsFromPEM(pemData)
} else {
// if provided cert filename doesn't exists stop mcs
panic(fmt.Sprintf(certDontExists, caCert))
}
}
tlsConfig.RootCAs = certs
}
DefaultTransport.TLSClientConfig = tlsConfig
}
return DefaultTransport
}

// PrepareSTSClient returns an http.Client with custom configurations need it by *credentials.STSAssumeRole
// custom configurations include skipVerification flag, and root CA certificates
func PrepareSTSClient() *http.Client {
transport := prepareSTSClientTransport()
// Return http client with default configuration
return &http.Client{
Transport: transport,
}
}
38 changes: 22 additions & 16 deletions restapi/user_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import (
)

var (
errorGeneric = errors.New("an error occurred, please try again")
errorGeneric = errors.New("an error occurred, please try again")
errInvalidCredentials = errors.New("invalid Credentials")
)

func registerLoginHandlers(api *operations.McsAPI) {
Expand Down Expand Up @@ -61,35 +62,35 @@ func registerLoginHandlers(api *operations.McsAPI) {
})
}

var errInvalidCredentials = errors.New("invalid minioCredentials")

// login performs a check of minioCredentials against MinIO
func login(credentials MCSCredentials) (*string, error) {
// try to obtain minioCredentials,
tokens, err := credentials.Get()
if err != nil {
log.Println("error authenticating user", err)
return nil, errInvalidCredentials
}
// if we made it here, the minioCredentials work, generate a jwt with claims
jwt, err := auth.NewJWTWithClaimsForClient(&tokens, getMinIOServer())
if err != nil {
log.Println("error authenticating user", err)
return nil, errInvalidCredentials
}
return &jwt, nil
}

func getConfiguredRegion(client MinioAdmin) string {
func getConfiguredRegionForLogin(client MinioAdmin) (string, error) {
location := ""
configuration, err := getConfig(client, "region")
if err != nil {
log.Println("error obtaining MinIO region:", err)
return location
return location, errorGeneric
}
// region is an array of 1 element
if len(configuration) > 0 {
location = configuration[0].Value
}
return location
return location, nil
}

// getLoginResponse performs login() and serializes it to the handler's output
Expand All @@ -102,16 +103,18 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, error) {
adminClient := adminClient{client: mAdmin}
// obtain the configured MinIO region
// need it for user authentication
location := getConfiguredRegion(adminClient)
location, err := getConfiguredRegionForLogin(adminClient)
if err != nil {
return nil, err
}
creds, err := newMcsCredentials(*lr.AccessKey, *lr.SecretKey, location)
if err != nil {
log.Println("error login:", err)
return nil, err
return nil, errInvalidCredentials
}
credentials := mcsCredentials{minioCredentials: creds}
sessionID, err := login(credentials)
if err != nil {
log.Println("error login:", err)
return nil, err
}
// serialize output
Expand All @@ -131,7 +134,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
// initialize new oauth2 client
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
if err != nil {
return nil, err
log.Println("error getting new oauth2 provider client", err)
return nil, errorGeneric
}
// Validate user against IDP
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
Expand All @@ -147,7 +151,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
func loginOauth2Auth(ctx context.Context, provider *auth.IdentityProvider, code, state string) (*oauth2.User, error) {
userIdentity, err := provider.VerifyIdentity(ctx, code, state)
if err != nil {
return nil, err
log.Println("error validating user identity against idp:", err)
return nil, errorGeneric
}
return userIdentity, nil
}
Expand All @@ -166,8 +171,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
// Validate user against IDP
identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State)
if err != nil {
log.Println("error validating user identity against idp:", err)
return nil, errorGeneric
return nil, err
}
mAdmin, err := newSuperMAdminClient()
if err != nil {
Expand All @@ -179,7 +183,10 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
secretKey := utils.RandomCharString(32)
// obtain the configured MinIO region
// need it for user authentication
location := getConfiguredRegion(adminClient)
location, err := getConfiguredRegionForLogin(adminClient)
if err != nil {
return nil, err
}
// create user in MinIO
if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil {
log.Println("error adding user:", err)
Expand Down Expand Up @@ -207,8 +214,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
credentials := mcsCredentials{minioCredentials: creds}
jwt, err := login(credentials)
if err != nil {
log.Println("error login:", err)
return nil, errorGeneric
return nil, err
}
// serialize output
loginResponse := &models.LoginResponse{
Expand Down