github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/common-main.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "bufio" 22 "bytes" 23 "context" 24 "crypto/tls" 25 "crypto/x509" 26 "encoding/gob" 27 "encoding/pem" 28 "errors" 29 "fmt" 30 "net" 31 "net/url" 32 "os" 33 "path" 34 "path/filepath" 35 "runtime" 36 "sort" 37 "strconv" 38 "strings" 39 "syscall" 40 "time" 41 42 "github.com/dustin/go-humanize" 43 fcolor "github.com/fatih/color" 44 "github.com/go-openapi/loads" 45 "github.com/inconshreveable/mousetrap" 46 dns2 "github.com/miekg/dns" 47 "github.com/minio/cli" 48 consoleapi "github.com/minio/console/api" 49 "github.com/minio/console/api/operations" 50 consoleoauth2 "github.com/minio/console/pkg/auth/idp/oauth2" 51 consoleCerts "github.com/minio/console/pkg/certs" 52 "github.com/minio/kms-go/kes" 53 "github.com/minio/madmin-go/v3" 54 "github.com/minio/minio-go/v7" 55 "github.com/minio/minio-go/v7/pkg/set" 56 "github.com/minio/minio/internal/auth" 57 "github.com/minio/minio/internal/color" 58 "github.com/minio/minio/internal/config" 59 "github.com/minio/minio/internal/kms" 60 "github.com/minio/minio/internal/logger" 61 "github.com/minio/pkg/v2/certs" 62 "github.com/minio/pkg/v2/console" 63 "github.com/minio/pkg/v2/ellipses" 64 "github.com/minio/pkg/v2/env" 65 xnet "github.com/minio/pkg/v2/net" 66 ) 67 68 // serverDebugLog will enable debug printing 69 var ( 70 serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn 71 currentReleaseTime time.Time 72 orchestrated = IsKubernetes() || IsDocker() 73 ) 74 75 func init() { 76 if runtime.GOOS == "windows" { 77 if mousetrap.StartedByExplorer() { 78 fmt.Printf("Don't double-click %s\n", os.Args[0]) 79 fmt.Println("You need to open cmd.exe/PowerShell and run it from the command line") 80 fmt.Println("Refer to the docs here on how to run it as a Windows Service https://github.com/minio/minio-service/tree/master/windows") 81 fmt.Println("Press the Enter Key to Exit") 82 fmt.Scanln() 83 os.Exit(1) 84 } 85 } 86 87 logger.Init(GOPATH, GOROOT) 88 logger.RegisterError(config.FmtError) 89 90 globalBatchJobsMetrics = batchJobMetrics{metrics: make(map[string]*batchJobInfo)} 91 go globalBatchJobsMetrics.purgeJobMetrics() 92 93 t, _ := minioVersionToReleaseTime(Version) 94 if !t.IsZero() { 95 globalVersionUnix = uint64(t.Unix()) 96 } 97 98 globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != "" 99 100 console.SetColor("Debug", fcolor.New()) 101 102 gob.Register(StorageErr("")) 103 gob.Register(madmin.TimeInfo{}) 104 gob.Register(madmin.XFSErrorConfigs{}) 105 gob.Register(map[string]string{}) 106 gob.Register(map[string]interface{}{}) 107 108 // All minio-go and madmin-go API operations shall be performed only once, 109 // another way to look at this is we are turning off retries. 110 minio.MaxRetry = 1 111 madmin.MaxRetry = 1 112 113 currentReleaseTime, _ = GetCurrentReleaseTime() 114 } 115 116 const consolePrefix = "CONSOLE_" 117 118 func minioConfigToConsoleFeatures() { 119 os.Setenv("CONSOLE_PBKDF_SALT", globalDeploymentID()) 120 os.Setenv("CONSOLE_PBKDF_PASSPHRASE", globalDeploymentID()) 121 if globalMinioEndpoint != "" { 122 os.Setenv("CONSOLE_MINIO_SERVER", globalMinioEndpoint) 123 } else { 124 // Explicitly set 127.0.0.1 so Console will automatically bypass TLS verification to the local S3 API. 125 // This will save users from providing a certificate with IP or FQDN SAN that points to the local host. 126 os.Setenv("CONSOLE_MINIO_SERVER", fmt.Sprintf("%s://127.0.0.1:%s", getURLScheme(globalIsTLS), globalMinioPort)) 127 } 128 if value := env.Get(config.EnvMinIOLogQueryURL, ""); value != "" { 129 os.Setenv("CONSOLE_LOG_QUERY_URL", value) 130 if value := env.Get(config.EnvMinIOLogQueryAuthToken, ""); value != "" { 131 os.Setenv("CONSOLE_LOG_QUERY_AUTH_TOKEN", value) 132 } 133 } 134 // pass the console subpath configuration 135 if globalBrowserRedirectURL != nil { 136 subPath := path.Clean(pathJoin(strings.TrimSpace(globalBrowserRedirectURL.Path), SlashSeparator)) 137 if subPath != SlashSeparator { 138 os.Setenv("CONSOLE_SUBPATH", subPath) 139 } 140 } 141 // Enable if prometheus URL is set. 142 if value := env.Get(config.EnvMinIOPrometheusURL, ""); value != "" { 143 os.Setenv("CONSOLE_PROMETHEUS_URL", value) 144 if value := env.Get(config.EnvMinIOPrometheusJobID, "minio-job"); value != "" { 145 os.Setenv("CONSOLE_PROMETHEUS_JOB_ID", value) 146 // Support additional labels for more granular filtering. 147 if value := env.Get(config.EnvMinIOPrometheusExtraLabels, ""); value != "" { 148 os.Setenv("CONSOLE_PROMETHEUS_EXTRA_LABELS", value) 149 } 150 } 151 // Support Prometheus Auth Token 152 if value := env.Get(config.EnvMinIOPrometheusAuthToken, ""); value != "" { 153 os.Setenv("CONSOLE_PROMETHEUS_AUTH_TOKEN", value) 154 } 155 } 156 // Enable if LDAP is enabled. 157 if globalIAMSys.LDAPConfig.Enabled() { 158 os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn) 159 } 160 // Handle animation in welcome page 161 if value := env.Get(config.EnvBrowserLoginAnimation, "on"); value != "" { 162 os.Setenv("CONSOLE_ANIMATED_LOGIN", value) 163 } 164 165 // Pass on the session duration environment variable, else we will default to 12 hours 166 if valueSts := env.Get(config.EnvMinioStsDuration, ""); valueSts != "" { 167 os.Setenv("CONSOLE_STS_DURATION", valueSts) 168 } else if valueSession := env.Get(config.EnvBrowserSessionDuration, ""); valueSession != "" { 169 os.Setenv("CONSOLE_STS_DURATION", valueSession) 170 } 171 172 os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region) 173 os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", "")) 174 175 // This section sets Browser (console) stored config 176 if valueSCP := globalBrowserConfig.GetCSPolicy(); valueSCP != "" { 177 os.Setenv("CONSOLE_SECURE_CONTENT_SECURITY_POLICY", valueSCP) 178 } 179 180 if hstsSeconds := globalBrowserConfig.GetHSTSSeconds(); hstsSeconds > 0 { 181 isubdom := globalBrowserConfig.IsHSTSIncludeSubdomains() 182 isprel := globalBrowserConfig.IsHSTSPreload() 183 os.Setenv("CONSOLE_SECURE_STS_SECONDS", strconv.Itoa(hstsSeconds)) 184 os.Setenv("CONSOLE_SECURE_STS_INCLUDE_SUB_DOMAINS", isubdom) 185 os.Setenv("CONSOLE_SECURE_STS_PRELOAD", isprel) 186 } 187 188 if valueRefer := globalBrowserConfig.GetReferPolicy(); valueRefer != "" { 189 os.Setenv("CONSOLE_SECURE_REFERRER_POLICY", valueRefer) 190 } 191 192 globalSubnetConfig.ApplyEnv() 193 } 194 195 func buildOpenIDConsoleConfig() consoleoauth2.OpenIDPCfg { 196 pcfgs := globalIAMSys.OpenIDConfig.ProviderCfgs 197 m := make(map[string]consoleoauth2.ProviderConfig, len(pcfgs)) 198 for name, cfg := range pcfgs { 199 callback := getConsoleEndpoints()[0] + "/oauth_callback" 200 if cfg.RedirectURI != "" { 201 callback = cfg.RedirectURI 202 } 203 m[name] = consoleoauth2.ProviderConfig{ 204 URL: cfg.URL.String(), 205 DisplayName: cfg.DisplayName, 206 ClientID: cfg.ClientID, 207 ClientSecret: cfg.ClientSecret, 208 HMACSalt: globalDeploymentID(), 209 HMACPassphrase: cfg.ClientID, 210 Scopes: strings.Join(cfg.DiscoveryDoc.ScopesSupported, ","), 211 Userinfo: cfg.ClaimUserinfo, 212 RedirectCallbackDynamic: cfg.RedirectURIDynamic, 213 RedirectCallback: callback, 214 EndSessionEndpoint: cfg.DiscoveryDoc.EndSessionEndpoint, 215 RoleArn: cfg.GetRoleArn(), 216 } 217 } 218 return m 219 } 220 221 func initConsoleServer() (*consoleapi.Server, error) { 222 // unset all console_ environment variables. 223 for _, cenv := range env.List(consolePrefix) { 224 os.Unsetenv(cenv) 225 } 226 227 // enable all console environment variables 228 minioConfigToConsoleFeatures() 229 230 // set certs dir to minio directory 231 consoleCerts.GlobalCertsDir = &consoleCerts.ConfigDir{ 232 Path: globalCertsDir.Get(), 233 } 234 consoleCerts.GlobalCertsCADir = &consoleCerts.ConfigDir{ 235 Path: globalCertsCADir.Get(), 236 } 237 238 // set certs before other console initialization 239 consoleapi.GlobalRootCAs, consoleapi.GlobalPublicCerts, consoleapi.GlobalTLSCertsManager = globalRootCAs, globalPublicCerts, globalTLSCerts 240 241 swaggerSpec, err := loads.Embedded(consoleapi.SwaggerJSON, consoleapi.FlatSwaggerJSON) 242 if err != nil { 243 return nil, err 244 } 245 246 api := operations.NewConsoleAPI(swaggerSpec) 247 248 if !serverDebugLog { 249 // Disable console logging if server debug log is not enabled 250 noLog := func(string, ...interface{}) {} 251 252 consoleapi.LogInfo = noLog 253 consoleapi.LogError = noLog 254 api.Logger = noLog 255 } 256 257 // Pass in console application config. This needs to happen before the 258 // ConfigureAPI() call. 259 consoleapi.GlobalMinIOConfig = consoleapi.MinIOConfig{ 260 OpenIDProviders: buildOpenIDConsoleConfig(), 261 } 262 263 server := consoleapi.NewServer(api) 264 // register all APIs 265 server.ConfigureAPI() 266 267 consolePort, _ := strconv.Atoi(globalMinioConsolePort) 268 269 server.Host = globalMinioConsoleHost 270 server.Port = consolePort 271 consoleapi.Port = globalMinioConsolePort 272 consoleapi.Hostname = globalMinioConsoleHost 273 274 if globalIsTLS { 275 // If TLS certificates are provided enforce the HTTPS. 276 server.EnabledListeners = []string{"https"} 277 server.TLSPort = consolePort 278 // Need to store tls-port, tls-host un config variables so secure.middleware can read from there 279 consoleapi.TLSPort = globalMinioConsolePort 280 consoleapi.Hostname = globalMinioConsoleHost 281 } 282 283 return server, nil 284 } 285 286 // Check for updates and print a notification message 287 func checkUpdate(mode string) { 288 updateURL := minioReleaseInfoURL 289 if runtime.GOOS == globalWindowsOSName { 290 updateURL = minioReleaseWindowsInfoURL 291 } 292 293 u, err := url.Parse(updateURL) 294 if err != nil { 295 return 296 } 297 298 if currentReleaseTime.IsZero() { 299 return 300 } 301 302 _, lrTime, err := getLatestReleaseTime(u, 2*time.Second, mode) 303 if err != nil { 304 return 305 } 306 307 var older time.Duration 308 var downloadURL string 309 if lrTime.After(currentReleaseTime) { 310 older = lrTime.Sub(currentReleaseTime) 311 downloadURL = getDownloadURL(releaseTimeToReleaseTag(lrTime)) 312 } 313 314 updateMsg := prepareUpdateMessage(downloadURL, older) 315 if updateMsg == "" { 316 return 317 } 318 319 logger.Info(prepareUpdateMessage("Run `mc admin update ALIAS`", lrTime.Sub(currentReleaseTime))) 320 } 321 322 func newConfigDir(dir string, dirSet bool, getDefaultDir func() string) (*ConfigDir, error) { 323 if dir == "" { 324 dir = getDefaultDir() 325 } 326 327 if dir == "" { 328 if !dirSet { 329 return nil, fmt.Errorf("missing option must be provided") 330 } 331 return nil, fmt.Errorf("provided option cannot be empty") 332 } 333 334 // Disallow relative paths, figure out absolute paths. 335 dirAbs, err := filepath.Abs(dir) 336 if err != nil { 337 return nil, err 338 } 339 err = mkdirAllIgnorePerm(dirAbs) 340 if err != nil { 341 return nil, fmt.Errorf("unable to create the directory `%s`: %w", dirAbs, err) 342 } 343 344 return &ConfigDir{path: dirAbs}, nil 345 } 346 347 func buildServerCtxt(ctx *cli.Context, ctxt *serverCtxt) (err error) { 348 // Get "json" flag from command line argument and 349 ctxt.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json") 350 // Get quiet flag from command line argument. 351 ctxt.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet") 352 // Get anonymous flag from command line argument. 353 ctxt.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous") 354 // Fetch address option 355 ctxt.Addr = ctx.GlobalString("address") 356 if ctxt.Addr == "" || ctxt.Addr == ":"+GlobalMinioDefaultPort { 357 ctxt.Addr = ctx.String("address") 358 } 359 360 // Fetch console address option 361 ctxt.ConsoleAddr = ctx.GlobalString("console-address") 362 if ctxt.ConsoleAddr == "" { 363 ctxt.ConsoleAddr = ctx.String("console-address") 364 } 365 366 if cxml := ctx.String("crossdomain-xml"); cxml != "" { 367 buf, err := os.ReadFile(cxml) 368 if err != nil { 369 return err 370 } 371 ctxt.CrossDomainXML = string(buf) 372 } 373 374 // Check "no-compat" flag from command line argument. 375 ctxt.StrictS3Compat = !(ctx.IsSet("no-compat") || ctx.GlobalIsSet("no-compat")) 376 377 switch { 378 case ctx.IsSet("config-dir"): 379 ctxt.ConfigDir = ctx.String("config-dir") 380 ctxt.configDirSet = true 381 case ctx.GlobalIsSet("config-dir"): 382 ctxt.ConfigDir = ctx.GlobalString("config-dir") 383 ctxt.configDirSet = true 384 } 385 386 switch { 387 case ctx.IsSet("certs-dir"): 388 ctxt.CertsDir = ctx.String("certs-dir") 389 ctxt.certsDirSet = true 390 case ctx.GlobalIsSet("certs-dir"): 391 ctxt.CertsDir = ctx.GlobalString("certs-dir") 392 ctxt.certsDirSet = true 393 } 394 395 ctxt.FTP = ctx.StringSlice("ftp") 396 ctxt.SFTP = ctx.StringSlice("sftp") 397 398 ctxt.Interface = ctx.String("interface") 399 ctxt.UserTimeout = ctx.Duration("conn-user-timeout") 400 ctxt.ConnReadDeadline = ctx.Duration("conn-read-deadline") 401 ctxt.ConnWriteDeadline = ctx.Duration("conn-write-deadline") 402 ctxt.ConnClientReadDeadline = ctx.Duration("conn-client-read-deadline") 403 ctxt.ConnClientWriteDeadline = ctx.Duration("conn-client-write-deadline") 404 405 ctxt.ShutdownTimeout = ctx.Duration("shutdown-timeout") 406 ctxt.IdleTimeout = ctx.Duration("idle-timeout") 407 ctxt.ReadHeaderTimeout = ctx.Duration("read-header-timeout") 408 ctxt.MaxIdleConnsPerHost = ctx.Int("max-idle-conns-per-host") 409 410 if conf := ctx.String("config"); len(conf) > 0 { 411 err = mergeServerCtxtFromConfigFile(conf, ctxt) 412 } else { 413 err = mergeDisksLayoutFromArgs(serverCmdArgs(ctx), ctxt) 414 } 415 416 return err 417 } 418 419 func handleCommonArgs(ctxt serverCtxt) { 420 if ctxt.JSON { 421 logger.EnableJSON() 422 } 423 if ctxt.Quiet { 424 logger.EnableQuiet() 425 } 426 if ctxt.Anonymous { 427 logger.EnableAnonymous() 428 } 429 430 consoleAddr := ctxt.ConsoleAddr 431 addr := ctxt.Addr 432 configDir := ctxt.ConfigDir 433 configSet := ctxt.configDirSet 434 certsDir := ctxt.CertsDir 435 certsSet := ctxt.certsDirSet 436 437 if consoleAddr == "" { 438 p, err := xnet.GetFreePort() 439 if err != nil { 440 logger.FatalIf(err, "Unable to get free port for Console UI on the host") 441 } 442 // hold the port 443 l, err := net.Listen("TCP", fmt.Sprintf(":%s", p.String())) 444 if err == nil { 445 defer l.Close() 446 } 447 consoleAddr = net.JoinHostPort("", p.String()) 448 } 449 450 if _, _, err := net.SplitHostPort(consoleAddr); err != nil { 451 logger.FatalIf(err, "Unable to start listening on console port") 452 } 453 454 if consoleAddr == addr { 455 logger.FatalIf(errors.New("--console-address cannot be same as --address"), "Unable to start the server") 456 } 457 458 globalMinioHost, globalMinioPort = mustSplitHostPort(addr) 459 if globalMinioPort == "0" { 460 p, err := xnet.GetFreePort() 461 if err != nil { 462 logger.FatalIf(err, "Unable to get free port for S3 API on the host") 463 } 464 globalMinioPort = p.String() 465 globalDynamicAPIPort = true 466 } 467 468 globalMinioConsoleHost, globalMinioConsolePort = mustSplitHostPort(consoleAddr) 469 470 if globalMinioPort == globalMinioConsolePort { 471 logger.FatalIf(errors.New("--console-address port cannot be same as --address port"), "Unable to start the server") 472 } 473 474 globalMinioAddr = addr 475 476 // Set all config, certs and CAs directories. 477 var err error 478 globalConfigDir, err = newConfigDir(configDir, configSet, defaultConfigDir.Get) 479 logger.FatalIf(err, "Unable to initialize the (deprecated) config directory") 480 globalCertsDir, err = newConfigDir(certsDir, certsSet, defaultCertsDir.Get) 481 logger.FatalIf(err, "Unable to initialize the certs directory") 482 483 // Remove this code when we deprecate and remove config-dir. 484 // This code is to make sure we inherit from the config-dir 485 // option if certs-dir is not provided. 486 if !certsSet && configSet { 487 globalCertsDir = &ConfigDir{path: filepath.Join(globalConfigDir.Get(), certsDir)} 488 } 489 490 globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)} 491 492 logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get()) 493 } 494 495 func runDNSCache(ctx *cli.Context) { 496 dnsTTL := ctx.Duration("dns-cache-ttl") 497 // Check if we have configured a custom DNS cache TTL. 498 if dnsTTL <= 0 { 499 if orchestrated { 500 dnsTTL = 30 * time.Second 501 } else { 502 dnsTTL = 10 * time.Minute 503 } 504 } 505 506 // Call to refresh will refresh names in cache. 507 go func() { 508 // Baremetal setups set DNS refresh window up to dnsTTL duration. 509 t := time.NewTicker(dnsTTL) 510 defer t.Stop() 511 for { 512 select { 513 case <-t.C: 514 globalDNSCache.Refresh() 515 516 case <-GlobalContext.Done(): 517 return 518 } 519 } 520 }() 521 } 522 523 type envKV struct { 524 Key string 525 Value string 526 Skip bool 527 } 528 529 func (e envKV) String() string { 530 if e.Skip { 531 return "" 532 } 533 return fmt.Sprintf("%s=%s", e.Key, e.Value) 534 } 535 536 func parsEnvEntry(envEntry string) (envKV, error) { 537 envEntry = strings.TrimSpace(envEntry) 538 if envEntry == "" { 539 // Skip all empty lines 540 return envKV{ 541 Skip: true, 542 }, nil 543 } 544 if strings.HasPrefix(envEntry, "#") { 545 // Skip commented lines 546 return envKV{ 547 Skip: true, 548 }, nil 549 } 550 envTokens := strings.SplitN(strings.TrimSpace(strings.TrimPrefix(envEntry, "export")), config.EnvSeparator, 2) 551 if len(envTokens) != 2 { 552 return envKV{}, fmt.Errorf("envEntry malformed; %s, expected to be of form 'KEY=value'", envEntry) 553 } 554 555 key := envTokens[0] 556 val := envTokens[1] 557 558 // Remove quotes from the value if found 559 if len(val) >= 2 { 560 quote := val[0] 561 if (quote == '"' || quote == '\'') && val[len(val)-1] == quote { 562 val = val[1 : len(val)-1] 563 } 564 } 565 566 return envKV{ 567 Key: key, 568 Value: val, 569 }, nil 570 } 571 572 // Similar to os.Environ returns a copy of strings representing 573 // the environment values from a file, in the form "key, value". 574 // in a structured form. 575 func minioEnvironFromFile(envConfigFile string) ([]envKV, error) { 576 f, err := Open(envConfigFile) 577 if err != nil { 578 return nil, err 579 } 580 defer f.Close() 581 var ekvs []envKV 582 scanner := bufio.NewScanner(f) 583 for scanner.Scan() { 584 ekv, err := parsEnvEntry(scanner.Text()) 585 if err != nil { 586 return nil, err 587 } 588 if ekv.Skip { 589 // Skips empty lines 590 continue 591 } 592 ekvs = append(ekvs, ekv) 593 } 594 if err = scanner.Err(); err != nil { 595 return nil, err 596 } 597 return ekvs, nil 598 } 599 600 func readFromSecret(sp string) (string, error) { 601 // Supports reading path from docker secrets, filename is 602 // relative to /run/secrets/ position. 603 if isFile(pathJoin("/run/secrets/", sp)) { 604 sp = pathJoin("/run/secrets/", sp) 605 } 606 credBuf, err := os.ReadFile(sp) 607 if err != nil { 608 if os.IsNotExist(err) { // ignore if file doesn't exist. 609 return "", nil 610 } 611 return "", err 612 } 613 return string(bytes.TrimSpace(credBuf)), nil 614 } 615 616 func loadEnvVarsFromFiles() { 617 if env.IsSet(config.EnvAccessKeyFile) { 618 accessKey, err := readFromSecret(env.Get(config.EnvAccessKeyFile, "")) 619 if err != nil { 620 logger.Fatal(config.ErrInvalidCredentials(err), 621 "Unable to validate credentials inherited from the secret file(s)") 622 } 623 if accessKey != "" { 624 os.Setenv(config.EnvRootUser, accessKey) 625 } 626 } 627 628 if env.IsSet(config.EnvSecretKeyFile) { 629 secretKey, err := readFromSecret(env.Get(config.EnvSecretKeyFile, "")) 630 if err != nil { 631 logger.Fatal(config.ErrInvalidCredentials(err), 632 "Unable to validate credentials inherited from the secret file(s)") 633 } 634 if secretKey != "" { 635 os.Setenv(config.EnvRootPassword, secretKey) 636 } 637 } 638 639 if env.IsSet(config.EnvRootUserFile) { 640 rootUser, err := readFromSecret(env.Get(config.EnvRootUserFile, "")) 641 if err != nil { 642 logger.Fatal(config.ErrInvalidCredentials(err), 643 "Unable to validate credentials inherited from the secret file(s)") 644 } 645 if rootUser != "" { 646 os.Setenv(config.EnvRootUser, rootUser) 647 } 648 } 649 650 if env.IsSet(config.EnvRootPasswordFile) { 651 rootPassword, err := readFromSecret(env.Get(config.EnvRootPasswordFile, "")) 652 if err != nil { 653 logger.Fatal(config.ErrInvalidCredentials(err), 654 "Unable to validate credentials inherited from the secret file(s)") 655 } 656 if rootPassword != "" { 657 os.Setenv(config.EnvRootPassword, rootPassword) 658 } 659 } 660 661 if env.IsSet(kms.EnvKMSSecretKeyFile) { 662 kmsSecret, err := readFromSecret(env.Get(kms.EnvKMSSecretKeyFile, "")) 663 if err != nil { 664 logger.Fatal(err, "Unable to read the KMS secret key inherited from secret file") 665 } 666 if kmsSecret != "" { 667 os.Setenv(kms.EnvKMSSecretKey, kmsSecret) 668 } 669 } 670 671 if env.IsSet(config.EnvConfigEnvFile) { 672 ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, "")) 673 if err != nil && !os.IsNotExist(err) { 674 logger.Fatal(err, "Unable to read the config environment file") 675 } 676 for _, ekv := range ekvs { 677 os.Setenv(ekv.Key, ekv.Value) 678 } 679 } 680 } 681 682 func serverHandleEnvVars() { 683 var err error 684 globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn)) 685 if err != nil { 686 logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable") 687 } 688 if globalBrowserEnabled { 689 if redirectURL := env.Get(config.EnvBrowserRedirectURL, ""); redirectURL != "" { 690 u, err := xnet.ParseHTTPURL(redirectURL) 691 if err != nil { 692 logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable") 693 } 694 // Look for if URL has invalid values and return error. 695 if !((u.Scheme == "http" || u.Scheme == "https") && 696 u.Opaque == "" && 697 !u.ForceQuery && u.RawQuery == "" && u.Fragment == "") { 698 err := fmt.Errorf("URL contains unexpected resources, expected URL to be one of http(s)://console.example.com or as a subpath via API endpoint http(s)://minio.example.com/minio format: %v", u) 699 logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable") 700 } 701 globalBrowserRedirectURL = u 702 } 703 globalBrowserRedirect = env.Get(config.EnvBrowserRedirect, config.EnableOn) == config.EnableOn 704 } 705 706 if serverURL := env.Get(config.EnvMinIOServerURL, ""); serverURL != "" { 707 u, err := xnet.ParseHTTPURL(serverURL) 708 if err != nil { 709 logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable") 710 } 711 // Look for if URL has invalid values and return error. 712 if !((u.Scheme == "http" || u.Scheme == "https") && 713 (u.Path == "/" || u.Path == "") && u.Opaque == "" && 714 !u.ForceQuery && u.RawQuery == "" && u.Fragment == "") { 715 err := fmt.Errorf("URL contains unexpected resources, expected URL to be of http(s)://minio.example.com format: %v", u) 716 logger.Fatal(err, "Invalid MINIO_SERVER_URL value is environment variable") 717 } 718 u.Path = "" // remove any path component such as `/` 719 globalMinioEndpoint = u.String() 720 globalMinioEndpointURL = u 721 } 722 723 globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff)) 724 if err != nil { 725 logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable") 726 } 727 728 rootDiskSize := env.Get(config.EnvRootDriveThresholdSize, "") 729 if rootDiskSize == "" { 730 rootDiskSize = env.Get(config.EnvRootDiskThresholdSize, "") 731 } 732 if rootDiskSize != "" { 733 size, err := humanize.ParseBytes(rootDiskSize) 734 if err != nil { 735 logger.Fatal(err, fmt.Sprintf("Invalid %s value in root drive threshold environment variable", rootDiskSize)) 736 } 737 globalRootDiskThreshold = size 738 } 739 740 domains := env.Get(config.EnvDomain, "") 741 if len(domains) != 0 { 742 for _, domainName := range strings.Split(domains, config.ValueSeparator) { 743 if _, ok := dns2.IsDomainName(domainName); !ok { 744 logger.Fatal(config.ErrInvalidDomainValue(nil).Msg("Unknown value `%s`", domainName), 745 "Invalid MINIO_DOMAIN value in environment variable") 746 } 747 globalDomainNames = append(globalDomainNames, domainName) 748 } 749 sort.Strings(globalDomainNames) 750 lcpSuf := lcpSuffix(globalDomainNames) 751 for _, domainName := range globalDomainNames { 752 if domainName == lcpSuf && len(globalDomainNames) > 1 { 753 logger.Fatal(config.ErrOverlappingDomainValue(nil).Msg("Overlapping domains `%s` not allowed", globalDomainNames), 754 "Invalid MINIO_DOMAIN value in environment variable") 755 } 756 } 757 } 758 759 publicIPs := env.Get(config.EnvPublicIPs, "") 760 if len(publicIPs) != 0 { 761 minioEndpoints := strings.Split(publicIPs, config.ValueSeparator) 762 domainIPs := set.NewStringSet() 763 for _, endpoint := range minioEndpoints { 764 if net.ParseIP(endpoint) == nil { 765 // Checking if the IP is a DNS entry. 766 addrs, err := globalDNSCache.LookupHost(GlobalContext, endpoint) 767 if err != nil { 768 logger.FatalIf(err, "Unable to initialize MinIO server with [%s] invalid entry found in MINIO_PUBLIC_IPS", endpoint) 769 } 770 for _, addr := range addrs { 771 domainIPs.Add(addr) 772 } 773 } 774 domainIPs.Add(endpoint) 775 } 776 updateDomainIPs(domainIPs) 777 } else { 778 // Add found interfaces IP address to global domain IPS, 779 // loopback addresses will be naturally dropped. 780 domainIPs := mustGetLocalIP4() 781 for _, host := range globalEndpoints.Hostnames() { 782 domainIPs.Add(host) 783 } 784 updateDomainIPs(domainIPs) 785 } 786 787 // In place update is true by default if the MINIO_UPDATE is not set 788 // or is not set to 'off', if MINIO_UPDATE is set to 'off' then 789 // in-place update is off. 790 globalInplaceUpdateDisabled = strings.EqualFold(env.Get(config.EnvUpdate, config.EnableOn), config.EnableOff) 791 792 // Check if the supported credential env vars, 793 // "MINIO_ROOT_USER" and "MINIO_ROOT_PASSWORD" are provided 794 // Warn user if deprecated environment variables, 795 // "MINIO_ACCESS_KEY" and "MINIO_SECRET_KEY", are defined 796 // Check all error conditions first 797 //nolint:gocritic 798 if !env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) { 799 logger.Fatal(config.ErrMissingEnvCredentialRootUser(nil), "Unable to start MinIO") 800 } else if env.IsSet(config.EnvRootUser) && !env.IsSet(config.EnvRootPassword) { 801 logger.Fatal(config.ErrMissingEnvCredentialRootPassword(nil), "Unable to start MinIO") 802 } else if !env.IsSet(config.EnvRootUser) && !env.IsSet(config.EnvRootPassword) { 803 if !env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) { 804 logger.Fatal(config.ErrMissingEnvCredentialAccessKey(nil), "Unable to start MinIO") 805 } else if env.IsSet(config.EnvAccessKey) && !env.IsSet(config.EnvSecretKey) { 806 logger.Fatal(config.ErrMissingEnvCredentialSecretKey(nil), "Unable to start MinIO") 807 } 808 } 809 810 globalDisableFreezeOnBoot = env.Get("_MINIO_DISABLE_API_FREEZE_ON_BOOT", "") == "true" || serverDebugLog 811 } 812 813 func loadRootCredentials() { 814 // At this point, either both environment variables 815 // are defined or both are not defined. 816 // Check both cases and authenticate them if correctly defined 817 var user, password string 818 var hasCredentials bool 819 //nolint:gocritic 820 if env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) { 821 user = env.Get(config.EnvRootUser, "") 822 password = env.Get(config.EnvRootPassword, "") 823 hasCredentials = true 824 } else if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) { 825 user = env.Get(config.EnvAccessKey, "") 826 password = env.Get(config.EnvSecretKey, "") 827 hasCredentials = true 828 } else if globalServerCtxt.RootUser != "" && globalServerCtxt.RootPwd != "" { 829 user, password = globalServerCtxt.RootUser, globalServerCtxt.RootPwd 830 hasCredentials = true 831 } 832 if hasCredentials { 833 cred, err := auth.CreateCredentials(user, password) 834 if err != nil { 835 logger.Fatal(config.ErrInvalidCredentials(err), 836 "Unable to validate credentials inherited from the shell environment") 837 } 838 if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) { 839 msg := fmt.Sprintf("WARNING: %s and %s are deprecated.\n"+ 840 " Please use %s and %s", 841 config.EnvAccessKey, config.EnvSecretKey, 842 config.EnvRootUser, config.EnvRootPassword) 843 logger.Info(color.RedBold(msg)) 844 } 845 globalActiveCred = cred 846 globalCredViaEnv = true 847 } else { 848 globalActiveCred = auth.DefaultCredentials 849 } 850 } 851 852 // Initialize KMS global variable after valiadating and loading the configuration. 853 // It depends on KMS env variables and global cli flags. 854 func handleKMSConfig() { 855 if env.IsSet(kms.EnvKMSSecretKey) && env.IsSet(kms.EnvKESEndpoint) { 856 logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKMSSecretKey, kms.EnvKESEndpoint)) 857 } 858 859 if env.IsSet(kms.EnvKMSSecretKey) { 860 KMS, err := kms.Parse(env.Get(kms.EnvKMSSecretKey, "")) 861 if err != nil { 862 logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment") 863 } 864 GlobalKMS = KMS 865 } 866 if env.IsSet(kms.EnvKESEndpoint) { 867 if env.IsSet(kms.EnvKESAPIKey) { 868 if env.IsSet(kms.EnvKESClientKey) { 869 logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientKey)) 870 } 871 if env.IsSet(kms.EnvKESClientCert) { 872 logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientCert)) 873 } 874 } 875 if !env.IsSet(kms.EnvKESKeyName) { 876 logger.Fatal(errors.New("Invalid KES configuration"), fmt.Sprintf("The mandatory environment variable %q not set", kms.EnvKESKeyName)) 877 } 878 879 var endpoints []string 880 for _, endpoint := range strings.Split(env.Get(kms.EnvKESEndpoint, ""), ",") { 881 if strings.TrimSpace(endpoint) == "" { 882 continue 883 } 884 if !ellipses.HasEllipses(endpoint) { 885 endpoints = append(endpoints, endpoint) 886 continue 887 } 888 patterns, err := ellipses.FindEllipsesPatterns(endpoint) 889 if err != nil { 890 logger.Fatal(err, fmt.Sprintf("Invalid KES endpoint %q", endpoint)) 891 } 892 for _, lbls := range patterns.Expand() { 893 endpoints = append(endpoints, strings.Join(lbls, "")) 894 } 895 } 896 rootCAs, err := certs.GetRootCAs(env.Get(kms.EnvKESServerCA, globalCertsCADir.Get())) 897 if err != nil { 898 logger.Fatal(err, fmt.Sprintf("Unable to load X.509 root CAs for KES from %q", env.Get(kms.EnvKESServerCA, globalCertsCADir.Get()))) 899 } 900 901 var kmsConf kms.Config 902 if env.IsSet(kms.EnvKESAPIKey) { 903 key, err := kes.ParseAPIKey(env.Get(kms.EnvKESAPIKey, "")) 904 if err != nil { 905 logger.Fatal(err, fmt.Sprintf("Failed to parse KES API key from %q", env.Get(kms.EnvKESAPIKey, ""))) 906 } 907 kmsConf = kms.Config{ 908 Endpoints: endpoints, 909 DefaultKeyID: env.Get(kms.EnvKESKeyName, ""), 910 APIKey: key, 911 RootCAs: rootCAs, 912 } 913 } else { 914 loadX509KeyPair := func(certFile, keyFile string) (tls.Certificate, error) { 915 // Manually load the certificate and private key into memory. 916 // We need to check whether the private key is encrypted, and 917 // if so, decrypt it using the user-provided password. 918 certBytes, err := os.ReadFile(certFile) 919 if err != nil { 920 return tls.Certificate{}, fmt.Errorf("Unable to load KES client certificate as specified by the shell environment: %v", err) 921 } 922 keyBytes, err := os.ReadFile(keyFile) 923 if err != nil { 924 return tls.Certificate{}, fmt.Errorf("Unable to load KES client private key as specified by the shell environment: %v", err) 925 } 926 privateKeyPEM, rest := pem.Decode(bytes.TrimSpace(keyBytes)) 927 if len(rest) != 0 { 928 return tls.Certificate{}, errors.New("Unable to load KES client private key as specified by the shell environment: private key contains additional data") 929 } 930 if x509.IsEncryptedPEMBlock(privateKeyPEM) { 931 keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(kms.EnvKESClientPassword, ""))) 932 if err != nil { 933 return tls.Certificate{}, fmt.Errorf("Unable to decrypt KES client private key as specified by the shell environment: %v", err) 934 } 935 keyBytes = pem.EncodeToMemory(&pem.Block{Type: privateKeyPEM.Type, Bytes: keyBytes}) 936 } 937 certificate, err := tls.X509KeyPair(certBytes, keyBytes) 938 if err != nil { 939 return tls.Certificate{}, fmt.Errorf("Unable to load KES client certificate as specified by the shell environment: %v", err) 940 } 941 return certificate, nil 942 } 943 944 reloadCertEvents := make(chan tls.Certificate, 1) 945 certificate, err := certs.NewCertificate(env.Get(kms.EnvKESClientCert, ""), env.Get(kms.EnvKESClientKey, ""), loadX509KeyPair) 946 if err != nil { 947 logger.Fatal(err, "Failed to load KES client certificate") 948 } 949 certificate.Watch(context.Background(), 15*time.Minute, syscall.SIGHUP) 950 certificate.Notify(reloadCertEvents) 951 952 kmsConf = kms.Config{ 953 Endpoints: endpoints, 954 DefaultKeyID: env.Get(kms.EnvKESKeyName, ""), 955 Certificate: certificate, 956 ReloadCertEvents: reloadCertEvents, 957 RootCAs: rootCAs, 958 } 959 } 960 961 KMS, err := kms.NewWithConfig(kmsConf) 962 if err != nil { 963 logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment") 964 } 965 // We check that the default key ID exists or try to create it otherwise. 966 // This implicitly checks that we can communicate to KES. We don't treat 967 // a policy error as failure condition since MinIO may not have the permission 968 // to create keys - just to generate/decrypt data encryption keys. 969 if err = KMS.CreateKey(context.Background(), env.Get(kms.EnvKESKeyName, "")); err != nil && !errors.Is(err, kes.ErrKeyExists) && !errors.Is(err, kes.ErrNotAllowed) { 970 logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment") 971 } 972 GlobalKMS = KMS 973 } 974 } 975 976 func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) { 977 if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) { 978 return nil, nil, false, nil 979 } 980 981 if x509Certs, err = config.ParsePublicCertFile(getPublicCertFile()); err != nil { 982 return nil, nil, false, err 983 } 984 985 manager, err = certs.NewManager(GlobalContext, getPublicCertFile(), getPrivateKeyFile(), config.LoadX509KeyPair) 986 if err != nil { 987 return nil, nil, false, err 988 } 989 990 // MinIO has support for multiple certificates. It expects the following structure: 991 // certs/ 992 // │ 993 // ├─ public.crt 994 // ├─ private.key 995 // │ 996 // ├─ example.com/ 997 // │ │ 998 // │ ├─ public.crt 999 // │ └─ private.key 1000 // └─ foobar.org/ 1001 // │ 1002 // ├─ public.crt 1003 // └─ private.key 1004 // ... 1005 // 1006 // Therefore, we read all filenames in the cert directory and check 1007 // for each directory whether it contains a public.crt and private.key. 1008 // If so, we try to add it to certificate manager. 1009 root, err := Open(globalCertsDir.Get()) 1010 if err != nil { 1011 return nil, nil, false, err 1012 } 1013 defer root.Close() 1014 1015 files, err := root.Readdir(-1) 1016 if err != nil { 1017 return nil, nil, false, err 1018 } 1019 for _, file := range files { 1020 // Ignore all 1021 // - regular files 1022 // - "CAs" directory 1023 // - any directory which starts with ".." 1024 if file.Mode().IsRegular() || file.Name() == "CAs" || strings.HasPrefix(file.Name(), "..") { 1025 continue 1026 } 1027 if file.Mode()&os.ModeSymlink == os.ModeSymlink { 1028 file, err = Stat(filepath.Join(root.Name(), file.Name())) 1029 if err != nil { 1030 // not accessible ignore 1031 continue 1032 } 1033 if !file.IsDir() { 1034 continue 1035 } 1036 } 1037 1038 var ( 1039 certFile = filepath.Join(root.Name(), file.Name(), publicCertFile) 1040 keyFile = filepath.Join(root.Name(), file.Name(), privateKeyFile) 1041 ) 1042 if !isFile(certFile) || !isFile(keyFile) { 1043 continue 1044 } 1045 if err = manager.AddCertificate(certFile, keyFile); err != nil { 1046 err = fmt.Errorf("Unable to load TLS certificate '%s,%s': %w", certFile, keyFile, err) 1047 logger.LogIf(GlobalContext, err, logger.ErrorKind) 1048 } 1049 } 1050 secureConn = true 1051 1052 // Certs might be symlinks, reload them every 10 seconds. 1053 manager.UpdateReloadDuration(10 * time.Second) 1054 1055 // syscall.SIGHUP to reload the certs. 1056 manager.ReloadOnSignal(syscall.SIGHUP) 1057 1058 return x509Certs, manager, secureConn, nil 1059 } 1060 1061 // contextCanceled returns whether a context is canceled. 1062 func contextCanceled(ctx context.Context) bool { 1063 select { 1064 case <-ctx.Done(): 1065 return true 1066 default: 1067 return false 1068 } 1069 } 1070 1071 // bgContext returns a context that can be used for async operations. 1072 // Cancellation/timeouts are removed, so parent cancellations/timeout will 1073 // not propagate from parent. 1074 // Context values are preserved. 1075 // This can be used for goroutines that live beyond the parent context. 1076 func bgContext(parent context.Context) context.Context { 1077 return bgCtx{parent: parent} 1078 } 1079 1080 type bgCtx struct { 1081 parent context.Context 1082 } 1083 1084 func (a bgCtx) Done() <-chan struct{} { 1085 return nil 1086 } 1087 1088 func (a bgCtx) Err() error { 1089 return nil 1090 } 1091 1092 func (a bgCtx) Deadline() (deadline time.Time, ok bool) { 1093 return time.Time{}, false 1094 } 1095 1096 func (a bgCtx) Value(key interface{}) interface{} { 1097 return a.parent.Value(key) 1098 }