github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/pgwire/hba_conf.go (about)

     1  // Copyright 2019 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package pgwire
    12  
    13  import (
    14  	"context"
    15  	"net"
    16  	"net/http"
    17  	"sort"
    18  	"strings"
    19  
    20  	"github.com/cockroachdb/cockroach/pkg/clusterversion"
    21  	"github.com/cockroachdb/cockroach/pkg/security"
    22  	"github.com/cockroachdb/cockroach/pkg/settings"
    23  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/hba"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
    27  	"github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented"
    28  	"github.com/cockroachdb/cockroach/pkg/util/log"
    29  	"github.com/cockroachdb/errors"
    30  )
    31  
    32  // This file contains the logic for the configuration of HBA rules.
    33  //
    34  // In a nutshell, administrators customize the cluster setting
    35  // `server.host_based_authentication.configuration`; each time they
    36  // do so, all the nodes parse this configuration and re-initialize
    37  // their authentication rules (a list of entries) from the setting.
    38  //
    39  // If the cluster setting is not initialized, or when it is assigned
    40  // the empty string, a special "default" configuration is used
    41  // instead:
    42  //
    43  //     host all root all cert           # require certs for root
    44  //     host all all  all cert-password  # require certs or password for everyone else
    45  //
    46  // (In fact, the first line `host all root all cert` is always
    47  // inserted at the start of any custom configuration, as a safeguard.)
    48  //
    49  // The HBA configuration is an ordered list of rules. Each time
    50  // a client attempts to connect, the server scans the
    51  // rules from the beginning of the list. The first rule that
    52  // matches the connection decides how to authenticate.
    53  //
    54  // The syntax is inspired/derived from that of PostgreSQL's pg_hba.conf:
    55  // https://www.postgresql.org/docs/12/auth-pg-hba-conf.html
    56  //
    57  // For now, CockroachDB only supports the following syntax:
    58  //
    59  //     host  all  <user[,user]...>  <IP-address/mask-length>  <auth-method>
    60  //
    61  // The matching rules are as follows:
    62  // - A rule matches if the connecting username matches either of the
    63  //   usernames listed in the rule, or if the pseudo-user 'all' is
    64  //   present in the user column.
    65  // - A rule matches if the connecting client's IP address is included
    66  //   in the network address specified in the CIDR notation.
    67  //
    68  
    69  // serverHBAConfSetting is the name of the cluster setting that holds
    70  // the HBA configuration.
    71  const serverHBAConfSetting = "server.host_based_authentication.configuration"
    72  
    73  // connAuthConf is the cluster setting that holds the HBA
    74  // configuration.
    75  var connAuthConf = func() *settings.StringSetting {
    76  	s := settings.RegisterValidatedStringSetting(
    77  		serverHBAConfSetting,
    78  		"host-based authentication configuration to use during connection authentication",
    79  		"",
    80  		checkHBASyntaxBeforeUpdatingSetting,
    81  	)
    82  	s.SetVisibility(settings.Public)
    83  	return s
    84  }()
    85  
    86  // loadLocalAuthConfigUponRemoteSettingChange initializes the local
    87  // node's cache of the auth configuration each time the cluster
    88  // setting is updated.
    89  func loadLocalAuthConfigUponRemoteSettingChange(
    90  	ctx context.Context, server *Server, st *cluster.Settings,
    91  ) {
    92  	val := connAuthConf.Get(&st.SV)
    93  
    94  	// An empty HBA configuration is special and means "use the
    95  	// default".
    96  	conf := DefaultHBAConfig
    97  	if val != "" {
    98  		var err error
    99  		conf, err = ParseAndNormalize(val)
   100  		if err != nil {
   101  			// The default is also used if the node is unable to load the
   102  			// config from the cluster setting.
   103  			log.Warningf(ctx, "invalid %s: %v", serverHBAConfSetting, err)
   104  			conf = DefaultHBAConfig
   105  		}
   106  	}
   107  	server.auth.Lock()
   108  	defer server.auth.Unlock()
   109  	server.auth.conf = conf
   110  }
   111  
   112  // checkHBASyntaxBeforeUpdatingSetting is run by the SQL gateway each
   113  // time a SQL client attempts to update the cluster setting.
   114  // It is also used when initially loading the default value.
   115  func checkHBASyntaxBeforeUpdatingSetting(values *settings.Values, s string) error {
   116  	if s == "" {
   117  		// An empty configuration is always valid.
   118  		return nil
   119  	}
   120  	// Note: we parse, but do not normalize, here, so as to
   121  	// check for unsupported features in the input.
   122  	conf, err := hba.Parse(s)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	if len(conf.Entries) == 0 {
   128  		// If the string was not empty, the user likely intended to have
   129  		// *something* in the configuration, so us not finding anything
   130  		// likely indicates either a parsing bug, or that the user
   131  		// mistakenly put only comments in their config.
   132  		return errors.WithHint(errors.New("no entries"),
   133  			"To use the default configuration, assign the empty string ('').")
   134  	}
   135  
   136  	// Retrieve the cluster version handle. We'll need to check the current cluster version.
   137  	var vh clusterversion.Handle
   138  	if values != nil {
   139  		vh = values.Opaque().(clusterversion.Handle)
   140  	}
   141  
   142  	for _, entry := range conf.Entries {
   143  		switch entry.ConnType {
   144  		case hba.ConnHostAny:
   145  		case hba.ConnLocal:
   146  			if vh != nil &&
   147  				!vh.IsActive(context.TODO(), clusterversion.VersionAuthLocalAndTrustRejectMethods) {
   148  				return pgerror.Newf(pgcode.ObjectNotInPrerequisiteState,
   149  					`authentication rule type 'local' requires all nodes to be upgraded to %s`,
   150  					clusterversion.VersionByKey(clusterversion.VersionAuthLocalAndTrustRejectMethods),
   151  				)
   152  			}
   153  		default:
   154  			return unimplemented.Newf("hba-type-"+entry.ConnType.String(),
   155  				"unsupported connection type: %s", entry.ConnType)
   156  		}
   157  		for _, db := range entry.Database {
   158  			if !db.IsKeyword("all") {
   159  				return errors.WithHint(
   160  					unimplemented.New("hba-per-db", "per-database HBA rules are not supported"),
   161  					"Use the special value 'all' (without quotes) to match all databases.")
   162  			}
   163  		}
   164  
   165  		if entry.ConnType != hba.ConnLocal {
   166  			// Verify the user is not requesting hostname-based validation,
   167  			// which is not yet implemented.
   168  			addrOk := true
   169  			switch t := entry.Address.(type) {
   170  			case *net.IPNet:
   171  			case hba.AnyAddr:
   172  			case hba.String:
   173  				addrOk = t.IsKeyword("all")
   174  			default:
   175  				addrOk = false
   176  			}
   177  			if !addrOk {
   178  				return errors.WithHint(
   179  					unimplemented.New("hba-hostnames", "hostname-based HBA rules are not supported"),
   180  					"List the numeric CIDR notation instead, for example: 127.0.0.1/8.\n"+
   181  						"Alternatively, use 'all' (without quotes) for any IPv4/IPv6 address.")
   182  			}
   183  		}
   184  
   185  		// Verify that the auth method is supported.
   186  		method, ok := hbaAuthMethods[entry.Method.Value]
   187  		if !ok || method.fn == nil {
   188  			return errors.WithHintf(unimplemented.Newf("hba-method-"+entry.Method.Value,
   189  				"unknown auth method %q", entry.Method.Value),
   190  				"Supported methods: %s", listRegisteredMethods())
   191  		}
   192  		// Verify that the cluster setting is at least the required version.
   193  		if vh != nil && !vh.IsActive(context.TODO(), method.minReqVersion) {
   194  			return pgerror.Newf(pgcode.ObjectNotInPrerequisiteState,
   195  				`authentication method '%s' requires all nodes to be upgraded to %s`,
   196  				entry.Method.Value,
   197  				clusterversion.VersionByKey(method.minReqVersion))
   198  		}
   199  		// Run the per-method validation.
   200  		if check := hbaCheckHBAEntries[entry.Method.Value]; check != nil {
   201  			if err := check(entry); err != nil {
   202  				return err
   203  			}
   204  		}
   205  	}
   206  	return nil
   207  }
   208  
   209  // ParseAndNormalize calls hba.ParseAndNormalize and also ensures the
   210  // configuration starts with a rule that authenticates the root user
   211  // with client certificates.
   212  //
   213  // This prevents users from shooting themselves in the foot and making
   214  // root not able to login, thus disallowing anyone from fixing the HBA
   215  // configuration.
   216  func ParseAndNormalize(val string) (*hba.Conf, error) {
   217  	conf, err := hba.ParseAndNormalize(val)
   218  	if err != nil {
   219  		return conf, err
   220  	}
   221  
   222  	if len(conf.Entries) == 0 || !conf.Entries[0].Equivalent(rootEntry) {
   223  		entries := make([]hba.Entry, 1, len(conf.Entries)+1)
   224  		entries[0] = rootEntry
   225  		entries = append(entries, conf.Entries...)
   226  		conf.Entries = entries
   227  	}
   228  
   229  	// Lookup and cache the auth methods.
   230  	for i := range conf.Entries {
   231  		method := conf.Entries[i].Method.Value
   232  		methodEntry, ok := hbaAuthMethods[method]
   233  		if !ok {
   234  			// TODO(knz): Determine if an error should be reported
   235  			// upon unknown auth methods.
   236  			// See: https://github.com/cockroachdb/cockroach/issues/43716
   237  			return nil, errors.Errorf("unknown auth method %s", method)
   238  		}
   239  		conf.Entries[i].MethodFn = methodEntry.methodInfo
   240  	}
   241  
   242  	return conf, nil
   243  }
   244  
   245  var insecureEntry = hba.Entry{
   246  	ConnType: hba.ConnHostAny,
   247  	User:     []hba.String{{Value: "all", Quoted: false}},
   248  	Address:  hba.AnyAddr{},
   249  	Method:   hba.String{Value: "--insecure"},
   250  }
   251  
   252  var rootEntry = hba.Entry{
   253  	ConnType: hba.ConnHostAny,
   254  	User:     []hba.String{{Value: security.RootUser, Quoted: false}},
   255  	Address:  hba.AnyAddr{},
   256  	Method:   hba.String{Value: "cert-password"},
   257  	Input:    "host  all root all cert-password # CockroachDB mandatory rule",
   258  }
   259  
   260  // DefaultHBAConfig is used when the stored HBA configuration string
   261  // is empty or invalid.
   262  var DefaultHBAConfig = func() *hba.Conf {
   263  	loadDefaultMethods()
   264  	conf, err := ParseAndNormalize(`
   265  host  all all  all cert-password # built-in CockroachDB default
   266  local all all      password      # built-in CockroachDB default
   267  `)
   268  	if err != nil {
   269  		panic(err)
   270  	}
   271  	return conf
   272  }()
   273  
   274  // GetAuthenticationConfiguration retrieves the current applicable
   275  // authentication configuration.
   276  //
   277  // This is guaranteed to return a valid configuration. Additionally,
   278  // the various setters for the configuration also pass through
   279  // ParseAndNormalize(), whereby an entry is always present at the start,
   280  // to enable root to log in with a valid client cert.
   281  //
   282  // The data returned by this method is also observable via the debug
   283  // endpoint /debug/hba_conf.
   284  func (s *Server) GetAuthenticationConfiguration() *hba.Conf {
   285  	s.auth.RLock()
   286  	auth := s.auth.conf
   287  	s.auth.RUnlock()
   288  
   289  	if auth == nil {
   290  		// This can happen when using the value for the first time before
   291  		// the cluster setting has ever been set.
   292  		auth = DefaultHBAConfig
   293  	}
   294  	return auth
   295  }
   296  
   297  // RegisterAuthMethod registers an AuthMethod for pgwire
   298  // authentication and for use in HBA configuration.
   299  //
   300  // The minReqVersion is checked upon configuration to verify whether
   301  // the current active cluster version is at least the version
   302  // specified.
   303  //
   304  // The validConnTypes is checked during rule matching when accepting
   305  // connections: if the connection type is not accepted by the auth
   306  // method, authentication is refused upfront. For example, the "cert"
   307  // method requires SSL; if a rule specifies "host .... cert" and the
   308  // client connects without SSL, the authentication is refused.
   309  // (To express "cert on SSL, password on non-SSL", the HBA conf
   310  // can list 'hostssl ... cert; hostnossl .... password' instead.)
   311  //
   312  // The checkEntry method, if provided, is called upon configuration
   313  // the cluster setting in the SQL client which attempts to change the
   314  // configuration. It can block the configuration if e.g. the syntax is
   315  // invalid.
   316  func RegisterAuthMethod(
   317  	method string,
   318  	fn AuthMethod,
   319  	minReqVersion clusterversion.VersionKey,
   320  	validConnTypes hba.ConnType,
   321  	checkEntry CheckHBAEntry,
   322  ) {
   323  	hbaAuthMethods[method] = authMethodEntry{methodInfo{validConnTypes, fn}, minReqVersion}
   324  	if checkEntry != nil {
   325  		hbaCheckHBAEntries[method] = checkEntry
   326  	}
   327  }
   328  
   329  // listsupportedMethods returns a sorted, comma-delimited list
   330  // of registered AuthMethods.
   331  func listRegisteredMethods() string {
   332  	methods := make([]string, 0, len(hbaAuthMethods))
   333  	for method := range hbaAuthMethods {
   334  		methods = append(methods, method)
   335  	}
   336  	sort.Strings(methods)
   337  	return strings.Join(methods, ", ")
   338  }
   339  
   340  var (
   341  	hbaAuthMethods     = map[string]authMethodEntry{}
   342  	hbaCheckHBAEntries = map[string]CheckHBAEntry{}
   343  )
   344  
   345  type authMethodEntry struct {
   346  	methodInfo
   347  	minReqVersion clusterversion.VersionKey
   348  }
   349  
   350  type methodInfo struct {
   351  	validConnTypes hba.ConnType
   352  	fn             AuthMethod
   353  }
   354  
   355  // CheckHBAEntry defines a method for validating an hba Entry upon
   356  // configuration of the cluster setting by a SQL client.
   357  type CheckHBAEntry func(hba.Entry) error
   358  
   359  // HBADebugFn exposes the computed HBA configuration via the debug
   360  // interface, for inspection by tests.
   361  func (s *Server) HBADebugFn() http.HandlerFunc {
   362  	return func(w http.ResponseWriter, req *http.Request) {
   363  		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   364  
   365  		auth := s.GetAuthenticationConfiguration()
   366  
   367  		_, _ = w.Write([]byte("# Active authentication configuration on this node:\n"))
   368  		_, _ = w.Write([]byte(auth.String()))
   369  	}
   370  }