mirror of
https://github.com/minio/mc.git
synced 2025-11-10 13:42:32 +03:00
automatic prompt for certs broke system trusted tls endpoints since it ignored the currently loaded certs as trusted. if they are available and we can establish TLS we should proceed instead of failing and asking users to always use `--insecure`
487 lines
15 KiB
Go
487 lines
15 KiB
Go
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// 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 cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/minio/cli"
|
|
"github.com/minio/mc/pkg/probe"
|
|
"github.com/minio/minio-go/v7"
|
|
"github.com/minio/pkg/console"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
const cred = "YellowItalics"
|
|
|
|
var aliasSetFlags = []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "path",
|
|
Value: "auto",
|
|
Usage: "bucket path lookup supported by the server. Valid options are '[auto, on, off]'",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "api",
|
|
Usage: "API signature. Valid options are '[S3v4, S3v2]'",
|
|
},
|
|
}
|
|
|
|
var aliasSetCmd = cli.Command{
|
|
Name: "set",
|
|
ShortName: "s",
|
|
Usage: "set a new alias to configuration file",
|
|
Action: func(cli *cli.Context) error {
|
|
return mainAliasSet(cli, false)
|
|
},
|
|
OnUsageError: onUsageError,
|
|
Before: setGlobalsFromContext,
|
|
Flags: append(aliasSetFlags, globalFlags...),
|
|
HideHelpCommand: true,
|
|
CustomHelpTemplate: `NAME:
|
|
{{.HelpName}} - {{.Usage}}
|
|
USAGE:
|
|
{{.HelpName}} ALIAS URL ACCESSKEY SECRETKEY
|
|
FLAGS:
|
|
{{range .VisibleFlags}}{{.}}
|
|
{{end}}
|
|
EXAMPLES:
|
|
1. Add MinIO service under "myminio" alias. For security reasons turn off bash history momentarily.
|
|
{{.DisableHistory}}
|
|
{{.Prompt}} {{.HelpName}} myminio http://localhost:9000 minio minio123
|
|
{{.EnableHistory}}
|
|
2. Add MinIO service under "myminio" alias, to use dns style bucket lookup. For security reasons
|
|
turn off bash history momentarily.
|
|
{{.DisableHistory}}
|
|
{{.Prompt}} {{.HelpName}} myminio http://localhost:9000 minio minio123 --api "s3v4" --path "off"
|
|
{{.EnableHistory}}
|
|
3. Add Amazon S3 storage service under "mys3" alias. For security reasons turn off bash history momentarily.
|
|
{{.DisableHistory}}
|
|
{{.Prompt}} {{.HelpName}} mys3 https://s3.amazonaws.com \
|
|
BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
|
|
{{.EnableHistory}}
|
|
4. Add Amazon S3 storage service under "mys3" alias, prompting for keys.
|
|
{{.Prompt}} {{.HelpName}} mys3 https://s3.amazonaws.com --api "s3v4" --path "off"
|
|
Enter Access Key: BKIKJAA5BMMU2RHO6IBB
|
|
Enter Secret Key: V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
|
|
5. Add Amazon S3 storage service under "mys3" alias using piped keys.
|
|
{{.DisableHistory}}
|
|
{{.Prompt}} echo -e "BKIKJAA5BMMU2RHO6IBB\nV8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12" | \
|
|
{{.HelpName}} mys3 https://s3.amazonaws.com --api "s3v4" --path "off"
|
|
{{.EnableHistory}}
|
|
`,
|
|
}
|
|
|
|
// checkAliasSetSyntax - verifies input arguments to 'alias set'.
|
|
func checkAliasSetSyntax(ctx *cli.Context, accessKey string, secretKey string, deprecated bool) {
|
|
args := ctx.Args()
|
|
argsNr := len(args)
|
|
|
|
if argsNr == 0 {
|
|
cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1) // last argument is exit code
|
|
}
|
|
|
|
if argsNr > 4 || argsNr < 2 {
|
|
fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
|
|
"Incorrect number of arguments for alias set command.")
|
|
}
|
|
|
|
alias := cleanAlias(args.Get(0))
|
|
url := args.Get(1)
|
|
api := ctx.String("api")
|
|
path := ctx.String("path")
|
|
bucketLookup := ctx.String("lookup")
|
|
|
|
if !isValidAlias(alias) {
|
|
fatalIf(errInvalidAlias(alias), "Invalid alias.")
|
|
}
|
|
|
|
if !isValidHostURL(url) {
|
|
fatalIf(errInvalidURL(url), "Invalid URL.")
|
|
}
|
|
|
|
if !isValidAccessKey(accessKey) {
|
|
fatalIf(errInvalidArgument().Trace(accessKey),
|
|
"Invalid access key `"+accessKey+"`.")
|
|
}
|
|
|
|
if !isValidSecretKey(secretKey) {
|
|
fatalIf(errInvalidArgument().Trace(secretKey),
|
|
"Invalid secret key `"+secretKey+"`.")
|
|
}
|
|
|
|
if api != "" && !isValidAPI(api) { // Empty value set to default "S3v4".
|
|
fatalIf(errInvalidArgument().Trace(api),
|
|
"Unrecognized API signature. Valid options are `[S3v4, S3v2]`.")
|
|
}
|
|
|
|
if deprecated {
|
|
if !isValidLookup(bucketLookup) {
|
|
fatalIf(errInvalidArgument().Trace(bucketLookup),
|
|
"Unrecognized bucket lookup. Valid options are `[dns,auto, path]`.")
|
|
}
|
|
} else {
|
|
if !isValidPath(path) {
|
|
fatalIf(errInvalidArgument().Trace(bucketLookup),
|
|
"Unrecognized path value. Valid options are `[auto, on, off]`.")
|
|
}
|
|
}
|
|
}
|
|
|
|
// setAlias - set an alias config.
|
|
func setAlias(alias string, aliasCfgV10 aliasConfigV10) aliasMessage {
|
|
mcCfgV10, err := loadMcConfig()
|
|
fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config `"+mustGetMcConfigPath()+"`.")
|
|
|
|
// Add new host.
|
|
mcCfgV10.Aliases[alias] = aliasCfgV10
|
|
|
|
err = saveMcConfig(mcCfgV10)
|
|
fatalIf(err.Trace(alias), "Unable to update hosts in config version `"+mustGetMcConfigPath()+"`.")
|
|
|
|
return aliasMessage{
|
|
Alias: alias,
|
|
URL: aliasCfgV10.URL,
|
|
AccessKey: aliasCfgV10.AccessKey,
|
|
SecretKey: aliasCfgV10.SecretKey,
|
|
API: aliasCfgV10.API,
|
|
Path: aliasCfgV10.Path,
|
|
}
|
|
}
|
|
|
|
// probeS3Signature - auto probe S3 server signature: issue a Stat call
|
|
// using v4 signature then v2 in case of failure.
|
|
func probeS3Signature(ctx context.Context, accessKey, secretKey, url string, peerCert *x509.Certificate) (string, *probe.Error) {
|
|
probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bucket-sign-")
|
|
// Test s3 connection for API auto probe
|
|
s3Config := &Config{
|
|
// S3 connection parameters
|
|
Insecure: globalInsecure,
|
|
AccessKey: accessKey,
|
|
SecretKey: secretKey,
|
|
HostURL: urlJoinPath(url, probeBucketName),
|
|
Debug: globalDebug,
|
|
}
|
|
if peerCert != nil {
|
|
configurePeerCertificate(s3Config, peerCert)
|
|
}
|
|
|
|
probeSignatureType := func(stype string) (string, *probe.Error) {
|
|
s3Config.Signature = stype
|
|
s3Client, err := S3New(s3Config)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := s3Client.Stat(ctx, StatOptions{}); err != nil {
|
|
e := err.ToGoError()
|
|
if _, ok := e.(BucketDoesNotExist); ok {
|
|
// Bucket doesn't exist, means signature probing worked successfully.
|
|
return stype, nil
|
|
}
|
|
// AccessDenied means Stat() is not allowed but credentials are valid.
|
|
// AccessDenied is only returned when policy doesn't allow HeadBucket
|
|
// operations.
|
|
if minio.ToErrorResponse(err.ToGoError()).Code == "AccessDenied" {
|
|
return stype, nil
|
|
}
|
|
|
|
// For any other errors we fail.
|
|
return "", err.Trace(s3Config.Signature)
|
|
}
|
|
return stype, nil
|
|
}
|
|
|
|
stype, err := probeSignatureType("s3v4")
|
|
if err != nil {
|
|
if stype, err = probeSignatureType("s3v2"); err != nil {
|
|
return "", err.Trace("s3v4", "s3v2")
|
|
}
|
|
return stype, nil
|
|
}
|
|
return stype, nil
|
|
}
|
|
|
|
// BuildS3Config constructs an S3 Config and does
|
|
// signature auto-probe when needed.
|
|
func BuildS3Config(ctx context.Context, url, alias, accessKey, secretKey, api, path string, peerCert *x509.Certificate) (*Config, *probe.Error) {
|
|
s3Config := NewS3Config(url, &aliasConfigV10{
|
|
AccessKey: accessKey,
|
|
SecretKey: secretKey,
|
|
URL: url,
|
|
Path: path,
|
|
})
|
|
|
|
if peerCert != nil {
|
|
configurePeerCertificate(s3Config, peerCert)
|
|
}
|
|
|
|
// If api is provided we do not auto probe signature, this is
|
|
// required in situations when signature type is provided by the user.
|
|
if api != "" {
|
|
s3Config.Signature = api
|
|
return s3Config, nil
|
|
}
|
|
// Probe S3 signature version
|
|
api, err := probeS3Signature(ctx, accessKey, secretKey, url, peerCert)
|
|
if err != nil {
|
|
return nil, err.Trace(url, accessKey, secretKey, api, path)
|
|
}
|
|
|
|
s3Config.Signature = api
|
|
// Success.
|
|
return s3Config, nil
|
|
}
|
|
|
|
// fetchAliasKeys - returns the user accessKey and secretKey
|
|
func fetchAliasKeys(args cli.Args) (string, string) {
|
|
accessKey := ""
|
|
secretKey := ""
|
|
console.SetColor(cred, color.New(color.FgYellow, color.Italic))
|
|
isTerminal := terminal.IsTerminal(int(os.Stdin.Fd()))
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
argsNr := len(args)
|
|
|
|
if argsNr == 2 {
|
|
if isTerminal {
|
|
fmt.Printf("%s", console.Colorize(cred, "Enter Access Key: "))
|
|
}
|
|
value, _, _ := reader.ReadLine()
|
|
accessKey = string(value)
|
|
} else {
|
|
accessKey = args.Get(2)
|
|
}
|
|
|
|
if argsNr == 2 || argsNr == 3 {
|
|
if isTerminal {
|
|
fmt.Printf("%s", console.Colorize(cred, "Enter Secret Key: "))
|
|
bytePassword, _ := terminal.ReadPassword(int(os.Stdin.Fd()))
|
|
fmt.Printf("\n")
|
|
secretKey = string(bytePassword)
|
|
} else {
|
|
value, _, _ := reader.ReadLine()
|
|
secretKey = string(value)
|
|
}
|
|
} else {
|
|
secretKey = args.Get(3)
|
|
}
|
|
|
|
return accessKey, secretKey
|
|
}
|
|
|
|
func mainAliasSet(cli *cli.Context, deprecated bool) error {
|
|
console.SetColor("AliasMessage", color.New(color.FgGreen))
|
|
var (
|
|
args = cli.Args()
|
|
alias = cleanAlias(args.Get(0))
|
|
url = trimTrailingSeparator(args.Get(1))
|
|
api = cli.String("api")
|
|
path = cli.String("path")
|
|
|
|
peerCert *x509.Certificate
|
|
err *probe.Error
|
|
)
|
|
|
|
// Support deprecated lookup flag
|
|
if deprecated {
|
|
lookup := strings.ToLower(strings.TrimSpace(cli.String("lookup")))
|
|
switch lookup {
|
|
case "", "auto":
|
|
path = "auto"
|
|
case "path":
|
|
path = "on"
|
|
case "dns":
|
|
path = "off"
|
|
default:
|
|
}
|
|
}
|
|
|
|
accessKey, secretKey := fetchAliasKeys(args)
|
|
checkAliasSetSyntax(cli, accessKey, secretKey, deprecated)
|
|
|
|
ctx, cancelAliasAdd := context.WithCancel(globalContext)
|
|
defer cancelAliasAdd()
|
|
|
|
if !globalInsecure && !globalJSON && term.IsTerminal(int(os.Stdout.Fd())) {
|
|
peerCert, err = promptTrustSelfSignedCert(ctx, url, alias)
|
|
fatalIf(err.Trace(cli.Args()...), "Unable to initialize new alias from the provided credentials.")
|
|
}
|
|
|
|
s3Config, err := BuildS3Config(ctx, url, alias, accessKey, secretKey, api, path, peerCert)
|
|
fatalIf(err.Trace(cli.Args()...), "Unable to initialize new alias from the provided credentials.")
|
|
|
|
msg := setAlias(alias, aliasConfigV10{
|
|
URL: s3Config.HostURL,
|
|
AccessKey: s3Config.AccessKey,
|
|
SecretKey: s3Config.SecretKey,
|
|
API: s3Config.Signature,
|
|
Path: path,
|
|
}) // Add an alias with specified credentials.
|
|
|
|
msg.op = "set"
|
|
if deprecated {
|
|
msg.op = "add"
|
|
}
|
|
|
|
printMsg(msg)
|
|
return nil
|
|
}
|
|
|
|
// promptTrustSelfSignedCert connects to the given endpoint and
|
|
// checks whether the peer certificate can be verified.
|
|
// If not, it computes a fingerprint of the peer certificate
|
|
// public key, asks the user to confirm the fingerprint and
|
|
// adds the peer certificate to the local trust store in the
|
|
// CAs directory.
|
|
func promptTrustSelfSignedCert(ctx context.Context, endpoint, alias string) (*x509.Certificate, *probe.Error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, probe.NewError(err)
|
|
}
|
|
|
|
// no need to probe certs for http endpoints.
|
|
if req.URL.Scheme == "http" {
|
|
return nil, nil
|
|
}
|
|
|
|
client := http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: globalRootCAs, // make sure to use loaded certs before probing
|
|
},
|
|
},
|
|
}
|
|
|
|
_, tlsErr := client.Do(req)
|
|
if tlsErr == nil {
|
|
// certs are already trusted system wide, nothing to do.
|
|
return nil, nil
|
|
}
|
|
|
|
if tlsErr != nil && !strings.Contains(tlsErr.Error(), "certificate signed by unknown authority") {
|
|
return nil, probe.NewError(tlsErr)
|
|
}
|
|
|
|
// Now, we fetch the peer certificate, compute the SHA-256 of
|
|
// public key and let the user confirm the fingerprint.
|
|
// If the user confirms, we store the peer certificate in the CAs
|
|
// directory and retry.
|
|
peerCert, err := fetchPeerCertificate(ctx, endpoint)
|
|
if err != nil {
|
|
return nil, probe.NewError(err)
|
|
}
|
|
|
|
// Check that the subject key id is equal to the authority key id.
|
|
// If true, the certificate is its own issuer, and therefore, a
|
|
// self-signed certificate.
|
|
// Otherwise, the certificate has been issued by some other
|
|
// certificate that is just not trusted
|
|
if !bytes.Equal(peerCert.SubjectKeyId, peerCert.AuthorityKeyId) {
|
|
return nil, probe.NewError(tlsErr)
|
|
}
|
|
|
|
fingerprint := sha256.Sum256(peerCert.RawSubjectPublicKeyInfo)
|
|
fmt.Printf("Fingerprint of %s public key: %s\nConfirm public key y/N: ", color.GreenString(alias), color.YellowString(hex.EncodeToString(fingerprint[:])))
|
|
answer, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
if err != nil {
|
|
return nil, probe.NewError(err)
|
|
}
|
|
if answer = strings.ToLower(answer); answer != "y\n" && answer != "yes\n" {
|
|
return nil, probe.NewError(tlsErr)
|
|
}
|
|
|
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: peerCert.Raw})
|
|
if err = os.WriteFile(filepath.Join(mustGetCAsDir(), alias+".crt"), certPEM, 0o644); err != nil {
|
|
return nil, probe.NewError(err)
|
|
}
|
|
return peerCert, nil
|
|
}
|
|
|
|
// fetchPeerCertificate uses the given transport to fetch the peer
|
|
// certificate from the given endpoint.
|
|
func fetchPeerCertificate(ctx context.Context, endpoint string) (*x509.Certificate, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client := http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
},
|
|
},
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
|
|
return nil, fmt.Errorf("Unable to read remote TLS certificate")
|
|
}
|
|
return resp.TLS.PeerCertificates[0], nil
|
|
}
|
|
|
|
// configurePeerCertificate adds the peer certificate to the
|
|
// TLS root CAs of s3Config. Once configured, any client
|
|
// initialized with this config trusts the given peer certificate.
|
|
func configurePeerCertificate(s3Config *Config, peerCert *x509.Certificate) {
|
|
switch {
|
|
case s3Config.Transport == nil:
|
|
if globalRootCAs != nil {
|
|
globalRootCAs.AddCert(peerCert)
|
|
}
|
|
s3Config.Transport = &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 10 * time.Second,
|
|
KeepAlive: 15 * time.Second,
|
|
}).DialContext,
|
|
MaxIdleConnsPerHost: 256,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 10 * time.Second,
|
|
DisableCompression: true,
|
|
TLSClientConfig: &tls.Config{RootCAs: globalRootCAs},
|
|
}
|
|
case s3Config.Transport.TLSClientConfig == nil || s3Config.Transport.TLSClientConfig.RootCAs == nil:
|
|
if globalRootCAs != nil {
|
|
globalRootCAs.AddCert(peerCert)
|
|
}
|
|
s3Config.Transport.TLSClientConfig = &tls.Config{RootCAs: globalRootCAs}
|
|
default:
|
|
s3Config.Transport.TLSClientConfig.RootCAs.AddCert(peerCert)
|
|
}
|
|
}
|