mirror of
https://github.com/minio/minio.git
synced 2024-12-25 14:45:54 -05:00
fbd1c5f51a
This commit refactors the certificate management implementation in the `certs` package such that multiple certificates can be specified at the same time. Therefore, the following layout of the `certs/` directory is expected: ``` certs/ │ ├─ public.crt ├─ private.key ├─ CAs/ // CAs directory is ignored │ │ │ ... │ ├─ example.com/ │ │ │ ├─ public.crt │ └─ private.key └─ foobar.org/ │ ├─ public.crt └─ private.key ... ``` However, directory names like `example.com` are just for human readability/organization and don't have any meaning w.r.t whether a particular certificate is served or not. This decision is made based on the SNI sent by the client and the SAN of the certificate. *** The `Manager` will pick a certificate based on the client trying to establish a TLS connection. In particular, it looks at the client hello (i.e. SNI) to determine which host the client tries to access. If the manager can find a certificate that matches the SNI it returns this certificate to the client. However, the client may choose to not send an SNI or tries to access a server directly via IP (`https://<ip>:<port>`). In this case, we cannot use the SNI to determine which certificate to serve. However, we also should not pick "the first" certificate that would be accepted by the client (based on crypto. parameters - like a signature algorithm) because it may be an internal certificate that contains internal hostnames. We would disclose internal infrastructure details doing so. Therefore, the `Manager` returns the "default" certificate when the client does not specify an SNI. The default certificate the top-level `public.crt` - i.e. `certs/public.crt`. This approach has some consequences: - It's the operator's responsibility to ensure that the top-level `public.crt` does not disclose any information (i.e. hostnames) that are not publicly visible. However, this was the case in the past already. - Any other `public.crt` - except for the top-level one - must not contain any IP SAN. The reason for this restriction is that the Manager cannot match a SNI to an IP b/c the SNI is the server host name. The entire purpose of SNI is to indicate which host the client tries to connect to when multiple hosts run on the same IP. So, a client will not set the SNI to an IP. If we would allow IP SANs in a lower-level `public.crt` a user would expect that it is possible to connect to MinIO directly via IP address and that the MinIO server would pick "the right" certificate. However, the MinIO server cannot determine which certificate to serve, and therefore always picks the "default" one. This may lead to all sorts of confusing errors like: "It works if I use `https:instance.minio.local` but not when I use `https://10.0.2.1`. These consequences/limitations should be pointed out / explained in our docs in an appropriate way. However, the support for multiple certificates should not have any impact on how deployment with a single certificate function today. Co-authored-by: Harshavardhana <harsha@minio.io>
365 lines
11 KiB
Go
365 lines
11 KiB
Go
/*
|
|
* MinIO Cloud Storage, (C) 2017-2019 MinIO, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/gob"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
dns2 "github.com/miekg/dns"
|
|
"github.com/minio/cli"
|
|
"github.com/minio/minio-go/v7/pkg/set"
|
|
"github.com/minio/minio/cmd/config"
|
|
"github.com/minio/minio/cmd/logger"
|
|
"github.com/minio/minio/pkg/auth"
|
|
"github.com/minio/minio/pkg/certs"
|
|
"github.com/minio/minio/pkg/env"
|
|
)
|
|
|
|
func init() {
|
|
logger.Init(GOPATH, GOROOT)
|
|
logger.RegisterError(config.FmtError)
|
|
|
|
// Initialize globalConsoleSys system
|
|
globalConsoleSys = NewConsoleLogger(GlobalContext)
|
|
logger.AddTarget(globalConsoleSys)
|
|
|
|
gob.Register(StorageErr(""))
|
|
}
|
|
|
|
func verifyObjectLayerFeatures(name string, objAPI ObjectLayer) {
|
|
if (GlobalKMS != nil) && !objAPI.IsEncryptionSupported() {
|
|
logger.Fatal(errInvalidArgument,
|
|
"Encryption support is requested but '%s' does not support encryption", name)
|
|
}
|
|
|
|
if strings.HasPrefix(name, "gateway") {
|
|
if GlobalGatewaySSE.IsSet() && GlobalKMS == nil {
|
|
uiErr := config.ErrInvalidGWSSEEnvValue(nil).Msg("MINIO_GATEWAY_SSE set but KMS is not configured")
|
|
logger.Fatal(uiErr, "Unable to start gateway with SSE")
|
|
}
|
|
}
|
|
|
|
if globalCompressConfig.Enabled && !objAPI.IsCompressionSupported() {
|
|
logger.Fatal(errInvalidArgument,
|
|
"Compression support is requested but '%s' does not support compression", name)
|
|
}
|
|
}
|
|
|
|
// Check for updates and print a notification message
|
|
func checkUpdate(mode string) {
|
|
updateURL := minioReleaseInfoURL
|
|
if runtime.GOOS == globalWindowsOSName {
|
|
updateURL = minioReleaseWindowsInfoURL
|
|
}
|
|
|
|
u, err := url.Parse(updateURL)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Its OK to ignore any errors during doUpdate() here.
|
|
crTime, err := GetCurrentReleaseTime()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
_, lrTime, err := getLatestReleaseTime(u, 2*time.Second, mode)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var older time.Duration
|
|
var downloadURL string
|
|
if lrTime.After(crTime) {
|
|
older = lrTime.Sub(crTime)
|
|
downloadURL = getDownloadURL(releaseTimeToReleaseTag(lrTime))
|
|
}
|
|
|
|
updateMsg := prepareUpdateMessage(downloadURL, older)
|
|
if updateMsg == "" {
|
|
return
|
|
}
|
|
|
|
if globalInplaceUpdateDisabled {
|
|
logStartupMessage(updateMsg)
|
|
} else {
|
|
logStartupMessage(prepareUpdateMessage("Run `mc admin update`", lrTime.Sub(crTime)))
|
|
}
|
|
}
|
|
|
|
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
|
|
var dir string
|
|
var dirSet bool
|
|
|
|
switch {
|
|
case ctx.IsSet(option):
|
|
dir = ctx.String(option)
|
|
dirSet = true
|
|
case ctx.GlobalIsSet(option):
|
|
dir = ctx.GlobalString(option)
|
|
dirSet = true
|
|
// cli package does not expose parent's option option. Below code is workaround.
|
|
if dir == "" || dir == getDefaultDir() {
|
|
dirSet = false // Unset to false since GlobalIsSet() true is a false positive.
|
|
if ctx.Parent().GlobalIsSet(option) {
|
|
dir = ctx.Parent().GlobalString(option)
|
|
dirSet = true
|
|
}
|
|
}
|
|
default:
|
|
// Neither local nor global option is provided. In this case, try to use
|
|
// default directory.
|
|
dir = getDefaultDir()
|
|
if dir == "" {
|
|
logger.FatalIf(errInvalidArgument, "%s option must be provided", option)
|
|
}
|
|
}
|
|
|
|
if dir == "" {
|
|
logger.FatalIf(errors.New("empty directory"), "%s directory cannot be empty", option)
|
|
}
|
|
|
|
// Disallow relative paths, figure out absolute paths.
|
|
dirAbs, err := filepath.Abs(dir)
|
|
logger.FatalIf(err, "Unable to fetch absolute path for %s=%s", option, dir)
|
|
|
|
logger.FatalIf(mkdirAllIgnorePerm(dirAbs), "Unable to create directory specified %s=%s", option, dir)
|
|
|
|
return &ConfigDir{path: dirAbs}, dirSet
|
|
}
|
|
|
|
func handleCommonCmdArgs(ctx *cli.Context) {
|
|
|
|
// Get "json" flag from command line argument and
|
|
// enable json and quite modes if json flag is turned on.
|
|
globalCLIContext.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
|
|
if globalCLIContext.JSON {
|
|
logger.EnableJSON()
|
|
}
|
|
|
|
// Get quiet flag from command line argument.
|
|
globalCLIContext.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
|
|
if globalCLIContext.Quiet {
|
|
logger.EnableQuiet()
|
|
}
|
|
|
|
// Get anonymous flag from command line argument.
|
|
globalCLIContext.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous")
|
|
if globalCLIContext.Anonymous {
|
|
logger.EnableAnonymous()
|
|
}
|
|
|
|
// Fetch address option
|
|
globalCLIContext.Addr = ctx.GlobalString("address")
|
|
if globalCLIContext.Addr == "" || globalCLIContext.Addr == ":"+GlobalMinioDefaultPort {
|
|
globalCLIContext.Addr = ctx.String("address")
|
|
}
|
|
|
|
// Check "no-compat" flag from command line argument.
|
|
globalCLIContext.StrictS3Compat = true
|
|
if ctx.IsSet("no-compat") || ctx.GlobalIsSet("no-compat") {
|
|
globalCLIContext.StrictS3Compat = false
|
|
}
|
|
|
|
// Set all config, certs and CAs directories.
|
|
var configSet, certsSet bool
|
|
globalConfigDir, configSet = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
|
|
globalCertsDir, certsSet = newConfigDirFromCtx(ctx, "certs-dir", defaultCertsDir.Get)
|
|
|
|
// Remove this code when we deprecate and remove config-dir.
|
|
// This code is to make sure we inherit from the config-dir
|
|
// option if certs-dir is not provided.
|
|
if !certsSet && configSet {
|
|
globalCertsDir = &ConfigDir{path: filepath.Join(globalConfigDir.Get(), certsDir)}
|
|
}
|
|
|
|
globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
|
|
|
|
logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
|
|
}
|
|
|
|
func handleCommonEnvVars() {
|
|
wormEnabled, err := config.LookupWorm()
|
|
if err != nil {
|
|
logger.Fatal(config.ErrInvalidWormValue(err), "Invalid worm configuration")
|
|
}
|
|
if wormEnabled {
|
|
logger.Fatal(errors.New("WORM is deprecated"), "global MINIO_WORM support is removed, please downgrade your server or migrate to https://github.com/minio/minio/tree/master/docs/retention")
|
|
}
|
|
|
|
globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn))
|
|
if err != nil {
|
|
logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
|
|
}
|
|
|
|
globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
|
|
if err != nil {
|
|
logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable")
|
|
}
|
|
|
|
domains := env.Get(config.EnvDomain, "")
|
|
if len(domains) != 0 {
|
|
for _, domainName := range strings.Split(domains, config.ValueSeparator) {
|
|
if _, ok := dns2.IsDomainName(domainName); !ok {
|
|
logger.Fatal(config.ErrInvalidDomainValue(nil).Msg("Unknown value `%s`", domainName),
|
|
"Invalid MINIO_DOMAIN value in environment variable")
|
|
}
|
|
globalDomainNames = append(globalDomainNames, domainName)
|
|
}
|
|
}
|
|
|
|
publicIPs := env.Get(config.EnvPublicIPs, "")
|
|
if len(publicIPs) != 0 {
|
|
minioEndpoints := strings.Split(publicIPs, config.ValueSeparator)
|
|
var domainIPs = set.NewStringSet()
|
|
for _, endpoint := range minioEndpoints {
|
|
if net.ParseIP(endpoint) == nil {
|
|
// Checking if the IP is a DNS entry.
|
|
addrs, err := net.LookupHost(endpoint)
|
|
if err != nil {
|
|
logger.FatalIf(err, "Unable to initialize MinIO server with [%s] invalid entry found in MINIO_PUBLIC_IPS", endpoint)
|
|
}
|
|
for _, addr := range addrs {
|
|
domainIPs.Add(addr)
|
|
}
|
|
}
|
|
domainIPs.Add(endpoint)
|
|
}
|
|
updateDomainIPs(domainIPs)
|
|
} else {
|
|
// Add found interfaces IP address to global domain IPS,
|
|
// loopback addresses will be naturally dropped.
|
|
domainIPs := mustGetLocalIP4()
|
|
for _, host := range globalEndpoints.Hostnames() {
|
|
domainIPs.Add(host)
|
|
}
|
|
updateDomainIPs(domainIPs)
|
|
}
|
|
|
|
// In place update is true by default if the MINIO_UPDATE is not set
|
|
// or is not set to 'off', if MINIO_UPDATE is set to 'off' then
|
|
// in-place update is off.
|
|
globalInplaceUpdateDisabled = strings.EqualFold(env.Get(config.EnvUpdate, config.EnableOn), config.EnableOff)
|
|
|
|
if env.IsSet(config.EnvAccessKey) || env.IsSet(config.EnvSecretKey) {
|
|
cred, err := auth.CreateCredentials(env.Get(config.EnvAccessKey, ""), env.Get(config.EnvSecretKey, ""))
|
|
if err != nil {
|
|
logger.Fatal(config.ErrInvalidCredentials(err),
|
|
"Unable to validate credentials inherited from the shell environment")
|
|
}
|
|
globalActiveCred = cred
|
|
globalConfigEncrypted = true
|
|
}
|
|
|
|
if env.IsSet(config.EnvAccessKeyOld) && env.IsSet(config.EnvSecretKeyOld) {
|
|
oldCred, err := auth.CreateCredentials(env.Get(config.EnvAccessKeyOld, ""), env.Get(config.EnvSecretKeyOld, ""))
|
|
if err != nil {
|
|
logger.Fatal(config.ErrInvalidCredentials(err),
|
|
"Unable to validate the old credentials inherited from the shell environment")
|
|
}
|
|
globalOldCred = oldCred
|
|
os.Unsetenv(config.EnvAccessKeyOld)
|
|
os.Unsetenv(config.EnvSecretKeyOld)
|
|
}
|
|
}
|
|
|
|
func logStartupMessage(msg string) {
|
|
if globalConsoleSys != nil {
|
|
globalConsoleSys.Send(msg, string(logger.All))
|
|
}
|
|
logger.StartupMessage(msg)
|
|
}
|
|
|
|
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
|
|
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
|
return nil, nil, false, nil
|
|
}
|
|
|
|
if x509Certs, err = config.ParsePublicCertFile(getPublicCertFile()); err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
|
|
manager, err = certs.NewManager(GlobalContext, getPublicCertFile(), getPrivateKeyFile(), config.LoadX509KeyPair)
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
|
|
// MinIO has support for multiple certificates. It expects the following structure:
|
|
// certs/
|
|
// │
|
|
// ├─ public.crt
|
|
// ├─ private.key
|
|
// │
|
|
// ├─ example.com/
|
|
// │ │
|
|
// │ ├─ public.crt
|
|
// │ └─ private.key
|
|
// └─ foobar.org/
|
|
// │
|
|
// ├─ public.crt
|
|
// └─ private.key
|
|
// ...
|
|
//
|
|
// Therefore, we read all filenames in the cert directory and check
|
|
// for each directory whether it contains a public.crt and private.key.
|
|
// If so, we try to add it to certificate manager.
|
|
root, err := os.Open(globalCertsDir.Get())
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
defer root.Close()
|
|
|
|
files, err := root.Readdir(-1)
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
for _, file := range files {
|
|
// We exclude any regular file and the "CAs/" directory.
|
|
// The "CAs/" directory contains (root) CA certificates
|
|
// that MinIO adds to its list of trusted roots (tls.Config.RootCAs).
|
|
// Therefore, "CAs/" does not contain X.509 certificates that
|
|
// are meant to be served by MinIO.
|
|
if !file.IsDir() || file.Name() == "CAs" {
|
|
continue
|
|
}
|
|
|
|
var (
|
|
certFile = filepath.Join(root.Name(), file.Name(), publicCertFile)
|
|
keyFile = filepath.Join(root.Name(), file.Name(), privateKeyFile)
|
|
)
|
|
if !isFile(certFile) || !isFile(keyFile) {
|
|
continue
|
|
}
|
|
if err := manager.AddCertificate(certFile, keyFile); err != nil {
|
|
err = fmt.Errorf("Failed to load TLS certificate '%s': %v", certFile, err)
|
|
logger.LogIf(GlobalContext, err, logger.Minio)
|
|
}
|
|
}
|
|
secureConn = true
|
|
return x509Certs, manager, secureConn, nil
|
|
}
|