You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/cmd/base/servers.go

516 lines
15 KiB

package base
import (
"context"
"crypto/rand"
"crypto/tls"
"errors"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"sync"
"github.com/armon/go-metrics"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-hclog"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/internalshared/gatedwriter"
"github.com/hashicorp/vault/internalshared/reloadutil"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/helper/mlock"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/watchtower/globals"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/authenticate"
"github.com/hashicorp/watchtower/version"
"github.com/jinzhu/gorm"
"github.com/mitchellh/cli"
"google.golang.org/grpc/grpclog"
)
type Server struct {
*Command
InfoKeys []string
Info map[string]string
logOutput io.Writer
GatedWriter *gatedwriter.Writer
Logger hclog.Logger
CombineLogs bool
LogLevel hclog.Level
ControllerKMS wrapping.Wrapper
WorkerAuthKMS wrapping.Wrapper
SecureRandomReader io.Reader
InmemSink *metrics.InmemSink
PrometheusEnabled bool
ReloadFuncsLock *sync.RWMutex
ReloadFuncs map[string][]reloadutil.ReloadFunc
ShutdownFuncs []func() error
Listeners []*ServerListener
DefaultOrgId string
DevAuthMethodId string
DevDatabaseUrl string
DevDatabaseCleanupFunc func() error
Database *gorm.DB
}
func NewServer(cmd *Command) *Server {
return &Server{
Command: cmd,
InfoKeys: make([]string, 0, 20),
Info: make(map[string]string),
SecureRandomReader: rand.Reader,
ReloadFuncsLock: new(sync.RWMutex),
ReloadFuncs: make(map[string][]reloadutil.ReloadFunc),
}
}
func (b *Server) SetupLogging(flagLogLevel, flagLogFormat, configLogLevel, configLogFormat string) error {
b.logOutput = os.Stderr
if b.CombineLogs {
b.logOutput = os.Stdout
}
b.GatedWriter = gatedwriter.NewWriter(b.logOutput)
// Set up logging
logLevel, logFormat, err := ProcessLogLevelAndFormat(flagLogLevel, flagLogFormat, configLogLevel, configLogFormat)
if err != nil {
return err
}
b.Logger = hclog.New(&hclog.LoggerOptions{
Output: b.GatedWriter,
Level: logLevel,
// Note that if logFormat is either unspecified or standard, then
// the resulting logger's format will be standard.
JSONFormat: logFormat == logging.JSONFormat,
})
// create GRPC logger
namedGRPCLogFaker := b.Logger.Named("grpclogfaker")
grpclog.SetLogger(&GRPCLogFaker{
Logger: namedGRPCLogFaker,
Log: os.Getenv("WATCHTOWER_GRPC_LOGGING") != "",
})
b.Info["log level"] = logLevel.String()
b.InfoKeys = append(b.InfoKeys, "log level")
b.LogLevel = logLevel
// log proxy settings
// TODO: It would be good to show this but Vault has, or will soon, address
// the fact that this can log users/passwords if they are part of the proxy
// URL. When they change things to address that we should update the below
// logic and re-enable.
/*
proxyCfg := httpproxy.FromEnvironment()
b.Logger.Info("proxy environment", "http_proxy", proxyCfg.HTTPProxy,
"https_proxy", proxyCfg.HTTPSProxy, "no_proxy", proxyCfg.NoProxy)
*/
// Setup gorm logging
return nil
}
func (b *Server) ReleaseLogGate() {
// Release the log gate.
b.Logger.(hclog.OutputResettable).ResetOutputWithFlush(&hclog.LoggerOptions{
Output: b.logOutput,
}, b.GatedWriter)
}
func (b *Server) StorePidFile(pidPath string) error {
// Quit fast if no pidfile
if pidPath == "" {
return nil
}
// Open the PID file
pidFile, err := os.OpenFile(pidPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("could not open pid file: %w", err)
}
defer pidFile.Close()
// Write out the PID
pid := os.Getpid()
_, err = pidFile.WriteString(fmt.Sprintf("%d", pid))
if err != nil {
return fmt.Errorf("could not write to pid file: %w", err)
}
b.ShutdownFuncs = append(b.ShutdownFuncs, func() error {
if err := b.RemovePidFile(pidPath); err != nil {
return fmt.Errorf("Error deleting the PID file: %w", err)
}
return nil
})
return nil
}
func (b *Server) RemovePidFile(pidPath string) error {
if pidPath == "" {
return nil
}
return os.Remove(pidPath)
}
func (b *Server) SetupMetrics(ui cli.Ui, telemetry *configutil.Telemetry) error {
// TODO: Figure out a user-agent we want to use for the last param
// TODO: Do we want different names for different components?
var err error
b.InmemSink, _, b.PrometheusEnabled, err = configutil.SetupTelemetry(&configutil.SetupTelemetryOpts{
Config: telemetry,
Ui: ui,
ServiceName: "watchtower",
DisplayName: "Watchtower",
UserAgent: "watchtower",
})
if err != nil {
return fmt.Errorf("Error initializing telemetry: %w", err)
}
return nil
}
func (b *Server) PrintInfo(ui cli.Ui, mode string) {
b.InfoKeys = append(b.InfoKeys, "version")
verInfo := version.Get()
b.Info["version"] = verInfo.FullVersionNumber(false)
if verInfo.Revision != "" {
b.Info["version sha"] = strings.Trim(verInfo.Revision, "'")
b.InfoKeys = append(b.InfoKeys, "version sha")
}
b.InfoKeys = append(b.InfoKeys, "cgo")
b.Info["cgo"] = "disabled"
if version.CgoEnabled {
b.Info["cgo"] = "enabled"
}
// Server configuration output
padding := 24
sort.Strings(b.InfoKeys)
ui.Output(fmt.Sprintf("==> Watchtower %s configuration:\n", mode))
for _, k := range b.InfoKeys {
ui.Output(fmt.Sprintf(
"%s%s: %s",
strings.Repeat(" ", padding-len(k)),
strings.Title(k),
b.Info[k]))
}
ui.Output("")
// Output the header that the server has started
if !b.CombineLogs {
ui.Output(fmt.Sprintf("==> Watchtower %s started! Log data will stream in below:\n", mode))
}
}
func (b *Server) SetupListeners(ui cli.Ui, config *configutil.SharedConfig) error {
// Initialize the listeners
b.Listeners = make([]*ServerListener, 0, len(config.Listeners))
// Make sure we close everything before we exit
// If we successfully started a controller we'll have done this anyways so
// we ignore errors
b.ShutdownFuncs = append(b.ShutdownFuncs, func() error {
for _, ln := range b.Listeners {
ln.Mux.Close()
}
return nil
})
b.ReloadFuncsLock.Lock()
defer b.ReloadFuncsLock.Unlock()
for i, lnConfig := range config.Listeners {
// Override for now
// TODO: Way to configure
lnConfig.TLSCipherSuites = []uint16{
// 1.3
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
// 1.2
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
}
lnMux, props, reloadFunc, err := NewListener(lnConfig, b.Logger, ui)
if err != nil {
return fmt.Errorf("Error initializing listener of type %s: %w", lnConfig.Type, err)
}
// X-Forwarded-For props
{
if len(lnConfig.XForwardedForAuthorizedAddrs) > 0 {
props["x_forwarded_for_authorized_addrs"] = fmt.Sprintf("%v", lnConfig.XForwardedForAuthorizedAddrs)
props["x_forwarded_for_reject_not_present"] = strconv.FormatBool(lnConfig.XForwardedForRejectNotPresent)
props["x_forwarded_for_hop_skips"] = "0"
}
if lnConfig.XForwardedForHopSkips > 0 {
props["x_forwarded_for_hop_skips"] = fmt.Sprintf("%d", lnConfig.XForwardedForHopSkips)
}
}
if reloadFunc != nil {
relSlice := b.ReloadFuncs["listener|"+lnConfig.Type]
relSlice = append(relSlice, reloadFunc)
b.ReloadFuncs["listener|"+lnConfig.Type] = relSlice
}
if lnConfig.MaxRequestSize == 0 {
lnConfig.MaxRequestSize = globals.DefaultMaxRequestSize
}
props["max_request_size"] = fmt.Sprintf("%d", lnConfig.MaxRequestSize)
if lnConfig.MaxRequestDuration == 0 {
lnConfig.MaxRequestDuration = globals.DefaultMaxRequestDuration
}
props["max_request_duration"] = fmt.Sprintf("%s", lnConfig.MaxRequestDuration.String())
b.Listeners = append(b.Listeners, &ServerListener{
Mux: lnMux,
Config: lnConfig,
})
props["purpose"] = strings.Join(lnConfig.Purpose, ",")
// Store the listener props for output later
key := fmt.Sprintf("listener %d", i+1)
propsList := make([]string, 0, len(props))
for k, v := range props {
propsList = append(propsList, fmt.Sprintf(
"%s: %q", k, v))
}
sort.Strings(propsList)
b.InfoKeys = append(b.InfoKeys, key)
b.Info[key] = fmt.Sprintf(
"%s (%s)", lnConfig.Type, strings.Join(propsList, ", "))
}
return nil
}
func (b *Server) SetupKMSes(ui cli.Ui, config *configutil.SharedConfig, purposes []string) error {
for _, kms := range config.Seals {
for _, purpose := range kms.Purpose {
purpose = strings.ToLower(purpose)
switch purpose {
case "":
return errors.New("KMS block missing 'purpose'")
case "controller", "worker-auth", "config":
default:
return fmt.Errorf("Unknown KMS purpose %q", kms.Purpose)
}
kmsLogger := b.Logger.ResetNamed(fmt.Sprintf("kms-%s-%s", purpose, kms.Type))
wrapper, wrapperConfigError := configutil.ConfigureWrapper(kms, &b.InfoKeys, &b.Info, kmsLogger)
if wrapperConfigError != nil {
if !errwrap.ContainsType(wrapperConfigError, new(logical.KeyNotFoundError)) {
return fmt.Errorf(
"Error parsing KMS configuration: %s", wrapperConfigError)
}
}
if wrapper == nil {
return fmt.Errorf(
"After configuration nil KMS returned, KMS type was %s", kms.Type)
}
if purpose == "controller" {
b.ControllerKMS = wrapper
} else {
b.WorkerAuthKMS = wrapper
}
// Ensure that the seal finalizer is called, even if using verify-only
b.ShutdownFuncs = append(b.ShutdownFuncs, func() error {
if err := wrapper.Finalize(context.Background()); err != nil {
return fmt.Errorf("Error finalizing kms of type %s and purpose %s: %v", kms.Type, purpose, err)
}
return nil
})
}
}
// prepare a secure random reader
var err error
b.SecureRandomReader, err = configutil.CreateSecureRandomReaderFunc(config, b.ControllerKMS)
if err != nil {
return err
}
// This might not be the _best_ place for this but we have access to config
// here
b.Info["mlock"] = fmt.Sprintf(
"supported: %v, enabled: %v",
mlock.Supported(), !config.DisableMlock && mlock.Supported())
b.InfoKeys = append(b.InfoKeys, "mlock")
return nil
}
func (b *Server) RunShutdownFuncs() error {
var mErr *multierror.Error
for _, f := range b.ShutdownFuncs {
if err := f(); err != nil {
mErr = multierror.Append(mErr, err)
}
}
return mErr.ErrorOrNil()
}
func (b *Server) CreateDevDatabase(dialect string) error {
c, url, container, err := db.InitDbInDocker(dialect)
if err != nil {
c()
return fmt.Errorf("unable to start dev database with dialect %s: %w", dialect, err)
}
b.DevDatabaseCleanupFunc = c
b.DevDatabaseUrl = url
b.InfoKeys = append(b.InfoKeys, "dev database url")
b.Info["dev database url"] = b.DevDatabaseUrl
if container != "" {
b.InfoKeys = append(b.InfoKeys, "dev database container")
b.Info["dev database container"] = strings.TrimPrefix(container, "/")
}
dbase, err := gorm.Open(dialect, url)
if err != nil {
c()
return fmt.Errorf("unable to create db object with dialect %s: %w", dialect, err)
}
b.Database = dbase
gorm.LogFormatter = db.GetGormLogFormatter(b.Logger)
b.Database.SetLogger(db.GetGormLogger(b.Logger))
b.Database.LogMode(true)
rw := db.New(b.Database)
repo, err := iam.NewRepository(rw, rw, b.ControllerKMS)
if err != nil {
c()
return fmt.Errorf("unable to create repo for org id: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-b.ShutdownCh
cancel()
}()
var orgScope *iam.Scope
if b.DefaultOrgId != "" {
orgScope, err = repo.LookupScope(ctx, b.DefaultOrgId)
if err != nil {
c()
return fmt.Errorf("error looking up existing scope with org ID %q: %w", b.DefaultOrgId, err)
}
}
if orgScope == nil {
orgScope, err = iam.NewOrg()
if err != nil {
c()
return fmt.Errorf("error creating new org scope: %w", err)
}
orgScope, err = repo.CreateScope(ctx, orgScope, iam.WithPublicId(b.DefaultOrgId))
if err != nil {
c()
return fmt.Errorf("error persisting new org scope: %w", err)
}
if b.DefaultOrgId != "" {
if orgScope.GetPublicId() != b.DefaultOrgId {
c()
return fmt.Errorf("expected org ID %q, got %q after persisting", b.DefaultOrgId, orgScope.GetPublicId())
}
} else {
b.DefaultOrgId = orgScope.GetPublicId()
}
}
ar, err := iam.NewRole(orgScope.PublicId)
if err != nil {
return fmt.Errorf("error creating in memory role for anon authen: %w", err)
}
authenRole, err := repo.CreateRole(ctx, ar, iam.WithDescription("role for anon authen"))
if err != nil {
return fmt.Errorf("error creating role for anon authen: %w", err)
}
if _, err := repo.AddRoleGrants(ctx, authenRole.PublicId, authenRole.Version, []string{"type=auth-method;actions=list,authenticate"}); err != nil {
return fmt.Errorf("error creating grant for anon authen: %w", err)
}
if _, err := repo.AddPrincipalRoles(ctx, authenRole.PublicId, authenRole.Version+1, []string{"u_anon"}, nil); err != nil {
return fmt.Errorf("error adding principal to role for anon authen: %w", err)
}
pr, err := iam.NewRole(orgScope.PublicId)
if err != nil {
return fmt.Errorf("error creating in memory role for default dev grants: %w", err)
}
defPermsRole, err := repo.CreateRole(ctx, pr, iam.WithDescription("role for def grants"))
if err != nil {
return fmt.Errorf("error creating role for default dev grants: %w", err)
}
if _, err := repo.AddRoleGrants(ctx, defPermsRole.PublicId, defPermsRole.Version, []string{"id=*;actions=*"}); err != nil {
return fmt.Errorf("error creating grant for default dev grants: %w", err)
}
if _, err := repo.AddPrincipalRoles(ctx, defPermsRole.PublicId, defPermsRole.Version+1, []string{"u_auth"}, nil); err != nil {
return fmt.Errorf("error adding principal to role for default dev grants: %w", err)
}
// TODO: Remove this when Auth Account repo is in place.
authenticate.Scope = orgScope.GetPublicId()
insert := `insert into auth_method
(public_id, scope_id)
values
($1, $2);`
amId := b.DevAuthMethodId
if amId == "" {
amId = "am_1234567890"
}
authenticate.RWDb.Store(rw)
_, err = b.Database.DB().Exec(insert, amId, orgScope.GetPublicId())
if err != nil {
c()
return err
}
b.InfoKeys = append(b.InfoKeys, "dev org id", "dev auth method id")
b.Info["dev org id"] = b.DefaultOrgId
b.Info["dev auth method id"] = amId
return nil
}
func (b *Server) DestroyDevDatabase() error {
if b.Database != nil {
b.Database.Close()
}
if b.DevDatabaseCleanupFunc != nil {
return b.DevDatabaseCleanupFunc()
}
return errors.New("no dev database cleanup function found")
}